diff options
| author | Cliff Click <cliffc@acm.org> | 2026-08-10 11:11:18 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-11-14 16:23:39 -0800 |
| commit | 207febef9924c8fa5391185e75d42a7ea480bb38 (patch) | |
| tree | 68ec6225a89fa1178c32fdf659b9b4f2c3327288 | |
| parent | 259f60b105eb770bc6f7e3bd36d91f2b217e9315 (diff) | |
| download | Simple-207febef9924c8fa5391185e75d42a7ea480bb38.tar.gz Simple-207febef9924c8fa5391185e75d42a7ea480bb38.zip | |
Chapter 31: Software Drawing, Time, and Graphical Gameslinear
Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com>
Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com>
Co-authored-by: Symious <14933944+symious@users.noreply.github.com>
Co-authored-by: Jun Kawasaki <jun784@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
152 files changed, 7633 insertions, 581 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..5f2120e0 --- /dev/null +++ b/docs/chapter31/31-grammar.md @@ -0,0 +1,273 @@ +# 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 ']' arrayInitializer? | arrayInitializer | callSuffix)? + ; +// With no explicit length, type must end in [] or [~] and the initializer is a list. +arrayInitializer + : '{' (assignment (',' assignment)* ','?)? '}' + | '{' ('int' | 'i32' | 'i64' | 'u32') IDENTIFIER '->' statement* '}' + ; + +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`, `arrayInitializer` | [`Allocations.parseNew`](../../src/main/java/com/seaofnodes/simple/parser/Allocations.java), [`ArrayInitializers`](../../src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java); private initialization, list checks, and generator loops | +| `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..f689944f --- /dev/null +++ b/docs/chapter31/README.md @@ -0,0 +1,185 @@ +# Chapter 31: Software Drawing, Time, and Graphical Games + +[Previous: Chapter 30 — Text Formatting and Diagnostics](../chapter30/README.md) + +In this chapter we use small graphical games to drive the language and library +work. We add graphics, sound, and time support, with a few small C adapters +for native services. We also add array initializers and read-only arrays, hex, +octal parsing, move constant arrays into the ELF constant pool. We then write +a few games, with the drawing and game logic written in Simple. + + +## Games and demos + +[Clock](examples/Clock.smp) is a classic clock demo. It shows local time as +digital digits or an analog dial. Click its button or press Space to switch +views. It combines timers, mouse input, text, and drawing in one event loop. + +[Graphical Tic-tac-toe](examples/TicTacToeGraphical.smp) accepts mouse +clicks or keys 1 through 9. It highlights the winning line and plays move and +result sounds. Game logic is the same as in the terminal version from +the prior chapter, with the graphics added to the event loop. + +[Breakout](examples/Breakout.smp) has a ball, a paddle, and a wall of +bricks. Move the paddle with the arrow keys or mouse. The ball's bounce angle +depends on where it hits the paddle. Deeper bricks score more points, make +higher-pitched sounds, and increase the ball's speed. Hitting the *back* of a +brick gives a one-time larger speed boost. Clear all the bricks before using +up three balls. + + +## Graphics and sound + +Graphics and sound are optional. The Simple language and compiler do not need +them. The `sys` library includes bindings to the native C backends, but you do +not need those backends for programs without a display or sound. Their +dependencies download with the existing `make init` setup command. + +Each pixel holds 24 bits of RGB color in a `u32`. A pixel buffer is a `u32[]`. + +[`sys.draw.Canvas`](../../src/main/smp/sys/draw/Canvas.smp) draws pixels, rectangles, +circles, thick lines, and text into a `u32[]` pixel buffer. Shapes clip to the +canvas. The 3x5 font includes digits, letters, and punctuation such as colons +and slashes. Its glyphs are stored in a read-only array. + +`sys.draw` supplies a few color constants plus `rgb(r,g,b)` to clamp and pack +computed color components. + +`sys.draw.Button` handles button hover, press, release, and disabled states. + +`sys.gfx` opens a window, presents pixels, and reads keyboard and mouse input. +The window backend uses MiniFB. `gfx.wait(timeout_ms)` waits for input or a +timeout, so animation need not spin continuously. + +`sys.audio` plays built-in cues or sample arrays generated in Simple. Its +optional miniaudio backend plays sound while the game continues. The games +can run silently when audio is unavailable. + +See [drawing](drawing.md), [windows](graphics.md), and +[audio](audio.md) for the APIs. + + +## Time library + +[`sys.time`](../../src/main/smp/sys/time.smp) provides wall-clock time, monotonic +time, sleep, calendar conversion, ISO date parsing, and formatting. Use +wall-clock time for dates. Use monotonic time for elapsed time and deadlines - +perfect for games. + +Instants and durations normally use `i64` milliseconds. Constants show their +units directly: `SECOND=1000`, `MINUTE=60*SECOND`, and `HOUR=60*MINUTE`. + +`sys.time.Timers` supports one-shot and repeating timers. The event loop polls +for due timer IDs; no timer thread or callback is needed. This might be a mindset +change for you game writers, it depends on what era you started writing them. +Here it is all single-threaded and event-loop driven - but when there is nothing +left to do, you need to wait for an event. + +Time also works in string interpolation: + +```simple +sys.io.p("UTC %tT(sys.time.now())\n"); +``` + +Millisecond parameters and fields end in `_ms` to distinguish them from other +integers. + +See [time and timers](time.md) for local-time formatting and other forms. + + +## Array initializers + +An array can use a list of values or an expression evaluated for each index: + +```simple +val primes = new int[]{2,3,5,7}; +val squares = new int[10]{int i -> i*i;}; +val names = new u8[~][~2]{"Alice","Bob"}; +``` + +The generator runs once per element, from index zero through length minus one. +Its body can contain statements and return the element value. It can read +surrounding locals but cannot reassign them; it is not a true function. + +If the array has a fixed initialization list, then the length can be inferred. + + +`[~n]` (or `[~]` with a list) makes an array read-only. `val` prevents +reassignment of the array variable, but not changes to its contents. In C, +this is the classic `const u8* const` distinction: the first `const` protects +the contents; the second protects the pointer. Array initialization normally +runs at runtime; read-only does not require compile-time evaluation. + +When the compiler knows a read-only integer array's complete contents, it can +emit them as static ELF data. Identical constant arrays can share storage, +just like strings. Canvas's font now uses this path instead of allocating +and filling its table at startup. The trigger is a `TypeConAry` constant type, +regardless of which node computes it. Constant lists are the first new +producer; generators still run at runtime. + +Initializers also allow arrays of non-null references: every element is +supplied before the array becomes accessible. In the example, `names` is a +read-only array of two non-null, read-only strings. + + +## Math support and intrinsics + +[`sys.math`](../../src/main/smp/sys/math.smp) provides the `PI` and `E` constants, +plus two operations: + +```simple +i64 bits = sys.math.f64Bits(1.0); // 0x3ff0000000000000 +f64 value = sys.math.f64FromBits(bits); // 1.0 +``` + +They *reinterpret* the same 64 bits, preserving signed zero and NaN payloads. +Ordinary assignments between integer and floating-point types still perform +numeric conversion. + +These functions introduce compiler intrinsics: C functions with fixed meanings +that the compiler recognizes and replaces with internal operations. For the +bit-reinterpretation operations, Simple uses plain register moves. Calls whose +targets remain unknown at compile time use the C implementations. Recognition +and expansion live in `Intrinsic.java`. + +See [the intrinsic contract](intrinsics.md) for the implemented behavior. + +Simple also now recognizes hexadecimal (`0xff0000`) and leading-zero octal +(`025755`) integer literals. + + +## Compiler fixes + +- Syntax errors raised through JUnit now show the source line and error pointer. + Actual compiler failures still report Java stack traces. +- Integer memory operations on x86 use the correct register class. This fixes + the instruction-encoding failure exposed by Dijkstra. +- Floating-point comparisons handle NaNs correctly in branches and Boolean + results. Floating-point negation also handles signed zero correctly. +- Array-initializer memory tracking no longer adds an unrelated function to + the returned result. Loop-load optimization checks that the pointer is + available before moving a load. +- Imported read-only primitive arrays retain their types. Object files can + represent more than 255 distinct memory regions. +- Floating-point constants and register copies preserve raw NaN bits. + +## Build and run + +From the `chapter31` directory: + +```sh +make libs CTAGS= # Download dependencies once. +make run-graphical-tictactoe CTAGS= +make run-clock CTAGS= +make run-breakout CTAGS= +``` + +Dependencies stay cached across `make clean`. The run targets build the needed +window and audio backends. Use `graphical-tictactoe`, `clock-example`, or +`breakout` to build an executable without opening a window. + +Run the ordinary tests without a display or audio device: + +```sh +make tests_nographics CTAGS= +``` 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..b99c1bf8 --- /dev/null +++ b/docs/chapter31/drawing.md @@ -0,0 +1,103 @@ +# 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`. +The font includes all printable ASCII punctuation except `@`, `&`, and `$`, +which use the question-mark fallback. Colons and slashes support clock and date +labels; quotes, brackets, arithmetic signs, and other small symbols fit in 3×5. + +## 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/Breakout.smp b/docs/chapter31/examples/Breakout.smp new file mode 100644 index 00000000..b1d0b443 --- /dev/null +++ b/docs/chapter31/examples/Breakout.smp @@ -0,0 +1,364 @@ +// Clear 24 x 8 bricks. Arrows or mouse move; Space/click serves; P pauses; +// R restarts; Q/Escape exits. Any argument runs ten frames as a smoke test. +u32 WIDTH = 1008, HEIGHT = 760, COLS = 24, ROWS = 8; +int LEFT = 24, RIGHT = 984, TOP = 84, BOTTOM = 718; +int BRICK_TOP = 152, BRICK_DX = 40, BRICK_DY = 22, BRICK_W = 36, BRICK_H = 18; +int RADIUS = 5, PADDLE_HALF = 56, PADDLE_Y = 674; +int READY = 0, PLAY = 1, WON = 2, LOST = 3; +int KEY_LEFT = 263, KEY_RIGHT = 262; // MiniFB key codes; gfx.LEFT is a mouse button. +i64 STEP_ms = 8, FRAME_ms = 16; +u32 BACKGROUND = 0x0b1220, WHITE = 0xeef7ff, MUTED = 0x8aabc4, CYAN = 0x4ce0ef; +val COLORS = new u32[~]{0xff5268,0xff8050,0xffb548,0xffdb58, + 0x9bdd58,0x45d8b0,0x49b9f5,0x8d94ff}; +val DIGITS = new u8[~][~]{"0","1","2","3","4","5","6","7","8","9"}; + +struct Game { + u8[] bricks = new u8[COLS*ROWS]{int i -> 1;}; + int score = 0, remaining = COLS*ROWS, lives = 3, depth = 1, phase = READY; + bool boosted = false, backWall = false, paused = false; + f64 x = 504.0, y = 663.0, dx = 0.4, dy = -0.916515138991168; + + // Fixed 8 ms simulation, split further to travel at most two pixels per step. + // Even at maximum boosted speed a ball cannot tunnel through an 18 px brick. + val _tick = { int direction -> + if( paused || phase == WON || phase == LOST ) return 0; + _paddle(paddle+direction*6.4); + if( phase == READY ) { x = paddle; y = PADDLE_Y-RADIUS-6; return 0; } + f64 seconds = 0.008; // 8ms + while( seconds > 0.000000001 && phase == PLAY ) { + f64 dt = 2.0/speed; + if( dt > seconds ) dt = seconds; + seconds -= dt; + f64 oldX = x, oldY = y; + x += dx*speed*dt; + y += dy*speed*dt; + if( x < LEFT+RADIUS && dx < 0.0 ) { + x = 2.0*(LEFT+RADIUS)-x; dx = -dx; _cue(1); + } + if( x > RIGHT-RADIUS && dx > 0.0 ) { + x = 2.0*(RIGHT-RADIUS)-x; dx = -dx; _cue(1); + } + if( y < TOP+RADIUS && dy < 0.0 ) { + y = 2.0*(TOP+RADIUS)-y; dy = -dy; + backWall = true; _cue(1); + } + if( dy > 0.0 && oldY <= PADDLE_Y-RADIUS && y >= PADDLE_Y-RADIUS && + paddle-PADDLE_HALF-RADIUS <= x <= paddle+PADDLE_HALF+RADIUS ) _rebound(); + if( y > BOTTOM+RADIUS ) { + _lostBall(); + break; + } + // Map the center into the grid of radius-expanded brick boxes. + // Their overlap is smaller than one cell: at most 2 x 2 candidates. + if( x <= LEFT+2 -RADIUS || x >= LEFT+2 +(COLS-1)*BRICK_DX+BRICK_W+RADIUS || + y <= BRICK_TOP-RADIUS || y >= BRICK_TOP+(ROWS-1)*BRICK_DY+BRICK_H+RADIUS ) + continue; + int col = (x-(LEFT+2 -RADIUS))/BRICK_DX; + int row = (y-(BRICK_TOP-RADIUS))/BRICK_DY; + bool hit = false; + for( int r = row > 0 ? row-1 : 0; r <= row && r < ROWS && !hit; r++ ) + for( int c = col > 0 ? col-1 : 0; c <= col && c < COLS; c++ ) { + int i = r*COLS+c; + if( bricks[i] == 0 ) continue; + f64 left = LEFT+2 +c*BRICK_DX-RADIUS; + f64 top = BRICK_TOP+r*BRICK_DY-RADIUS; + f64 right = left+BRICK_W+2*RADIUS; + f64 bottom= top +BRICK_H+2*RADIUS; + if( left < x < right && top < y < bottom ) { + bool back = false; + // Resolve the face crossed this step, using the previous center. + // Vertical faces win a simultaneous corner contact. + if( oldY <= top && dy > 0.0 ) { + y = top; dy = -dy; back = true; + } else if( oldY >= bottom && dy < 0.0 ) { + y = bottom; dy = -dy; + } else if( oldX <= left && dx > 0.0 ) { + x = left; dx = -dx; + } else if( oldX >= right && dx < 0.0 ) { + x = right; dx = -dx; + } else continue; // Missed all bricks + _brick(i,back); + hit = true; + break; + } + } + } + return 0; + }; + + + // Several sound things might happen in this event, pick one + int sound = 0; // Increasing priority: wall, paddle, brick, boost, miss, win. + val _cue = { int snd -> if( snd > sound ) sound = snd; }; + + // Ball speed is a fraction of deepest brick hit, plus backwall boost + f64 speed; + val _speed = { -> speed = (320.0+35.0*(depth-1))*(boosted ? 1.65 : 1.0); }; + + // Clamp paddle to wall sides + f64 paddle; + val _paddle = { f64 x -> + paddle = x < LEFT +PADDLE_HALF ? LEFT +PADDLE_HALF : + x > RIGHT-PADDLE_HALF ? RIGHT-PADDLE_HALF : + x; + }; + + // Hit the paddle + val _rebound = { -> + // Where along the paddle did you hit? + f64 offset = (x-paddle)/PADDLE_HALF; + if( offset < -1.0 ) offset = -1.0; + if( offset > 1.0 ) offset = 1.0; + dx = 0.88*offset; + // A near-center hit still drifts sideways instead of repeating forever. + if( -0.12 < dx < 0.12 ) dx = dx < 0.0 ? -0.12 : 0.12; + dy = -_sqrt(1.0-dx*dx); // Bounce upwards speed + y = PADDLE_Y-RADIUS; // Reset ball hit to top of paddle + backWall = false; // Did not hit back wall + _cue(2); // Paddle bounce sound + }; + + // Hit a brick? + int soundDepth; // Which brick depth for the boop + val _brick = { int index, bool back -> + if( bricks[index] == 0 ) return 0; // No brick to hit! + bricks[index] = 0; // Clear brick + int row = ROWS-index/COLS; // Get brick row + score += row; // Score by row + remaining--; // Fewer bricks to iwn + if( row > depth ) depth = row; // Max depth seen + // Brick-hit sound + if( sound <= 3 ) { _cue(3); soundDepth = row; } + if( back && backWall && !boosted ) { _cue(3); boosted = true; } + if( remaining == 0 ) { _cue(6); phase = WON; } + _speed(); + return 0; + }; + + // Missed ball with paddle + val _lostBall = { -> + lives--; + phase = lives == 0 ? LOST : READY; + backWall = false; + depth = 1; + boosted = false; + _speed(); + x = paddle; y = PADDLE_Y-RADIUS-6; + _cue(5); + }; + + // Reset for another round + val _reset = { -> + for( int i = 0; i < bricks#; i++ ) bricks[i] = 1; + score = 0; + remaining = COLS*ROWS; + lives = 3; + depth = 1; + phase = READY; + boosted = false; + backWall = false; + paused = false; + sound = 0; + soundDepth = 1; + paddle = 504.0; + x = paddle; + y = PADDLE_Y-RADIUS-6; + _speed(); + }; + + // -------------------------------- + // Drawing code + val _hud = { !sys.draw.Canvas c -> + c.text(24,22,"BREAKOUT",5,WHITE); + c.text(264,20,"SCORE",2,MUTED); + _number(c,264,40,score,3,4,WHITE); + c.text(424,20,"BRICKS",2,MUTED); + _number(c,424,40,remaining,3,4,WHITE); + c.text(584,20,"BALLS",2,MUTED); + for( int i = 0; i < lives; i++ ) c.fillCircle(590+23*i,50,5,CYAN); + c.text(714,20,"SPEED",2,MUTED); + int ispeed = speed; + _number(c,714,40,ispeed,3,4,boosted ? COLORS[0] : WHITE); + c.text(838,40,boosted ? "BOOST!" : "NORMAL",3,boosted ? COLORS[0] : MUTED); + }; + + + val _bricks = { !sys.draw.Canvas c -> + c.fillRect(LEFT-4,TOP-4,RIGHT-LEFT+8,4,MUTED); + c.fillRect(LEFT-4,TOP,4,BOTTOM-TOP,0x29435a); + c.fillRect(RIGHT,TOP,4,BOTTOM-TOP,0x29435a); + c.text(376,108,boosted ? "BACK WALL BOOST ACTIVE" : "BREAK THROUGH FOR A BOOST",2,boosted ? COLORS[0] : MUTED); + for( int i = 0; i < bricks#; i++ ) { + if( bricks[i] == 0 ) continue; + int row = i/COLS; + i64 x = LEFT+2+(i-row*COLS)*BRICK_DX, y = BRICK_TOP+row*BRICK_DY; + c.fillRect(x,y,BRICK_W,BRICK_H,COLORS[row]); + c.fillRect(x,y,BRICK_W,2,WHITE); + } + }; + + val _overlay = { !sys.draw.Canvas c -> + if( phase != PLAY || paused ) { + c.fillRect(246,422,516,108,0x17263a).rect(246,422,516,108,2,0x29435a); + u8[~] title = paused ? "PAUSED" : phase == WON ? "WALL CLEARED!" : phase == LOST ? "GAME OVER" : "READY?"; + c.text(504-(title#*4-1)*4/2,442,title,4,WHITE); + u8[~] help = paused ? "P TO CONTINUE" : phase == WON || phase == LOST ? "R TO PLAY AGAIN" : "SPACE OR CLICK TO SERVE"; + c.text(504-(help#*4-1)*2/2,490,help,2,CYAN); + } + return 0; + }; + + // With this Canvas, we draw into the blank canvas, then "present". The + // underlying graphics lib copies the bits, and we are then free to clear + // and draw again while the old bits continue to be presented. + val _draw = { !sys.draw.Canvas c -> + c.clear(BACKGROUND); + _hud(c); + _bricks(c); + i64 paddle = paddle, ballX = x, ballY = y; + int glowRadius = RADIUS+3; + c.fillRect(paddle-PADDLE_HALF,PADDLE_Y,2*PADDLE_HALF,12,CYAN); + c.fillRect(paddle-PADDLE_HALF+4,PADDLE_Y+2,2*PADDLE_HALF-8,2,WHITE); + c.fillCircle(ballX,ballY,glowRadius,boosted ? 0x672a42 : 0x224b61); + c.fillCircle(ballX,ballY,RADIUS,boosted ? COLORS[0] : WHITE); + _overlay(c); + c.text(24,738,"ARROWS / MOUSE: MOVE SPACE / CLICK: SERVE P: PAUSE R: RESTART Q / ESC: QUIT",2,MUTED); + }; + + // Served and still holding serve key + bool serveHeld; + // Paused and still holding pause key + bool pauseHeld; + // Restart and still holding restart key + bool restartHeld; + // Last mouse position; track moving mouse + int lastMouse; + + // Read and act on the controls + val _input = { -> + val gfx = sys.gfx; + bool serve = gfx.key(' ') || gfx.button(gfx.LEFT); + bool pause = gfx.key('P'); + bool restart= gfx.key('R'); + + // Keyboard direction and mouse position + int direction = (gfx.key(KEY_RIGHT) ? 1 : 0)-(gfx.key(KEY_LEFT) ? 1 : 0); + int mouse = gfx.mouseX(); + + // Restart requested + restartHeld = debounce( restart, restartHeld, { -> _reset(); sys.audio.stop(); } ); + + // Playing or ready to play game? + if( phase == READY || phase == PLAY ) { + // Flip pause state + pauseHeld = debounce( pause, pauseHeld, { -> paused = !paused; } ); + + // If no key and mouse moved, set paddle to mouse + if( mouse != lastMouse && direction == 0 && !paused ) { + _paddle(mouse*1.0); // Set paddle to mouse + // If READY and not PLAY, ball tracks paddle directly + if( phase == READY ) x = paddle; + } + lastMouse = mouse; + + } + + // Serve requested + serveHeld = debounce( serve, serveHeld, _serve ); + + // Return key direction, so the tick tracking can slide paddle smoothly + return direction; + }; + + val debounce = { bool b, bool bHeld, { int } action -> + if( b && !bHeld ) // Action requested and repeating + action(); // Do action + b; // Dont action again, until 'b' is released + }; + + + // Serve the ball; give it a nominal direction and speed + val _serve = { -> + if( phase == READY && !paused ) { + phase = PLAY; + dx = 0.4; + dy = -_sqrt(1.0-dx*dx); + } + return 0; + }; +}; + +// Fixed-width decimal labels avoid allocating new strings on every frame. +val _number = { !sys.draw.Canvas c, i64 x, i64 y, int value, int digits, int scale, u32 color -> + for( int i = 0; i < digits; i++ ) { + c.text(x+(digits-1-i)*4*scale,y,DIGITS[value-value/10*10],scale,color); + value /= 10; + } +}; + +// Short, fading triangle waves; the array initializer builds PCM once at startup. +val _tone = { int period -> + val samples = new i16[~2400]{int i -> + int phase = i-i/period*period; + int triangle = phase < period/2 ? phase : period-phase; + return (triangle*4-period)*6000/period*(2400-i)/2400; + }; + int handle = sys.audio.load(samples); + return handle < 0 ? sys.audio.BOOP : handle; +}; + + +// Fast low accuracy sqrt +val _sqrt = { f64 n -> + f64 r = 1.0; + for( int i = 0; i < 8; i++ ) r = (r+n/r)*0.5; + return r; +}; + +val main = { u8[~][] args -> + // Shortcuts + val gfx = sys.gfx, audio = sys.audio; + + // Open a window + if( gfx.open("Simple Breakout",WIDTH,HEIGHT) != gfx.OK ) return 1; + + // Audio setup; pre-specify a series of sounds + audio.open(); // Optional: a missing audio backend leaves play silent. + val sounds = new int[~]{0,_tone(120),_tone(80),audio.BOOP,audio.WIN,audio.LOSE,audio.WIN}; + // Bottom to top: roughly 333..800 Hz, below the old 1000 Hz brick tone. + val brickSounds = new int[~ROWS]{int i -> int period = 144-i*12; _tone(period);}; + + // Setup drawing board + val c = new sys.draw.Canvas(WIDTH,HEIGHT); + + // Setup game contents + val g = new Game; + g._reset(); + + // + int frames = 0, status = gfx.OK; + i64 last_ms = sys.time.monotonic(), pending_ms = 0, frame_ms = last_ms; + + while( status == gfx.OK ) { + if( gfx.key('Q') || gfx.key(gfx.ESCAPE) ) break; + int direction = g._input(); + i64 now_ms = sys.time.monotonic(), elapsed_ms = now_ms-last_ms; + last_ms = now_ms; + // Drop long stalls instead of teleporting the ball after a window drag. + pending_ms += elapsed_ms < 0 ? 0 : elapsed_ms > 64 ? 64 : elapsed_ms; + while( pending_ms >= STEP_ms ) { g._tick(direction); pending_ms -= STEP_ms; } + if( g.sound != 0 ) { + audio.play(g.sound == 3 ? brickSounds[g.soundDepth-1] : sounds[g.sound]); + g.sound = 0; + } + if( now_ms >= frame_ms ) { + g._draw(c); + status = gfx.present(c.pixels); + frame_ms = now_ms+FRAME_ms; + if( args# > 1 && ++frames == 10 ) break; + } + if( status == gfx.OK ) status = gfx.wait(STEP_ms-pending_ms); + } + audio.close(); gfx.close(); + return status == gfx.ERROR ? 1 : 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..ecf0fae1 --- /dev/null +++ b/docs/chapter31/examples/Clock.smp @@ -0,0 +1,128 @@ +// 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 gfx = sys.gfx; + val time = sys.time; + // The main Canvas + val screen = new sys.draw.Canvas(720,540); + if( gfx.open("Simple digital and analog clocks",screen.width,screen.height) != gfx.OK ) return 1; + + // Button to flip clock style + val button = new sys.draw.Button(220,454,280,52,"ANALOG CLOCK"); + val timers = new sys.time.Timers(); + u32 frame_id = timers.every(33); + if( frame_id == 0 ) { gfx.close(); return 1; } + + bool analog = false, held = false; + int frames = 0, status = gfx.OK; + + // Main event loop + while( status == gfx.OK ) { + // Did the user press space bar? + bool space = gfx.key(' '); + // Did the left mouse button go down? + bool down = gfx.button(gfx.LEFT); + // ANd did it go down over this button? + bool click = button.update(gfx.mouseX(),gfx.mouseY(),down); + // If clicked, flip the clock mode + if( click || (space && !held) ) analog = !analog; + held = space; + if( gfx.key('Q') ) break; + // Change button label + button.label = analog ? "DIGITAL CLOCK" : "ANALOG CLOCK"; + // If the next timer event is the every-33-msec event + if( timers.poll(time.monotonic()) == frame_id ) { + // Draw the clock screen and button + _draw(screen,time.now(),analog); + button.draw(screen,3,GREEN,BACKGROUND,DIM); + // Presne the canvas on screen + status = gfx.present(screen.pixels); + // Testing only: show 5 frames, flip to analog, show 5 more and exit + if( args# > 1 ) { + frames++; + if( frames == 5 ) analog = true; + if( frames == 10 ) break; + } + } + if( status != gfx.OK ) break; + // Stall until next timer event + status = gfx.wait(time.remaining(timers.nextDeadline(),time.monotonic())); + } + gfx.close(); + return status == gfx.ERROR ? 1 : 0; +}; + +val _draw = { !sys.draw.Canvas screen, i64 wall_ms, i64 analog -> + val d = sys.time.local(wall_ms); + screen.clear(BACKGROUND); + screen.text(266,24,"SIMPLE CLOCK",4,GREEN); + if( analog ) _analog(screen,d.hour,d.minute,d.second,d.millisecond); + else _digital(screen,d.hour,d.minute,d.second); + screen.text(298,412,"%(sys.time.formatLocal(wall_ms,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 = { !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 = { !sys.draw.Canvas c, int hour, int minute, int second -> + _digit(c,68,166,hour/10); _digit(c,154,166,hour-hour/10*10); + _digit(c,284,166,minute/10); _digit(c,370,166,minute-minute/10*10); + _digit(c,500,166,second/10); _digit(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 = { 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 = { !sys.draw.Canvas c, f64 turn, int length, int thickness, u32 color -> + int x = 360+_sin(turn)*length, y = 232-_sin(turn+0.25)*length; + c.line(360,232,x,y,thickness,color); +}; +val _analog = { !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(turn)*inner, y0 = 232-_sin(turn+0.25)*inner; + int x1 = 360+_sin(turn)*149, y1 = 232-_sin(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(c,(hour-hour/12*12+minutes/60.0)/12.0,78,8,GREEN); + _hand(c,minutes/60.0,112,5,GREEN); + _hand(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..6591b310 --- /dev/null +++ b/docs/chapter31/examples/Dijkstra.smp @@ -0,0 +1,71 @@ + +// An implementation of +// https://sampleprograms.io/projects/dijkstra/ + +val shortestPath = { !sys.aryi64 matrix, int nodes, int src, int dst -> + i64 infinity = sys.integer.MAX_I64; + var visited = new sys.adt.bitset(); + // All nodes are infinitely far from each other, except the starting point + var dist = new i64[nodes]{ int i -> i == src ? 0 : infinity; }; + + 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..8b251fa2 --- /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..20b644cf --- /dev/null +++ b/docs/chapter31/examples/README.md @@ -0,0 +1,108 @@ +# 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. + +The [Breakout game](Breakout.smp) has 24 columns and 8 rows, with colors and +point values by depth: 1 at the bottom through 8 at the top, 864 points total. +Clear the wall before losing all three balls. Mouse movement or the arrow keys +move the paddle; Space/left click serves, P pauses, R restarts, Q/Escape exits. +Run `make run-breakout CTAGS=` or build only with `make breakout CTAGS=`. +The familiar paddle, serve, and limited-ball rules follow +[Atari's Breakout instructions](https://atari.com/pages/breakout). + +The deepest row reached with the current ball sets the base speed: 320 pixels/second plus 35 for +each additional depth. After touching the back wall, hitting the top of a brick +activates a **one-time 1.65x speed boost**; side/front hits do not. Returning to +the paddle disarms that trip's back-wall opportunity. Losing a ball resets depth +and boost, so each new ball starts at 320 pixels/second and can earn its own +back-wall boost. Edge hits on the paddle give a wider +rebound than center hits. Clear all bricks to win; no new wall replaces them. + +Physics uses fixed 8 ms ticks with movement split into steps of at most two +pixels, including at boosted speed. Rendering runs roughly every 16 ms. The +brick and sound tables demonstrate array initializers; the short bounce tones +use read-only PCM arrays made by index functions. Brick boops rise in pitch with +row depth, from about 333 Hz at the bottom to 800 Hz at the top. Audio is optional. Native +tests in `BreakoutTest` exercise collisions, scoring, lives, pause, paddle angles, +the boost, and clearing the whole wall without a display or audio device. + +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..ee4e3182 --- /dev/null +++ b/docs/chapter31/examples/TicTacToe.smp @@ -0,0 +1,132 @@ +// 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]{ int i -> ' '; }; // Initialize the board + // 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..d3d575a7 --- /dev/null +++ b/docs/chapter31/examples/TicTacToeGraphical.smp @@ -0,0 +1,168 @@ +// 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/intrinsics.md b/docs/chapter31/intrinsics.md new file mode 100644 index 00000000..3aae39bd --- /dev/null +++ b/docs/chapter31/intrinsics.md @@ -0,0 +1,53 @@ +# Fixed-semantics C intrinsics + +An intrinsic is an ordinary C binding with a reserved external symbol and a +fixed signature and meaning. The compiler may replace a known call with ideal +IR nodes; it does not match the spelling of the caller's local variable or a +Simple function with the same name. No new expression syntax is involved. + +The first intrinsics preserve IEEE binary64 representations: + +| External C symbol | Signature | Public alias | +| --- | --- | --- | +| `simple_f64_bits` | `{ f64 -> i64 }` | `sys.math.f64Bits` | +| `simple_f64_from_bits` | `{ i64 -> f64 }` | `sys.math.f64FromBits` | + +```simple +i64 bits = sys.math.f64Bits(1.0); // 0x3ff0000000000000 +f64 value = sys.math.f64FromBits(bits); // 1.0 +int numeric = value; // Numeric conversion, still 1. +``` + +Both operations preserve signed zero, infinities, subnormals, and NaN signs and +payloads, including the signaling bit. They perform no arithmetic and have no +memory effects. An `i64` holds the bit pattern; its sign is irrelevant to the +conversion. Applying either operation followed by the other is an identity. +Floating-point arithmetic between conversions retains its usual semantics and +may change a NaN representation. + +`Intrinsic.java` owns the reserved-symbol table, signatures, effect queries, +inlining, and ideal nodes. Call/CallEnd delegate to it. The cheap inliner checks +intrinsics before requiring a Simple body or Return edge. It validates the +signature and actual arguments, then replaces the complete control/memory/value +state. For these pure operations, control and memory pass through unchanged. +Aliases and imported bindings use the existing external-function identity map. + +Unknown indirect targets keep ordinary calls. `simple_crt.c` supplies real C +definitions, using `memcpy` to preserve representations without aliasing tricks. +These symbols have fixed semantics: a replacement implementation must honor the +same contract. Ordinary C functions remain opaque to the optimizer. + +The ideal nodes support constant folding, inverse cancellation, evaluation, and +serialization. Floating-point constant interning and serialization retain raw +NaN bits. x86 emits `MOVQ` between XMM and integer registers; ARM emits `FMOV`, +and RISC-V emits `FMV.X.D` / `FMV.D.X`. Register allocation may add copies or +spills, and normal module-initialization checks still apply. + +Future intrinsics such as compare-and-swap or fences must define their memory +and escape effects explicitly and expand to nodes with appropriate ordering +dependencies. They must not inherit the pure conversions' passthrough memory +behavior. Atomic operations and hardware square root are not introduced here. + +`make test CLASS=IntrinsicTest CTAGS=` checks folding, representation preservation, +type errors, side effects, indirect C fallback, binary imports, and instruction +selection on all three backends, plus ARM/RISC-V emulation and native x86 execution. diff --git a/docs/chapter31/time.md b/docs/chapter31/time.md new file mode 100644 index 00000000..10733006 --- /dev/null +++ b/docs/chapter31/time.md @@ -0,0 +1,129 @@ +# 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(duration_ms)` | Sleep for a nonnegative duration. | +| `time.sleepUntil(deadline_ms)` | Wait for a monotonic millisecond deadline; returns immediately if past. | +| `time.remaining(deadline_ms, current_ms)` | 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, +defined as `1000`, `60*SECOND`, `60*MINUTE`, and `24*HOUR` respectively. +`NANOS_PER_MS` is `1000*1000`. Overflow checks use the shared +`sys.integer.MIN_I64` and `MAX_I64` bounds; `MAX_I32` and `MAX_U32` in that +module also describe the library's array and timer-ID limits. + +## 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: + +Timer IDs are `u32`, with `0` reserved for registration failure or no timer due. +Deadlines, delays, and periods remain `i64` milliseconds; their parameter, +field, and local names end in `_ms` to distinguish them from IDs. + +| Operation | Meaning | +| --- | --- | +| `after(delay_ms)` | Register a one-shot timer; return its positive `u32` ID, or `0`. | +| `every(period_ms)` | Register a repeating timer; period must be positive. Returns a `u32` ID, or `0`. | +| `afterAt(current_ms, delay_ms)`, `everyAt(current_ms, period_ms)` | 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_ms)` | Return the next due `u32` ID, or `0` 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. +IDs are never reused within a collection; registration fails once all nonzero +`u32` IDs have been issued. +There are no callbacks or timer threads: the caller handles each returned ID. + +`gfx.wait(timeout_ms)` 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..2b557eea --- /dev/null +++ b/graphics.mk @@ -0,0 +1,78 @@ +# 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,) +BREAKOUT := $(GAME_DIR)/Breakout$(if $(filter Windows_NT,$(OS)),.exe,) + +.PHONY: breakout run-breakout +breakout: $(BREAKOUT) +$(BREAKOUT): docs/examples/Breakout.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)/Breakout.o $< +run-breakout: graphics audio breakout + SIMPLE_MINIFB="$(abspath $(GRAPHICS_LIB))" SIMPLE_MINIAUDIO="$(abspath $(AUDIO_LIB))" $(BREAKOUT) + +.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_crt.c b/src/main/c/simple_crt.c index 438ae5f9..39708629 100644 --- a/src/main/c/simple_crt.c +++ b/src/main/c/simple_crt.c @@ -6,6 +6,19 @@ #include <termios.h> #include <unistd.h> +// Fixed-semantics compiler intrinsics. Keep these definitions for indirect +// calls; direct calls may inline to representation-preserving register moves. +int64_t simple_f64_bits(double value) { + int64_t bits; + memcpy(&bits, &value, sizeof(bits)); + return bits; +} +double simple_f64_from_bits(int64_t bits) { + double value; + memcpy(&value, &bits, sizeof(value)); + return value; +} + // errno may be thread-local or a macro calling a platform accessor. int32_t simple_errno(void) { return errno; } int32_t simple_set_errno(int32_t error) { errno = error; return -1; } 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/CodeGen.java b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java index b4c93e1e..8618b47b 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java +++ b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -786,7 +786,11 @@ public class CodeGen { public boolean _asmLittle=true; public String asm() { return asm(new SB()).toString(); } SB asm(SB sb) { - ASMPrinter.print(sb,this); + if( _encodings.isEmpty() ) ASMPrinter.print(sb,this); + else for( var entry : _encodings.entrySet() ) { + sb.p("\n=== Compilation unit: ").p(entry.getKey()._cname).p(" ===\n"); + ASMPrinter.print(sb,this,entry.getValue()); + } return sb; } diff --git a/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java b/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java index fcf5c287..c5b25be0 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java +++ b/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java @@ -318,11 +318,15 @@ public class ElfWriter { _strtab = new StringSection(); // place all the symbols + Encoding enc = _code._encoding; int nlocals = 1/*null*/+9/*number of sections*/+1/*Simple section*/; + int debugLocals=0; + for( StaticData.ObjectData obj : enc._data.objects.values() ) + if( !obj.external ) debugLocals+=obj.debugNames.size(); + nlocals+=debugLocals; SymbolSection symbols = new SymbolSection(nlocals); // we've already constructed these entire sections in the encoding phase - Encoding enc = _code._encoding; // Append ELF-only thunks to a copy. Export must not change instruction // offsets or the image used by the in-memory linker and emulators. BAOS textBits = new BAOS(enc._bits.toByteArray(),enc._bits.size()); @@ -402,10 +406,17 @@ public class ElfWriter { SimpleSection simple = new SimpleSection(_code._serial); // Write the local section header symbols - assert nlocals == 1+_sections._len; + assert nlocals == 1+_sections._len+debugLocals; symbols.symbol("",0,0,0); // Null symbol at index 0 for( Section s : _sections ) symbols.symbol(s._name, s._index, SYM_BIND_LOCAL, SYM_TYPE_SECTION, 0, s.size()); + // Local aliases describe the pooled object without changing linkage. + // Several source constants may name the same bytes. + for( StaticData.ObjectData obj : enc._data.objects.values() ) + if( !obj.external ) + for( String name : obj.debugNames ) + symbols.symbol(name,obj.readOnly ? rodata._index : rwdata._index, + SYM_BIND_LOCAL,SYM_TYPE_OBJECT,obj.offset,obj.size); int size = 64; // ELF header size // size of all section data 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/IterPeeps.java b/src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java index 985cd4d2..f4acb729 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java +++ b/src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java @@ -65,8 +65,10 @@ public class IterPeeps { * Iterate peepholes and inlining to a fixed point */ public void iterate( CodeGen code ) { + assert !CodeGen.expensiveAssert(0) || (progressOnList(code, _work) && schedulableUses(code)); boolean didInline = false; Ary<Node> defer = new Ary<>(Node.class); + while( true ) { // Clean up everything that does not grow the code iteratePeeps(code); @@ -97,8 +99,6 @@ public class IterPeeps { // Run all the code-reduction and type-lifting peeps as possible private void iteratePeeps( CodeGen code ) { - assert !CodeGen.expensiveAssert(0) || (progressOnList(code, _work) && schedulableUses(code)); - Node n; while( (n = _work.pop()) != null ) { if( n.isDead() ) continue; diff --git a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java index 3306ff8f..882a0f8a 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java +++ b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java @@ -150,7 +150,8 @@ public abstract class ParseAll { } private static void addRequiredTypes(CodeGen code, CompUnit cunit) { - for( String typeName : code.P._requiredTypes ) { + for( var required : code.P._requiredTypes.entrySet() ) { + String typeName = required.getKey(); // A forward use can have been resolved later in this source file. if( Parser.TYPES.get(typeName) instanceof TypeStruct ts && !ts._fref ) continue; try { @@ -158,7 +159,7 @@ public abstract class ParseAll { if( xcunit != null ) cunit.addDep(xcunit); else - assert false : "expected to find required type"; + throw Parser.error("Unknown type '"+typeName+"'",required.getValue()); } catch( IOException ioe ) { throw new RuntimeException(ioe); } diff --git a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java index 3991cd48..1b335c50 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java +++ b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java @@ -215,7 +215,7 @@ public class RegAlloc { _deferredLoopSelf.clear(); for( byte round=0; !graphColor(round); round++ ) { split(round); - if( round >= 7 ) { + if( round >= 10 ) { FunNode fun = _cfg.at(0).fun(); throw new IllegalStateException("Register allocation failed after eight rounds in "+ (fun==null ? "<root>" : fun.label())); diff --git a/src/main/java/com/seaofnodes/simple/codegen/Serialize.java b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java index ae4ecb74..b168c4af 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,18 @@ 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.isConAry() ) { + TypeConAry ary=(TypeConAry)ts.field("[]")._t; + ts=TypeStruct.makeAry(ts._name,TypeInteger.U32,ts.field("#")._alias, + ary.storageType(),ts.field("[]")._alias,false); + } else 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/codegen/StaticData.java b/src/main/java/com/seaofnodes/simple/codegen/StaticData.java index 5d1a519b..3d3a40dd 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/StaticData.java +++ b/src/main/java/com/seaofnodes/simple/codegen/StaticData.java @@ -14,7 +14,10 @@ public final class StaticData { public final String symbol; public final boolean readOnly, external; public final int alignment, size; + // Source names are aliases, not part of the object's pooling identity. + final TreeSet<String> debugNames = new TreeSet<>(); public int offset; + public String label() { return debugNames.isEmpty() ? symbol : String.join(", ",debugNames); } ObjectData(Type t, String name, boolean ro, boolean ext) { type=t; symbol=name; readOnly=ro; external=ext; alignment=t.alignment(); @@ -73,7 +76,12 @@ public final class StaticData { // Register before following fields: class objects may refer to each other. if( !obj.external && t instanceof TypeStruct ts ) for( Field f : ts._fields ) - if( !f._extern && f._t.isConstant() ) target(f._t); + if( !f._extern && f._t.isConstant() ) { + target(f._t); + if( clz && f._final && f._t instanceof TypeMemPtr ptr && + pointerConstant(ptr) && !isClass(ptr._obj) ) + object(ptr._obj).debugNames.add(ts._name.substring(TypeStruct.CLZ.length())+"."+f._fname); + } return obj; } diff --git a/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java b/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java index a86cd18a..418edaf6 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java +++ b/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java @@ -21,12 +21,14 @@ abstract class TypeCheck { Ary<Node> errs = new Ary<>(Node.class); Ary<ConstructNode> constructs = new Ary<>(ConstructNode.class); Ary<InitViewNode> views = new Ary<>(InitViewNode.class); + Ary<ArrayInitNode> arrays = new Ary<>(ArrayInitNode.class); code._stop.walk( n -> { if( n.err() != null ) errs.add(n); if( n instanceof CLInitNode ) code.add(n); if( n instanceof ConstructNode con ) constructs.add(con); if( n instanceof InitViewNode view ) views.add(view); + if( n instanceof ArrayInitNode array ) arrays.add(array); return null; }); @@ -57,13 +59,15 @@ abstract class TypeCheck { if( !con.isDead() ) con.subsume(con.in(1)); for( InitViewNode view : views ) if( !view.isDead() ) view.subsume(view.in(1)); + for( ArrayInitNode array : arrays ) + if( !array.isDead() ) array.subsume(array.in(2)); } private static int errorRank(Node n) { // Bad memory access originates the error. Missing construction fields // in turn explain generic-scalar arithmetic further downstream. if( n instanceof MemOpNode && noBottomInputs(n) ) return 0; - if( n instanceof ConstructNode ) return 1; + if( n instanceof ConstructNode || n instanceof ArrayInitNode ) return 1; return noBottomInputs(n) ? 2 : 3; } diff --git a/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java b/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java new file mode 100644 index 00000000..c6ae90ea --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/ArrayInitNode.java @@ -0,0 +1,70 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; +import com.seaofnodes.simple.parser.Lexer; +import com.seaofnodes.simple.parser.Parser; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; + +/** Source checks for array construction and its yielded elements. Erased by + * TypeCheck before serialization and scheduling. */ +public final class ArrayInitNode extends Node { + private final int _count; // List size, -1 for a generator, -2 for a yielded element. + private final Lexer _loc; + public ArrayInitNode(Node ctrl, Node mem, Node ptr, Node len, int count, Lexer loc, Ary<Node> values) { + super(ctrl,mem,ptr,len); + _count=count; _loc=loc; + for( Node value : values ) addDef(value); + } + public ArrayInitNode(Node ctrl, Node value, Lexer loc) { + super(ctrl,null,value); + _count=-2; _loc=loc; + } + @Override public String label() { return "ArrayInit"; } + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("arrayInit(").n(in(2)).p(")"); } + @Override public Type compute() { + if( in(0)._type != Type.CONTROL ) return Type.TOP; + Type ptr = in(2)._type; + if( _count < 0 ) return ptr; + // A list is strict in all its inputs. Wait for every input before + // choosing a constant payload or falling to the ordinary array type. + // Otherwise one SCCP worklist order can return PtrTop (or an array) + // before another unresolved input sends the result back up to Top. + if( ptr.isHigh() || in(3)._type.isHigh() ) return Type.TOP; + for( int i = 4; i < nIns(); i++ ) + if( in(i)._type.isHigh() ) return Type.TOP; + if( !(ptr instanceof TypeMemPtr tmp) ) return ptr; + Field body = tmp._obj.field("[]"); + if( body == null || !body._final || !(body._t instanceof TypeInteger storage) ) return ptr; + if( !(in(3)._type instanceof TypeInteger length) || !length.isConstant() || length.value() != _count ) return ptr; + long[] values = new long[_count]; + for( int i = 0; i < _count; i++ ) { + Type value = in(i+4)._type; + if( !(value instanceof TypeInteger integer) || !integer.isConstant() ) return ptr; + values[i] = integer.value(); + } + TypeConAry contents = TypeConAry.make(storage,values); + TypeStruct array = TypeStruct.makeAry(tmp._obj._name,tmp._obj.field("#")._alias,body._alias,contents); + // The surrounding ReadOnlyNode supplies the public view. Keep this + // node's access flags consistent when its contents cease to be constant. + return TypeMemPtr.make((byte)2,array,true).withAccess(tmp._ro); + } + @Override public Node idealize() { return null; } + @Override public Parser.ParseException err() { + if( in(0)._type==Type.XCONTROL ) return null; + if( _count==-2 ) + return TypeMemPtr.mayBePartial(in(2)._type) + ? Parser.error("Array initializer requires fully initialized elements",_loc) : null; + if( !(in(3)._type instanceof TypeInteger len) ) + return Parser.error("Array length must be an integer",_loc); + if( len.isConstant() && (len.value()<0 || len.value()>TypeInteger.U32._max) ) + return Parser.error("Array length is outside the u32 range",_loc); + if( _count>=0 && (!len.isConstant() || len.value()!=_count) ) + return Parser.error("Array initializer has "+_count+" elements; length must equal that constant",_loc); + for( int i=4; i<nIns(); i++ ) + if( TypeMemPtr.mayBePartial(in(i)._type) ) + return Parser.error("Array initializer requires fully initialized elements",_loc); + return null; + } + @Override public boolean eq(Node n) { return this==n; } +} diff --git a/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java index 130b631c..b49163e7 100644 --- a/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java +++ b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java @@ -261,15 +261,35 @@ public class BulkMemPhiNode extends PhiNode { mphi = new MemPhiNode(ScopeNode.memName(alias),alias); mphi.addDef(region()); // Due to cycles, must set before calling peephole - // Splitting preserves the bulk Phi's established escape bound. Starting - // at BOT would temporarily invent escapes when the new slice is merged. + // An escaped slice must preserve the bulk Phi's established bound. + // An unescaped slice was excluded from that summary: include the + // facts hidden in predecessor slices before wiring any cyclic Phis. TypeMem old = (TypeMem)_type; + if( !XInt.bit(old._escAs,alias) ) + old = (TypeMem)old.meet(sliceBound(this,alias,new BitSet())); mphi.setType(TypeMem.make(alias,old._t,old._one,old._clz,false,old._escFs,old._escAs)); for( int i=1; i<nIns(); i++ ) mphi.addDef(CodeGen.CODE.add(preciseInput(in(i),alias))); return CodeGen.CODE.add(mphi); } + // A bulk summary ignores unescaped slots in MemMerge inputs. Follow the + // same slots that splitting will expose, including across loop backedges. + // A visited cycle contributes TOP; its incoming definitions supply the bound. + private static TypeMem sliceBound(Node mem, int alias, BitSet visit) { + if( visit.get(mem._nid) ) return TypeMem.TOP; + visit.set(mem._nid); + if( mem instanceof MemMergeNode merge ) + return sliceBound(merge.alias(alias),alias,visit); + if( mem instanceof BulkMemPhiNode bulk ) { + TypeMem bound = TypeMem.TOP; + for( int i = 1; i < bulk.nIns(); i++ ) + bound = (TypeMem)bound.meet(sliceBound(bulk.in(i),alias,visit)); + return bound; + } + return mem._type instanceof TypeMem bound ? bound : TypeMem.BOT; + } + MemPhiNode _findPhi(int alias) { for( Node use : region().outs() ) if( use instanceof MemPhiNode mphi && mphi._alias==alias ) diff --git a/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index b5156c2d..108dd024 100644 --- a/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -85,6 +85,8 @@ public class CallEndNode extends CFGNode implements MultiNode { return ftype.isHigh() ? TypeTuple.STATE.dual() : TypeTuple.STATE; // Mid-fold, just take the inlined callee's return. if( _folding ) return in(1)._type; + TypeTuple intrinsic=Intrinsic.compute(call,this); + if( intrinsic!=null ) return intrinsic; // The outside continuation reads Start directly: Call can remain // CONTROL while its possible returned identities change. @@ -135,6 +137,8 @@ public class CallEndNode extends CFGNode implements MultiNode { } public byte maybeInline() { + byte intrinsic=Intrinsic.tryInline(this); + if( intrinsic>0 ) return intrinsic; byte inline = inlineCandidate(false); if( CodeGen.CODE._midAssert ) return inline; diff --git a/src/main/java/com/seaofnodes/simple/node/CallNode.java b/src/main/java/com/seaofnodes/simple/node/CallNode.java index ac5e4eee..9e9c5224 100644 --- a/src/main/java/com/seaofnodes/simple/node/CallNode.java +++ b/src/main/java/com/seaofnodes/simple/node/CallNode.java @@ -97,6 +97,7 @@ public class CallNode extends CFGNode { if( _type!=Type.CONTROL ) return TypeMem.BOT; if( !(fptr()._type instanceof TypeFunPtr) ) return fptr()._type.isHigh() ? TypeMem.START : TypeMem.BOT; + if( Intrinsic.pure(this) ) return TypeMem.START; if( !external() ) return TypeMem.START; Type input = dep.addDepForwards(mem())._type; TypeMem escaped = input instanceof TypeMem m ? m : input.isHigh() ? TypeMem.TOP : TypeMem.BOT; @@ -241,6 +242,8 @@ public class CallNode extends CFGNode { return Parser.error("Expected a function, but found " + fptr()._type, _loc); if( !tfp.notNull() ) return Parser.error( "Might be null calling "+tfp, _loc); + Parser.ParseException intrinsic=Intrinsic.err(this); + if( intrinsic!=null ) return intrinsic; if( nargs() != tfp.nargs() ) return Parser.error( "Expecting "+(tfp.nargs()-1)+" arguments, but found "+(nargs()-1), _loc); @@ -253,6 +256,15 @@ public class CallNode extends CFGNode { // Constructors return private memory and carry hidden self and // selfMem arguments ahead of the source-level arguments. int argno = i - (tfp.ret() instanceof TypeMem ? 2 : 0); + // Access qualifiers are not shown by the structural type dump. + // Report the differing permission instead of two huge shapes. + if( arg(i+2)._type instanceof TypeMemPtr actual && actual._ro && + tfp.arg(i) instanceof TypeMemPtr formal && !formal._ro ) { + String who = i == 0 ? name() == null ? "Receiver" : "Receiver for method '"+name()+"'" + : "Argument #"+argno; + return Parser.error(who+" is read-only "+actual._obj.str()+ + "; expected writable "+formal._obj.str(),_loc); + } return Parser.error( "Argument #" + argno + " isa " + arg(i+2)._type + ", but must be a " + tfp.arg(i), _loc ); } diff --git a/src/main/java/com/seaofnodes/simple/node/EscapeNode.java b/src/main/java/com/seaofnodes/simple/node/EscapeNode.java index 57340609..a8a6151c 100644 --- a/src/main/java/com/seaofnodes/simple/node/EscapeNode.java +++ b/src/main/java/com/seaofnodes/simple/node/EscapeNode.java @@ -64,6 +64,10 @@ public class EscapeNode extends TypeNode { @Override public Type compute() { + // A static constant array has no private heap writes to publish. + // This depends on its type, regardless of which node produced it. + if( self()!=null && self()._type instanceof TypeMemPtr ptr && + ptr.isConstant() && ptr._obj.isConAry() ) return pub()._type; if( priv()._type.isHigh() ) return TypeMem.TOP; if( pub ()._type.isHigh() ) return TypeMem.TOP; TypeMem mpriv = (TypeMem)priv()._type; @@ -100,6 +104,8 @@ public class EscapeNode extends TypeNode { } @Override public Node idealize() { + if( self()._type instanceof TypeMemPtr ptr && ptr.isConstant() && ptr._obj.isConAry() ) + return pub(); // Sharpen private memory input if( priv() instanceof MemMergeNode mmm ) { setDef(2,mmm.alias(fld()._alias)); diff --git a/src/main/java/com/seaofnodes/simple/node/Intrinsic.java b/src/main/java/com/seaofnodes/simple/node/Intrinsic.java new file mode 100644 index 00000000..82626507 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/Intrinsic.java @@ -0,0 +1,119 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.parser.Parser; +import com.seaofnodes.simple.type.*; + +// Fixed-semantics C functions. Recognition, effects and expansion live here; +// Call/CallEnd only delegate. These symbols also have ordinary C definitions +// for calls whose target is not known at compile time. +public final class Intrinsic { + private Intrinsic() {} + + private enum Op { + F64_BITS("simple_f64_bits",TypeFloat.F64,TypeInteger.BOT,true), + F64_FROM_BITS("simple_f64_from_bits",TypeInteger.BOT,TypeFloat.F64,true); + + final String symbol; + final Type arg, ret; + final boolean pure; + Op(String symbol, Type arg, Type ret, boolean pure) { + this.symbol=symbol; this.arg=arg; this.ret=ret; this.pure=pure; + } + boolean signature(TypeFunPtr fp) { + // Slot zero is the implicit receiver, omitted by the C ABI. + return fp.nargs()==2 && fp.arg(1)==arg && fp.ret()==ret; + } + Expansion expand(CallNode call) { + Node value=switch(this) { + case F64_BITS -> new F64BitsNode(call.arg(3)).peephole(); + case F64_FROM_BITS -> new F64FromBitsNode(call.arg(3)).peephole(); + }; + return new Expansion(call.ctrl(),call.mem(),value); + } + } + + private static Op lookup(CallNode call) { + if( !(call.fptr()._type instanceof TypeFunPtr fp) || !fp.notNull() || !fp.isConstant() ) return null; + String symbol=CodeGen.CODE.externFunc(fp.fidx()); + if( symbol==null ) return null; // A Simple function with this name is ordinary code. + for( Op op : Op.values() ) if( op.symbol.equals(symbol) ) return op; + return null; + } + + public static Parser.ParseException err(CallNode call) { + Op op=lookup(call); + return op==null || op.signature(call.tfp()) ? null : Parser.error( + "Intrinsic '"+op.symbol+"' requires { "+op.arg.str()+" -> "+op.ret.str()+" }",call._loc); + } + + // Both initial operations are pure and expose no references. Future atomic + // operations need their own memory/escape semantics, not this passthrough. + public static boolean pure(CallNode call) { + Op op=lookup(call); + return op!=null && op.pure && op.signature(call.tfp()); + } + + public static TypeTuple compute(CallNode call, Node dep) { + if( !pure(call) ) return null; + return TypeTuple.make(Type.CONTROL,dep.addDep(call.mem())._type,call.tfp().ret()); + } + + // An expansion replaces the complete call state, even for a pure value op. + private record Expansion(Node control, Node memory, Node value) { + void replace(CallEndNode end) { + Node[] state={control,memory,value}; + for( Node n : state ) n.keep(); + end.keep(); + for( Node use : end._outputs.asAry() ) { + if( use==null ) continue; // Our temporary keep. + assert use instanceof Proj; + use.subsume(state[((Proj)use).idx()]); + } + end.unkill(); + for( Node n : state ) n.unkill(); + CodeGen.CODE.invalidateIDepthCaches(); + } + } + + // Same cheap-inlining worklist as ordinary functions, but no body/Return, + // cloning, size heuristic, or import loading is needed. + public static byte tryInline(CallEndNode end) { + if( end.folding() || !(end.in(0) instanceof CallNode call) ) return 0; + Op op=lookup(call); + if( op==null || !op.signature(call.tfp()) || call.nargs()!=call.tfp().nargs() || + !end.goodArgs(call,call.tfp()) ) return 0; + if( !CodeGen.CODE._midAssert ) op.expand(call).replace(end); + return 1; + } + + public abstract static class BitCastNode extends Node { + BitCastNode(Node value) { super(null,value); } + public abstract boolean toBits(); + @Override public Type compute() { + Type input=in(1)._type; + Type result=toBits() ? TypeInteger.BOT : TypeFloat.F64; + if( input.isHigh() ) return result.dual(); + if( toBits() && input instanceof TypeFloat f && f.isConstant() ) + return TypeInteger.constant(Double.doubleToRawLongBits(f.value())); + if( !toBits() && input instanceof TypeInteger i && i.isConstant() ) + return TypeFloat.constant(Double.longBitsToDouble(i.value())); + return result; + } + @Override public Node idealize() { + return in(1) instanceof BitCastNode cast && cast.toBits()!=toBits() ? cast.in(1) : null; + } + } + public static final class F64BitsNode extends BitCastNode { + public F64BitsNode(Node value) { super(value); } + @Override public boolean toBits() { return true; } + @Override public Tag serialTag() { return Tag.F64Bits; } + @Override protected String format() { return "f64Bits(%1)"; } + } + public static final class F64FromBitsNode extends BitCastNode { + public F64FromBitsNode(Node value) { super(value); } + @Override public boolean toBits() { return false; } + @Override public Tag serialTag() { return Tag.F64FromBits; } + @Override protected String format() { return "f64FromBits(%1)"; } + } +} diff --git a/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/src/main/java/com/seaofnodes/simple/node/LoadNode.java index eb78ed1f..14efa315 100644 --- a/src/main/java/com/seaofnodes/simple/node/LoadNode.java +++ b/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -98,7 +98,15 @@ public class LoadNode extends MemOpNode { t = mem._alias==_alias ? mem._t : actual==null ? TypeScalar.BOT : actual._t; } else if( mem._alias==_alias ) t = t.join(mem._t); - if( !mem._one ) t = mem.outside(t); + // A final method's identity comes from its declaration, not from an + // unknown outside writer. It remains callable on a local object even + // when that function has never escaped to outside code. + if( !mem._one ) { + Type declared = Parser.TYPES.get(tmp._obj._name); + Field field = declared instanceof TypeStruct obj ? obj.field(_name) : null; + if( !(field != null && field._final && field._t instanceof TypeFunPtr fun && fun.isConstant()) ) + t = mem.outside(t); + } // Store forwarding is also a value fact during SCCP. Waiting for the // later rewrite can turn a known non-null stored pointer into a // nullable declaration, then BOTTOM at the next dereference. @@ -364,15 +372,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/MemOpNode.java b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java index 35183380..bce11b50 100644 --- a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java +++ b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java @@ -94,7 +94,8 @@ public abstract class MemOpNode extends TypeNode { if( !(ptr()._type instanceof TypeMemPtr tmp) ) return Type.BOTTOM; Field fld = tmp._obj.field(_name); - return fld == null || fld._t == null ? Type.BOTTOM : fld._t; + return fld == null || fld._t == null ? Type.BOTTOM : + fld._t instanceof TypeConAry ary ? ary.storageType() : fld._t; } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return _printMach(p); } diff --git a/src/main/java/com/seaofnodes/simple/node/Node.java b/src/main/java/com/seaofnodes/simple/node/Node.java index 2815dfba..d4c0794c 100644 --- a/src/main/java/com/seaofnodes/simple/node/Node.java +++ b/src/main/java/com/seaofnodes/simple/node/Node.java @@ -77,7 +77,7 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { New,Never,Not,Or,Parm,Phi,Proj,PtrToInt, ReadOnly,Return,Region,RoundF32, Sar,Shl,Shr,Start,StartCU,StopCU,Stop,Store,Sub, - ToInteger,ToFloat,XCtrl,Xor; + ToInteger,ToFloat,XCtrl,Xor,F64Bits,F64FromBits; public static final Tag[] VALS = values(); public Node make( BAOS bais, String[] strs, Type[] types, GlobalBits fileAliases, GlobalBits aliases ) { return switch(this) { @@ -103,6 +103,8 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { case Sub -> new SubNode(null,null,(byte)bais.packed1()); case ToFloat-> new ToFloatNode(null); case ToInteger-> new ToIntegerNode(null); + case F64Bits-> new Intrinsic.F64BitsNode(null); + case F64FromBits-> new Intrinsic.F64FromBitsNode(null); case XCtrl -> new XCtrlNode(); case Xor -> { int mode=bais.packed1(); assert mode==1; yield new XorNode(null,null,null); } diff --git a/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/src/main/java/com/seaofnodes/simple/node/StoreNode.java index 010d5f61..204e0c4b 100644 --- a/src/main/java/com/seaofnodes/simple/node/StoreNode.java +++ b/src/main/java/com/seaofnodes/simple/node/StoreNode.java @@ -243,6 +243,10 @@ public class StoreNode extends MemOpNode { private byte storageType() { if( !(ptr()._type instanceof TypeMemPtr tmp) ) return 0; + // Completing a forward declaration changes TYPES without changing + // this Store's inputs. Wait for the pointer's type upgrade, which + // queues its users, before consulting the completed declaration. + if( tmp._obj._fref ) return 0; TypeStruct obj = (TypeStruct)Parser.TYPES.get(tmp._obj._name); Field fld = obj.field(_name); if( fld == null || fld._t == null ) diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/BitCastARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/BitCastARM.java new file mode 100644 index 00000000..ec57b3f8 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/BitCastARM.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.arm; + +import com.seaofnodes.isa.Arm64; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// A representation move, with no arithmetic, rounding, or flag changes. +public class BitCastARM extends MachConcreteNode implements MachNode { + private final boolean _toBits; + BitCastARM(Intrinsic.BitCastNode cast) { super(cast); _toBits=cast.toBits(); } + @Override public String op() { return _toBits ? "f64bits" : "f64frombits"; } + @Override public RegMask regmap(int i) { assert i==1; return _toBits ? arm.DMASK : arm.RMASK; } + @Override public RegMask outregmap() { return _toBits ? arm.WMASK : arm.DMASK; } + @Override public void encoding(Encoding enc) { + enc.add4(Arm64.f_mov_general(Arm64.OP_FMOV,0b01,0,_toBits ? 0b110 : 0b111, + enc.reg(in(1))-(_toBits ? arm.D_OFFSET : 0), + enc.reg(this)-(_toBits ? 0 : arm.D_OFFSET))); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = bits ").p(code.reg(in(1))); + } +} diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java index ee7b0fc3..2d47edf5 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java @@ -350,6 +350,7 @@ public class arm extends Machine { case StoreNode st -> st(st); case SubNode sub -> sub.mode()==2 ? new SubFARM(sub) : sub(sub); case ToFloatNode tfn -> new I2F8ARM(tfn); + case Intrinsic.BitCastNode cast -> new BitCastARM(cast); case ToIntegerNode cvt -> new F8ToIARM(cvt); case XorNode xor -> xor(xor); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BitCastRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BitCastRISC.java new file mode 100644 index 00000000..141ff7d5 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/BitCastRISC.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.riscv; + +import com.seaofnodes.isa.RiscV; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// A representation move, with no arithmetic, rounding, or flag changes. +public class BitCastRISC extends MachConcreteNode implements MachNode { + private final boolean _toBits; + BitCastRISC(Intrinsic.BitCastNode cast) { super(cast); _toBits=cast.toBits(); } + @Override public String op() { return _toBits ? "f64bits" : "f64frombits"; } + @Override public RegMask regmap(int i) { assert i==1; return _toBits ? riscv.FMASK : riscv.RMASK; } + @Override public RegMask outregmap() { return _toBits ? riscv.WMASK : riscv.FMASK; } + @Override public void encoding(Encoding enc) { + enc.add4(RiscV.r_type(RiscV.OP_FP, + enc.reg(this)-(_toBits ? 0 : riscv.F_OFFSET),0, + enc.reg(in(1))-(_toBits ? riscv.F_OFFSET : 0),0,_toBits ? 0b1110001 : 0b1111001)); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = bits ").p(code.reg(in(1))); + } +} diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java index d455337b..de754393 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java @@ -47,7 +47,7 @@ public class SplitRISC extends SplitNode { // FPR->FPR src -= riscv.F_OFFSET; dst -= riscv.F_OFFSET; - enc.add4(RiscV.r_type(RiscV.OP_FP, dst, 0b000, src, src, 0b0010101)); // dst = FMIN(src,src) + enc.add4(RiscV.r_type(RiscV.OP_FP, dst, 0b000, src, src, 0b0010001)); // FSGNJ.D preserves NaN bits. } else if(!srcX && dstX) { //GPR->FPR // fmv.d.x diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java index 378955a5..976d1a52 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java @@ -254,6 +254,7 @@ public class riscv extends Machine { case StoreNode st -> st(st); case SubNode sub -> sub.mode()==2 ? new SubFRISC(sub) : sub(sub); case ToFloatNode tfn -> i2f8(tfn); + case Intrinsic.BitCastNode cast -> new BitCastRISC(cast); case ToIntegerNode cvt -> new F8ToIRISC(cvt); case XorNode xor -> xor(xor); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/BitCastX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/BitCastX86.java new file mode 100644 index 00000000..bf550032 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/BitCastX86.java @@ -0,0 +1,23 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.util.SB; + +// A representation move, with no arithmetic, rounding, or flag changes. +public class BitCastX86 extends MachConcreteNode implements MachNode { + private final boolean _toBits; + BitCastX86(Intrinsic.BitCastNode cast) { super(cast); _toBits=cast.toBits(); } + @Override public String op() { return _toBits ? "f64bits" : "f64frombits"; } + @Override public RegMask regmap(int i) { assert i==1; return _toBits ? x86_64_v2.XMASK : x86_64_v2.RMASK; } + @Override public RegMask outregmap() { return _toBits ? x86_64_v2.WMASK : x86_64_v2.XMASK; } + @Override public void encoding(Encoding enc) { + X86.move(enc,!_toBits,_toBits, + enc.reg(this)-(_toBits ? 0 : x86_64_v2.XMM_OFFSET), + enc.reg(in(1))-(_toBits ? x86_64_v2.XMM_OFFSET : 0)); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" = bits ").p(code.reg(in(1))); + } +} 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/MemOpX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java index fcca7f8f..b0ecd685 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java @@ -67,9 +67,9 @@ public abstract class MemOpX86 extends MemOpNode implements MachNode { if( i==1 ) return null; // Memory if( i==2 ) return x86_64_v2.RMASK; // base in GPR if( i==3 ) return x86_64_v2.RMASK; // index in GPR - if( i==4 ) return _sz >= '4' - ? x86_64_v2.MEM_MASK // value in GPR or XMM - : x86_64_v2.RMASK; // Bytes and shorts in GPR only + // Integer arithmetic and comparisons require a GPR value operand. + // Only StoreX86 has alternate encodings that accept XMM values. + if( i==4 ) return x86_64_v2.RMASK; return null; // Anti-dependence } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegFX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegFX86.java new file mode 100644 index 00000000..61662fd2 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NegFX86.java @@ -0,0 +1,25 @@ +package com.seaofnodes.simple.node.cpus.x86_64_v2; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFloat; +import com.seaofnodes.simple.util.SB; + +// Flip the sign bit, preserving signed zero, infinities, and NaN payloads. +public class NegFX86 extends MachConcreteNode implements MachNode { + NegFX86(MinusNode neg) { + super(neg); + addDef(new FltX86(new ConstantNode(TypeFloat.constant(-0.0)),null)); + } + @Override public String op() { return "negf"; } + @Override public RegMask regmap(int i) { assert i==1 || i==2; return x86_64_v2.XMASK; } + @Override public RegMask outregmap() { return x86_64_v2.XMASK; } + @Override public int twoAddress() { return 1; } + @Override public void encoding(Encoding enc) { + X86.sse(enc,0x66,0x0F57,false,enc.reg(this)-x86_64_v2.XMM_OFFSET,enc.reg(in(2))-x86_64_v2.XMM_OFFSET); + } + @Override public void asm(CodeGen code, SB sb) { + sb.p(code.reg(this)).p(" ^= ").p(code.reg(in(2))).p(" // sign bit"); + } +} 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/StoreX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java index 9ffa25fc..8dfd31e7 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java @@ -25,6 +25,9 @@ public class StoreX86 extends MemOpX86 { } // Register mask allowed as a result. 0 for no register. @Override public RegMask outregmap() { return null; } + @Override public RegMask regmap(int i) { + return i==4 && _sz>='4' ? x86_64_v2.MEM_MASK : super.regmap(i); + } @Override public void encoding( Encoding enc ) { short ptr=enc.reg(ptr()), idx=enc.reg(idx()), src=enc.reg(val()); if( src==-1 ) X86.storeImm(enc,Integer.numberOfTrailingZeros(_sz-'0'),_imm,ptr,idx,_off,_scale); 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..dc15b185 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[]{ @@ -282,6 +294,7 @@ public class x86_64_v2 extends Machine { public Node instSelect(Node n) { return switch (n) { case AddNode add -> add(add); + case Intrinsic.BitCastNode cast -> new BitCastX86(cast); case AndNode and -> and(and); case BoolNode bool -> cmp(bool); case CLInitNode init -> new CLInitNode(init); @@ -298,10 +311,7 @@ public class x86_64_v2 extends Machine { case IfNode iff -> jmp(iff); case LoadNode ld -> ld(ld); case MemMergeNode mem -> new MemMergeNode(mem); - case MinusNode neg -> { - if( neg.mode()==2 ) throw Utils.TODO(); - yield new NegX86(neg); - } + case MinusNode neg -> neg.mode()==2 ? new NegFX86(neg) : new NegX86(neg); case MulNode mul -> mul.mode()==2 ? new MulFX86(mul) : mul(mul); case NewNode nnn -> new NewX86(nnn); case NotNode not -> new NotX86(not); @@ -370,8 +380,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/Allocations.java b/src/main/java/com/seaofnodes/simple/parser/Allocations.java index 861f95b6..64234045 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Allocations.java +++ b/src/main/java/com/seaofnodes/simple/parser/Allocations.java @@ -25,11 +25,20 @@ final class Allocations { } // Parse ary[ length_expr ] if( P.match("[") ) { - if( !t.makeZero().isa(t) ) - throw Parser.error("Cannot allocate a non-nullable, since arrays are always zero/null filled",site); + boolean readonly = P.match("~"); + if( P.match("]") ) { + P.require("{"); + return P.arrays.parse(t,readonly,null,site); + } Node len = P.expressions.parseAssignment().keep(); P.require("]"); + if( P.match("{") ) return P.arrays.parse(t,readonly,len,site); + if( !t.makeZero().isa(t) ) + throw Parser.error("Cannot allocate a non-nullable, since arrays are always zero/null filled",site); + if( readonly ) return new ReadOnlyNode(allocArray(P.types.typeAry(t,true),len)).peephole(); return allocArray(P.types.typeAry(t,false),len); + } else if( t instanceof TypeMemPtr ary && ary._obj.isAry() && P.match("{") ) { + return P.arrays.parse(ary._obj.field("[]")._t,ary._ro,null,site); } else { if( !(t instanceof TypeMemPtr tmp) ) throw Parser.error("Cannot allocate a "+t.str(),site); @@ -101,8 +110,6 @@ final class Allocations { // TODO: Needs runtime check. Node st = P.peep(new StoreNode(null, "#", 1, TypeInteger.U32, null, smem, self, ConFldOffNode.make(ts,"#"), len.unkeep(), true)); - // TODO: Allow add-on "constructor" to init the array elements - Node prior = P.mem(); Node esc = P.peep(new EscapeNode(lenFld,self,st,prior)); P.mem(P.mergeAlias(prior,lenFld._alias,esc)); @@ -117,7 +124,7 @@ final class Allocations { TypeStruct ts = P.types.typeAry(TypeInteger.U8,true); int lenAlias = ts.field("#" )._alias; int elemAlias = ts.field("[]")._alias; - TypeConAryB body = TypeConAryB.make(s); + TypeConAry body = TypeConAry.make(s); TypeInteger slen = TypeInteger.constant(s.length); // Make a TMP, not-null (byte)2, singleton (true) (requires text // strings are hash-interned), with a TypeStruct having a constant diff --git a/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java b/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java new file mode 100644 index 00000000..079853cd --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/parser/ArrayInitializers.java @@ -0,0 +1,187 @@ +package com.seaofnodes.simple.parser; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.ScopeNode.Kind; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; + +/** Array construction keeps the destination private until all elements exist. + * A generator is a scoped, immediately executed loop body, not an escaping closure. */ +final class ArrayInitializers { + private final Parser P; + Frame _frame; + + static final class Frame { + final Kind.Block _kind; + final int _depth; + final Var _value; + ScopeNode _returns; + Frame(Kind.Block kind, int depth, Var value) { + _kind=kind; _depth=depth; _value=value; + } + } + + ArrayInitializers(Parser P) { this.P=P; } + + // Opening brace consumed; an absent length means a list with inferred size. + Node parse(Type elem, boolean readonly, Node len, Lexer site) { + int pos=P.pos(); + Type indexType=P.types.parseType(); + String index=indexType==null ? null : P._lexer.matchId(); + boolean generator=index!=null && P.match("->"); + if( !generator ) P.pos(pos); + if( generator && Parser.KEYWORDS.contains(index) ) + throw Parser.error("Expected an array index name",site); + if( generator && (indexType!=TypeInteger.BOT && indexType!=TypeInteger.I32 && indexType!=TypeInteger.U32) ) + throw Parser.error("Array generator index must be int, i64, or u32",site); + if( generator && len==null ) + throw Parser.error("Array generator requires a length",site); + + Ary<Node> values=new Ary<>(Node.class); + if( !generator ) { + if( !P.peek('}') ) do { + values.add(P.conversions.convertExpr(P.expressions.parseAssignment(),elem).keep()); + } while( P.match(",") && !P.peek('}') ); + P.require("}"); + if( len==null ) len=P.con(values.size()).keep(); + } + + TypeStruct ts=P.types.typeAry(elem,readonly); + Field body=ts.field("[]"), length=ts.field("#"); + Node size=new AddNode(ConFldOffNode.make(ts,"[]"), + new ShlNode(null,len,P.con(ts.aryScale())).peephole()).peephole(); + NewNode alloc=new NewNode(ts,P.ctrl(),size).init(); + Node self=new ProjNode(alloc,0,ts.str()).init().keep(); + Node mem=new ProjNode(alloc,1,"#arrayMem").init().keep(); + Node slen=new StoreNode(site,"#",length._alias,TypeInteger.U32,null,mem,self, + ConFldOffNode.make(ts,"#"),len,true).peephole().keep(); + + if( generator ) { + mem=generator(ts,self,mem,len,index,indexType,site); + } else { + for( int i=0; i<values.size(); i++ ) { + Node st=store(ts,self,mem,P.con(i),values.at(i),site).keep(); + mem.unkeep(); + mem=st; + } + } + + Node result=new ArrayInitNode(P.ctrl(),mem,self,len, + generator ? -1 : values.size(),site,values).peephole().keep(); + Node prior=P.mem(); + P.mem(P.mergeAlias(prior,length._alias,new EscapeNode(length,result,slen.unkeep(),prior).peephole())); + prior=P.mem(); + P.mem(P.mergeAlias(prior,body._alias,new EscapeNode(body,result,mem,prior).peephole())); + mem.unkeep(); self.unkeep(); len.unkeep(); + for( Node value : values ) value.unkeep(); + result.unkeep(); + return readonly ? new ReadOnlyNode(result).peephole() : result; + } + + private Node store(TypeStruct ts, Node self, Node mem, Node idx, Node value, Lexer site) { + Field fld=ts.field("[]"); + Node off=new AddNode(ConFldOffNode.make(ts,"[]"), + new ShlNode(null,idx,P.con(ts.aryScale())).peephole()).peephole(); + return new StoreNode(site,"[]",fld._alias,fld._t,P.ctrl(),mem,self,off,value,true).peephole(); + } + + private Var define(String name, Type type, boolean fin, Node value, Lexer site) { + if( !P._scope.define(name,type,fin,value,site) ) throw Parser.error("Duplicate name '"+name+"'",site); + return P._scope.lookup(name); + } + + private Node read(Var var) { return P._scope.in(P._scope.update(var,null)); } + + private Node generator(TypeStruct ts, Node self, Node mem, Node len, String name, + Type indexType, Lexer site) { + Frame outer=_frame; + ScopeNode oldBreak=P.statements._breakScope, oldContinue=P.statements._continueScope; + P.statements._breakScope=P.statements._continueScope=null; + Kind.Block kind=new Kind.Block(); + P._scope.push(kind); + // The counter stays wide even when the source index is a narrow int. + // The private memory and result slots cannot be named in source. + Var counter=define("$arrayIndex",TypeInteger.BOT,false,P.con(0),site); + Var memory=define("$arrayMem",TypeMem.makePrivate(Type.BOTTOM),false,mem.unkeep(),site); + Var result=define("$arrayValue",ts.field("[]")._t,false,P.con(Type.TOP),site); + _frame=new Frame(kind,P._scope.depth(),result); + + P.ctrl(new LoopNode(site,null,P.ctrl()).peephole()); + ScopeNode head=P._scope.keep(); + P._xScopes.push(P._scope=P._scope.dup(true)); + Node idx=read(counter).keep(); + Node pred=new BoolNode.LT(idx,len).peephole().keep(); + Node iff=new IfNode(P.ctrl(),pred).peephole().keep(); + CFGNode yes=new CProjNode(iff,0,"True").peephole().keep(); + CFGNode no=(CFGNode)new CProjNode(iff.unkeep(),1,"False").peephole(); + P.ctrl(no); + ScopeNode exit=P._scope.dup(); + P._xScopes.push(exit); + exit.addGuards(no,pred,true); + P.ctrl(yes.unkeep()); + P._scope.addGuards(yes,pred.unkeep(),false); + + P._scope.push(new Kind.Block()); + define(name,indexType,true,new ConvertNode(indexType,idx).peephole(),site); + Node last=P._code.ZERO; + while( !P.peek('}') && !P._lexer.isEOF() ) { + P._code.add(last); + last=P.statements.parseStatement(); + } + P.require("}"); + if( P.ctrl()._type!=Type.XCONTROL ) addReturn(last); + P._scope.pop(); + if( _frame._returns!=null ) { + P._scope.kill(); + P._scope=_frame._returns; + } + Node value=read(result); + // The loop starts with the whole private array memory. Keep that shape + // on the backedge too: mixing it with an element slice yields BOTTOM, + // which would falsely publish every function when the array escapes. + Node prior=read(memory).keep(); + Node st=store(ts,self,prior,idx,value,site); + P._scope.update(memory,P.mergeAlias(prior.unkeep(),ts.field("[]")._alias,st)); + P._scope.update(counter,new AddNode(idx.unkeep(),P.con(1)).peephole()); + P._scope.removeGuards(yes); + head.balanceLoopFRefs(P._scope); + exit.balanceLoopFRefs(P._scope); + head.endLoop(P._scope,exit); + head.unkeep().kill(); + P._xScopes.pop(); + P._xScopes.pop(); + if( !P._xScopes.isEmpty() ) { P._xScopes.pop(); P._xScopes.push(exit); } + P._scope=exit; + Node done=read(memory).keep(); + P._scope.pop(); + _frame=outer; + P.statements._breakScope=oldBreak; + P.statements._continueScope=oldContinue; + return done; + } + + Node addReturn(Node expr) { + Node value=P.conversions.convertExpr(expr,_frame._value.type()); + value=new ArrayInitNode(P.ctrl(),value,P.loc()).peephole().keep(); + P.mem(); // Materialize lazy memory before cloning a return path. + ScopeNode cur=P._scope.dup(); + // Saved paths share Var records with the active scope. Drop locals + // without promoting them, then copy forward references explicitly. + while( cur.depth()>_frame._depth ) cur._pop(); + cur.balanceLoopFRefs(P._scope); + cur.update(_frame._value,value.unkeep()); + if( _frame._returns==null ) _frame._returns=cur; + else { + _frame._returns.balanceLoopFRefs(cur); + cur.balanceLoopFRefs(_frame._returns); + _frame._returns.ctrl(_frame._returns.mergeScopes(cur,P.loc()).peephole()); + } + P.ctrl(P._code.XCTRL); + return P._code.ZERO; + } + + void checkAssignment(Var var, Lexer site) { + if( _frame!=null && var._idx<_frame._kind._lexSize ) + throw Parser.error("Cannot assign captured local '"+var._name+"' in an array generator",site); + } +} diff --git a/src/main/java/com/seaofnodes/simple/parser/Conversions.java b/src/main/java/com/seaofnodes/simple/parser/Conversions.java index fa11311d..72247ca6 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Conversions.java +++ b/src/main/java/com/seaofnodes/simple/parser/Conversions.java @@ -37,7 +37,7 @@ final class Conversions { case TypeFloat tf -> TypeFloat.F64; case TypeMemPtr tmp -> tmp.makeVar(); case TypeFunPtr tfp -> tfp; - case TypeConAry<?> a -> a; + case TypeConAry a -> a; case TypePtr ptr -> TypePtr.PTR; case TypeScalar ts -> TypeScalar.BOT; default -> t; diff --git a/src/main/java/com/seaofnodes/simple/parser/Declarations.java b/src/main/java/com/seaofnodes/simple/parser/Declarations.java index 13fa59b8..473a7c24 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Declarations.java +++ b/src/main/java/com/seaofnodes/simple/parser/Declarations.java @@ -91,6 +91,10 @@ final class Declarations { // expr is a constant function if( t instanceof TypeFunPtr decl && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { t = decl.makeFrom(tfp.fidx()); + // The explicit signature specifies source parameters/return; + // the receiver of a method body is still inferred. + if( expr instanceof FunPtrNode && P.structs.inStructDeclaration() ) + t = ((TypeFunPtr)t).makeFrom(tfp.arg(0),0); FunNode fun = P._code.link(tfp); if( fun != null ) fun.setName(name); // Assign debug name to Simple function diff --git a/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/Functions.java b/src/main/java/com/seaofnodes/simple/parser/Functions.java index 5175acc6..84bd509e 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Functions.java +++ b/src/main/java/com/seaofnodes/simple/parser/Functions.java @@ -14,6 +14,11 @@ final class Functions { // Merge all function exits here. ScopeNode _returnScope; + // Instance method headers are available immediately; their bodies need + // the complete field scope, just like explicitly declared constructors. + private record Method(FunPtrNode ptr, Lexer body, Lexer loc, BitSet finals, String[] ids) {} + private Ary<Method> _methods; + Functions(Parser P) { this.P = P; } /** @@ -29,6 +34,7 @@ final class Functions { Ary<String> ids = new Ary<>(String.class); BitSet finals = new BitSet(); Type self = P.types.implicitSelfType(); + if( P.structs.inStructDeclaration() ) self = self.makeRO(); ts.push(self); ids.push(self == TypePtr.PTR ? "#self" : "self"); @@ -38,10 +44,49 @@ final class Functions { parameters(ts,ids,finals,false); // Make a concrete function type, with a fidx TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),Type.BOTTOM,P._code.fidx(P._ref._cname)); + if( P.structs.inStructDeclaration() ) { + FunPtrNode ptr = (FunPtrNode)new FunPtrNode(tfp,P._code._start,null).peephole().keep(); + _methods.push(new Method(ptr,P.loc(),loc,finals,ids.asAry())); + P._lexer.skipTo('{','}',"method body"); + return ptr; + } ReturnNode ret = parseFunctionBody(tfp,loc,finals,ids.asAry()); return new FunPtrNode(tfp,P._code._start,ret).peephole(); } + private void finishMethods() { + if( _methods == null ) return; + Ary<Method> methods = _methods; + Lexer after = P._lexer; + for( Method method : methods ) { + P._lexer = new Lexer(method.body()); + TypeFunPtr sig = (TypeFunPtr)method.ptr()._con; + ReturnNode ret = parseFunctionBody(sig,method.loc(),method.finals(),method.ids()); + method.ptr().setDef(1,ret); + P.require("}"); + } + Ary<FunNode> functions = new Ary<>(FunNode.class); + for( Method method : methods ) functions.add(method.ptr().fun()); + MethodAccess.infer(functions); + for( Method method : methods ) { + ReturnNode ret = method.ptr().ret(); + TypeFunPtr sig = ret.fun().sig(); + method.ptr().peephole(); + // Declarations saw the header before its FunNode existed. + for( Var var : P._scope._vars ) + if( var.type() instanceof TypeFunPtr tfp && tfp.isConstant() && tfp.fidx() == sig.fidx() ) { + ret.fun().setName(var._name); + var.refineType(var.type().join(method.ptr()._type)); + TypeStruct self = P._scope.constructorSelf(); + TypeStruct shape = (TypeStruct)Parser.TYPES.get(self._name); + Parser.TYPES.put(shape._name,shape.replace(shape.field(var._name).makeFrom(var.type()))); + break; + } + method.ptr().unkeep(); + } + P._lexer = after; + } + Node parseConstructor(String name, boolean explicit) { P.require("{"); Ary<Type> ts = new Ary<>(Type.class); @@ -198,9 +243,13 @@ final class Functions { } ReturnNode parseBody( String funName, TypeFunPtr sig, Lexer loc, BitSet finals, String[] ids) { + ArrayInitializers.Frame outerArray = P.arrays._frame; + P.arrays._frame = null; Structs.Constructor outerCtor = P.structs._pendingCtor; P.structs._pendingCtor = null; boolean isInit = FunNode.isInit(funName); + Ary<Method> outerMethods = _methods; + _methods = FunNode.isInstance(funName) ? new Ary<>(Method.class) : null; boolean isCtor = isInit || P.structs.isExplicitConstructorName(funName); String typeName = isCtor ? ((TypeMemPtr)sig._sig[0])._obj._name : null; @@ -233,7 +282,11 @@ final class Functions { nvar = P.structs.updateSelfAsFieldsDiscovered( typeName, nvar ); } - if( isInit ) P.structs.finishConstructor(); + if( isInit ) { + finishMethods(); + P.structs.finishConstructor(); + } + _methods = outerMethods; P.structs._pendingCtor = outerCtor; ParmNode self = fun.parm(2); @@ -275,6 +328,7 @@ final class Functions { P.statements._breakScope = breakScope; P._scope.pop(); P.construction._frame = outerConstruction; + P.arrays._frame = outerArray; return ret; } @@ -300,8 +354,14 @@ final class Functions { // All args, "as-if" called externally for( int i = 0; i<ids.length; i++ ) { Type t = sig.arg(i); + // Parse an implicit receiver with write access, then infer the + // public requirement from its uses once every method is present. + if( i == 0 && ids[0].equals("self") && !FunNode.isInit(funName) && + !(sig.ret() instanceof TypeMem) && t instanceof TypeMemPtr ptr ) + t = ptr.withAccess(false); // Args take a default input if called from Start/external-world - Node parm = new ParmNode(ids[i],i+2,t,fun,ConstantNode.seed(fun.defaultArg(i)).peephole()).peephole(); + Type seed = i == 0 && t instanceof TypeMemPtr ? t : fun.defaultArg(i); + Node parm = new ParmNode(ids[i],i+2,t,fun,ConstantNode.seed(seed).peephole()).peephole(); if( t instanceof TypeMemPtr ptr && ptr._partial ) parm = new ConstructNode(parm).peephole(); P._scope.define(ids[i], t, (i==0 && ids[0]=="self") || (finals!=null && finals.get(i)), parm, loc); } diff --git a/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/MethodAccess.java b/src/main/java/com/seaofnodes/simple/parser/MethodAccess.java new file mode 100644 index 00000000..099ba036 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/parser/MethodAccess.java @@ -0,0 +1,130 @@ +package com.seaofnodes.simple.parser; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/** Infer the access required through an instance method's receiver. */ +final class MethodAccess { + private final FunNode _fun; + private final Set<Node> _aliases = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set<MethodAccess> _calls = new HashSet<>(); + private boolean _writes; + + private MethodAccess(FunNode fun) { _fun = fun; } + + static void infer(Ary<FunNode> functions) { + HashMap<Integer,MethodAccess> methods = new HashMap<>(); + for( FunNode fun : functions ) methods.put(fun.sig().fidx(),new MethodAccess(fun)); + for( MethodAccess method : methods.values() ) method.scan(methods); + boolean changed; + do { + changed = false; + for( MethodAccess method : methods.values() ) + if( !method._writes ) + for( MethodAccess callee : method._calls ) + if( callee._writes ) { method._writes = true; changed = true; break; } + } while( changed ); + for( MethodAccess method : methods.values() ) method.finish(); + } + + private void scan(HashMap<Integer,MethodAccess> methods) { + if( _fun.parm(2) == null ) return; // Unused receiver. + ArrayDeque<Node> work = new ArrayDeque<>(); + work.add(_fun.parm(2)); + while( !work.isEmpty() ) { + Node value = work.remove(); + if( !_aliases.add(value) ) continue; + for( Node use : value._outputs ) { + if( use == null || use instanceof ReadOnlyNode ) continue; + if( use instanceof StoreNode store ) { + if( store.ptr() == value || store.val() == value ) _writes = true; + } else if( use instanceof CallNode call ) { + for( int i = 2; i < call.nIns()-1; i++ ) + if( call.in(i) == value ) call(call,i-2,methods); + } else if( use instanceof ReturnNode ret ) { + // Returning a writable alias must not grant write access + // to an object reached through a read-only receiver. + if( ret.expr() == value ) _writes = true; + } else if( use instanceof LoadNode load && load.ptr() == value ) { + Type type = type(load); + if( type instanceof TypeMemPtr ptr ) { + if( !ptr._ro ) work.add(load); + } else if( type == Type.BOTTOM || type == TypeScalar.BOT ) _writes = true; + } else if( use instanceof PhiNode || use instanceof GuardNode ) { + work.add(use); + } else if( use instanceof ConvertNode convert ) { + if( !(convert.dst() instanceof TypeMemPtr ptr && ptr._ro) ) _writes = true; + } else if( use instanceof BoolNode || use instanceof NotNode || + use instanceof FldOffNode || use instanceof ScopeNode || use instanceof CLInitNode ) { + // Comparisons and field offsets do not expose write access. + } else { + // Includes raw-address casts and unrecognized escapes. + _writes = true; + } + } + } + } + + private void call(CallNode call, int arg, HashMap<Integer,MethodAccess> methods) { + // Every member of this struct is now declared. An unresolved bare + // function name belongs to an outer scope, whose hidden slot is not self. + if( arg == 0 && call.fptr() instanceof FRefNode ) return; + Type type = type(call.fptr()); + if( !(type instanceof TypeFunPtr sig) || arg >= sig.nargs() ) { _writes = true; return; } + if( arg == 0 && sig.arg(0) == TypePtr.PTR ) return; + if( arg == 0 ) { + if( XInt.isHigh(sig.fidxs()) ) { _writes = true; return; } + for( int fidx = XInt.next(sig.fidxs(),-1); fidx >= 0; fidx = XInt.next(sig.fidxs(),fidx) ) { + MethodAccess local = methods.get(fidx); + if( local != null ) _calls.add(local); + else { + FunNode target = CodeGen.CODE.link(fidx); + Type receiver = target == null ? sig.arg(0) : target.sig().arg(0); + if( !(receiver instanceof TypeMemPtr ptr && ptr._ro) ) _writes = true; + } + } + return; + } + if( !(sig.arg(arg) instanceof TypeMemPtr ptr && ptr._ro) ) _writes = true; + } + + private static Type type(Node value) { + // Flow types can still be BOTTOM while the parser discovers fields. + if( value instanceof LoadNode load ) { + TypeStruct obj = load.off() instanceof ConFldOffNode off ? off._ts : + type(load.ptr()) instanceof TypeMemPtr ptr ? ptr._obj : null; + if( obj != null ) { + if( !obj.isAry() && Parser.TYPES.get(obj._name) instanceof TypeStruct latest ) obj = latest; + Field field = obj.field(load._name); + if( field != null ) return field._t; + } + } + return value._type; + } + + private void finish() { + ParmNode self = _fun.parm(2); + if( self == null ) return; + TypeMemPtr receiver = ((TypeMemPtr)self._declaredType).withAccess(!_writes); + _fun.setSig(_fun.sig().makeFrom(receiver,0)); + if( _writes ) return; + self._declaredType = receiver; + self.setDef(1,ConstantNode.seed(receiver).peephole()); + // These parser-time assumptions are settled before Iter/SCCP. No + // writable alias escapes a method classified as read-only. + for( Node alias : _aliases ) + if( alias._type instanceof TypeMemPtr ptr ) { + alias._type = ptr.makeRO(); + CodeGen.CODE.add(alias); + CodeGen.CODE.addAll(alias._outputs); + } + } +} diff --git a/src/main/java/com/seaofnodes/simple/parser/Parser.java b/src/main/java/com/seaofnodes/simple/parser/Parser.java index 0cf3c0e7..9c18385d 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Parser.java +++ b/src/main/java/com/seaofnodes/simple/parser/Parser.java @@ -7,7 +7,7 @@ import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.Ary; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashSet; +import java.util.LinkedHashMap; import java.util.Stack; /** Source-unit entry point and shared cursor, scope, and IR utilities. @@ -21,6 +21,7 @@ public class Parser { final Functions functions = new Functions(this); final Structs structs = new Structs(this); final Allocations allocations = new Allocations(this); + final ArrayInitializers arrays = new ArrayInitializers(this); final References references = new References(this); final Conversions conversions = new Conversions(this); final Construction construction = new Construction(this); @@ -91,7 +92,7 @@ public class Parser { public final Stack<ScopeNode> _xScopes = new Stack<>(); // Layouts needed by field accesses in this source file. Resolve after // parsing, since a later local struct declaration can satisfy the use. - public final LinkedHashSet<String> _requiredTypes = new LinkedHashSet<>(); + public final LinkedHashMap<String,Lexer> _requiredTypes = new LinkedHashMap<>(); // Mapping from a type name to a Type. The string name matches // `type.str()` call. @@ -120,6 +121,11 @@ public class Parser { public final Lexer _loc; ParseException( String msg, Lexer loc ) { super(msg); _loc = loc==null ? null : new Lexer(loc); } public String diagnostic() { return _loc==null ? getMessage() : _loc.diagnostic(getMessage()); } + // JUnit and uncaught-exception reporters use these entry points rather + // than the CLI's diagnostic(). Source errors should retain their source + // excerpt and caret there too. getMessage() stays stable for callers. + @Override public void printStackTrace(java.io.PrintStream out) { out.println(diagnostic()); } + @Override public void printStackTrace(java.io.PrintWriter out) { out.println(diagnostic()); } } public Parser(CodeGen code ) { @@ -265,7 +271,7 @@ public class Parser { // Field offset for an unknown type public Node fldoff( Node n, String name ) { if( n._type instanceof TypeMemPtr ptr && ptr._obj._fref ) - _requiredTypes.add(ptr._obj._name); + _requiredTypes.putIfAbsent(ptr._obj._name,loc()); return new FldOffNode(n,name,loc()).peephole(); } public Node peep( Node n ) { diff --git a/src/main/java/com/seaofnodes/simple/parser/README.md b/src/main/java/com/seaofnodes/simple/parser/README.md index cee1d64a..48cf13c8 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 | @@ -14,6 +14,7 @@ productions to their entry points. | `Declarations.java` | Bindings, initializers, mutability modifiers, and native declarations. | | `Types.java` | Type syntax, qualified-name lookup, arrays, function types, and forward types. | | `Functions.java` | Function expressions, parameters, arguments, calls, and function entry/return graphs. | +| `MethodAccess.java` | Infers whether an instance method needs writable access through its receiver. | | `Structs.java` | Struct and constructor declarations, field discovery, and initialization Stores. | | `Construction.java` | Selects private self memory during parsing; its comment explains BOT fields, the TypeMemPtr qualifier, and checks deferred to TypeCheck. | | `References.java` | Bare-name and field resolution, implicit receivers, assignments, and forward references. | @@ -23,8 +24,16 @@ productions to their entry points. | `Var.java` | Lexical binding metadata: name, type, scope slot, and mutability. | Start with `Statements.parseStatement` or `Expressions.parseAssignment` to follow -the grammar. Loop updates are parsed after their bodies, and declared constructor -bodies after field discovery; `Lexer.skipTo` scans these deferred fragments. +the grammar. Loop updates are parsed after their bodies. Instance method headers +are parsed immediately, but their bodies and declared constructor bodies wait +until all fields are known. Methods can therefore use fields declared later in +the struct. `Lexer.skipTo` scans these deferred fragments. + +Method receivers accept read-only access unless the body needs writable access. +The check follows aliases, child references, array elements, and calls between +methods. Writing an independent argument does not require a writable receiver. +Returning or storing a writable receiver alias also requires writable access; +an explicit read-only view can be exposed without that requirement. `Parser` keeps the short cursor, scope/memory, IR-building, and diagnostic helpers used by these components. Independent helpers live with their data: class-object diff --git a/src/main/java/com/seaofnodes/simple/parser/References.java b/src/main/java/com/seaofnodes/simple/parser/References.java index 6335bad2..7724103e 100644 --- a/src/main/java/com/seaofnodes/simple/parser/References.java +++ b/src/main/java/com/seaofnodes/simple/parser/References.java @@ -120,6 +120,7 @@ final class References { return P.expressions.parsePostfixMethod(P.construction.view(rvalue),P._scope.in(P._scope.update(P._scope.var(selfx),null))); } // Assign-update direct into Scope + P.arrays.checkAssignment(var,site); if( var._final && !P._scope.canInit(var) ) throw Parser.error("Cannot reassign final '"+var._name+"'",site); checkSharedFunction(var); @@ -130,6 +131,7 @@ final class References { } void checkAssignment(Var def, Lexer site) { + if( fieldSelf(def)==null ) P.arrays.checkAssignment(def,site); String name = def._name; // A final field may receive its first value in its constructor. if( P._scope.in(def._idx)._type!=Type.TOP && def._final && @@ -278,7 +280,7 @@ final class References { // Check for assign-update, "ptr.fld += expr" or "ary[idx]++" char ch = pre==0 ? P._lexer.matchOperAssign() : (char)pre; if( ch!=0 ) { - if( fld!=null && decl==Type.BOTTOM ) + if( fld!=null && decl==Type.BOTTOM && !ts._fref ) throw Parser.error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor",site ); Node op = P.expressions.opAssign(ch,load, decl ).keep(); mem.keep(); @@ -306,10 +308,15 @@ final class References { // Fields are lvalues in memory, including inside their initializer and // explicit constructor. Scope slots retain declaration metadata only. private Node fieldSelf(Var var) { - int kx = P._scope.kindx(var), fx = P._scope.enclosingFuncOrDecl(); + int kx = P._scope.kindx(var); ScopeNode.Kind kind = P._scope._kinds.at(kx); if( !(kind instanceof Kind.Func inst) || !FunNode.isInstance(inst._name) || var._idx<inst._lexSize+2 ) return null; - if( kx==fx || kx+1==fx ) return defaultSelf(); + // Nested functions have their own receiver parameter. Resolve fields + // through it even across intervening function and block scopes. + Node self = defaultSelf(); + Type owner = P._scope.var(inst._lexSize).type(); + if( self._type instanceof TypeMemPtr ptr && owner instanceof TypeMemPtr declared && + ptr._obj._name.equals(declared._obj._name) ) return self; return null; } diff --git a/src/main/java/com/seaofnodes/simple/parser/Statements.java b/src/main/java/com/seaofnodes/simple/parser/Statements.java index 707202ab..0c2a25f7 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Statements.java +++ b/src/main/java/com/seaofnodes/simple/parser/Statements.java @@ -78,6 +78,7 @@ final class Statements { */ private Node parseReturn() { var expr = P.require(P.expressions.parseAssignment(), ";"); + if( P.arrays._frame!=null ) return P.arrays.addReturn(expr); return P.functions.addReturn(expr); } 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/Types.java b/src/main/java/com/seaofnodes/simple/parser/Types.java index 827518fd..501924f0 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Types.java +++ b/src/main/java/com/seaofnodes/simple/parser/Types.java @@ -37,6 +37,12 @@ final class Types { if( t0==Type.BOTTOM || t0==Type.TOP ) return t0; // var/val tname = typeName(tname).intern(); t0 = Parser.TYPES.get(tname); + // Bare forward types use the same file-relative name as `new`. + // A later sibling declaration must complete this very type. + if( t0 == null && tname.indexOf('.') == -1 ) { + tname = fullTypeName(tname); + t0 = Parser.TYPES.get(tname); + } } Type t1 = t0==null ? TypeStruct.forward(tname) : t0; @@ -135,9 +141,8 @@ final class Types { Type implicitSelfType() { TypeStruct self = P._scope.constructorSelf(); - // Ordinary instance methods accept the declared object shape. The - // constructor-only receiver starts at the low/default field values, - // and is built explicitly by parseConstructor(). + // Function types without a body must allow for writes. Function + // expressions can infer a weaker requirement in MethodAccess. return self == null ? TypePtr.PTR : TypeMemPtr.make(self); } diff --git a/src/main/java/com/seaofnodes/simple/parser/Var.java b/src/main/java/com/seaofnodes/simple/parser/Var.java index ddbb91d8..5361148f 100644 --- a/src/main/java/com/seaofnodes/simple/parser/Var.java +++ b/src/main/java/com/seaofnodes/simple/parser/Var.java @@ -33,6 +33,11 @@ public class Var { public Type type() { return _type; } public Type _type() { return _type; } // ScopeNode printer needs to have no side effects + void refineType(Type type) { + assert type.isa(_type) : _name+": "+_type+" -> "+type; + _type = type; + } + // Forward reference variables (not types) must be BOTTOM and // distinct from inferred variables public boolean isFRef() { return _fref; } diff --git a/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..e4d1659d 100644 --- a/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java +++ b/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java @@ -10,19 +10,22 @@ 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> { private final CodeGen _code; private final Encoding _enc; - private ASMPrinter(CodeGen code) { + private ASMPrinter(CodeGen code, Encoding enc) { super(new IRPrinter()); _code=code; - _enc=code._encoding; + _enc=enc; } public static SB print(SB sb, CodeGen code) { - return sb.p(new com.seaofnodes.print.ASMPrinter<>(new ASMPrinter(code)).print()); + return print(sb,code,code._encoding); + } + public static SB print(SB sb, CodeGen code, Encoding enc) { + return sb.p(new com.seaofnodes.print.ASMPrinter<>(new ASMPrinter(code,enc)).print()); } @Override public ArrayList<Node> blocks() { var blocks=new ArrayList<Node>(); @@ -89,11 +92,11 @@ public final class ASMPrinter extends AssemblyAdapter<Node> { var data=new ArrayList<Data>(); for( var obj : _enc._data.entries() ) if( !obj.external && obj.readOnly==ro ) - data.add(new Data(obj,obj.symbol+": "+obj.type.str(),obj.alignment,obj.size)); + data.add(new Data(obj,obj.label()+": "+obj.type.str(),obj.alignment,obj.size)); 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/Type.java b/src/main/java/com/seaofnodes/simple/type/Type.java index 0f655f49..2e228494 100644 --- a/src/main/java/com/seaofnodes/simple/type/Type.java +++ b/src/main/java/com/seaofnodes/simple/type/Type.java @@ -531,7 +531,7 @@ public class Type /*implements Cloneable*/ { // tag-space except for the first tag-space which uses 1 tag for // multiple Types instead of 1 Type having multiple tags. Has to // be ordered to match _type enum. - for( Type t : new Type[]{Type.XNIL,TypeScalar.BOT,TypePtr.PTR,TypeInteger.BOT,TypeFloat.F64,TypeConAryB.ABC,TypeRPC.BOT,TypeMemPtr.BOT,TypeFunPtr.BOT,TypeMem.BOT,Field.BOT,TypeStruct.BOT,TypeTuple.BOT} ) + for( Type t : new Type[]{Type.XNIL,TypeScalar.BOT,TypePtr.PTR,TypeInteger.BOT,TypeFloat.F64,TypeConAry.ABC,TypeRPC.BOT,TypeMemPtr.BOT,TypeFunPtr.BOT,TypeMem.BOT,Field.BOT,TypeStruct.BOT,TypeTuple.BOT} ) TAGOFFS[t._type+1] = TAGOFFS[t._type] + t.TAGOFF(); assert TAGOFFS[TMAX] <= 255; return TAGOFFS; diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAry.java b/src/main/java/com/seaofnodes/simple/type/TypeConAry.java index 9cb23e09..c10f09aa 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeConAry.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeConAry.java @@ -1,33 +1,48 @@ package com.seaofnodes.simple.type; import com.seaofnodes.simple.util.BAOS; -import com.seaofnodes.simple.util.Utils; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; /** - * Represents a constant array of primitives + * Constant integer contents with an explicit storage width and signedness. */ -public abstract class TypeConAry<A> extends TypeScalar { +public final class TypeConAry extends TypeScalar { boolean _any; byte _widen; - // One of byte or int array - public final A _ary; + private final TypeInteger _storage; + private final long[] _ary; - TypeConAry( boolean any, byte widen, A ary ) { super(TCONARY); _any = any; _widen = widen; _ary = ary; } + private TypeConAry(boolean any, byte widen, TypeInteger storage, long[] ary) { + super(TCONARY); + _any=any; _widen=widen; _storage=storage; _ary=ary; + } + static final TypeConAry ABC=make("abc"); + static final TypeConAry ABCD=make("abcd"); public static void gather(ArrayList<Type> ts) { - ts.add(TypeConAryB.make("")); - ts.add(TypeConAryB.ABC); - ts.add(TypeConAryB.ABCD); - ts.add(TypeConAryI.I123); + ts.add(make("")); + ts.add(ABC); + ts.add(ABCD); + ts.add(make(TypeInteger.I32,new long[]{1,2,3})); + ts.add(make(TypeInteger.U16,new long[]{0,65535})); + ts.add(make(TypeInteger.BOT,new long[]{Long.MIN_VALUE,Long.MAX_VALUE})); + for( TypeInteger storage : STORAGE ) { + ts.add(make(storage,new long[0])); + ts.add(make(storage,new long[]{0,1})); + } } // Fresh, uninterned type sharing this array; xdual must not intern. - abstract TypeConAry<A> _make(boolean any, byte widen); + private TypeConAry _make(boolean any, byte widen) { + return new TypeConAry(any,widen,_storage,_ary); + } - @Override TypeConAry<A> xdual() { return _make( !_any, (byte)(3-_widen) ); } + @Override TypeConAry xdual() { return _make( !_any, (byte)(3-_widen) ); } @Override Type xmeet(Type t) { TypeConAry ary = (TypeConAry)t; // Invariant + if( len()==0 ) return emptyMeet(ary); + if( ary.len()==0 ) return ary.emptyMeet(this); // Same base array but different? if( _equals(ary) ) // Return array with larger widen @@ -37,15 +52,7 @@ public abstract class TypeConAry<A> extends TypeScalar { } Type ymeet( TypeInteger ti ) { - // Empty arrays have no observed elements: use the storage bounds. - // Check both endpoints just as the loop checks a nonempty body; - // retaining high integer inputs here preserves lattice duality. - if( len()==0 ) { - TypeInteger bound=emptyElem(); - if( !ti.isa(TypeInteger.make(bound._min,bound._min,ti._widen)) || - !ti.isa(TypeInteger.make(bound._max,bound._max,ti._widen)) ) - return ti.meet(bound); - } + if( len()==0 ) return emptyMeet(ti); // if i can isa *each* element, then maybe can keep. // no good to i.isa(elem()) because fails the dual for( int j=0; j<len(); j++ ) @@ -56,12 +63,21 @@ public abstract class TypeConAry<A> extends TypeScalar { } private boolean _equals(TypeConAry ary) { - int len = len(); - if( len != ary.len() ) return false; - for( int i=0; i<len; i++ ) - if( at8(i) != ary.at8(i) ) - return false; - return true; + return _storage==ary._storage && (len()!=0 || _any==ary._any) && Arrays.equals(_ary,ary._ary); + } + @Override boolean eq(Type t) { return t instanceof TypeConAry ary && _widen==ary._widen && _equals(ary); } + @Override int hash() { return Arrays.hashCode(_ary)^_storage.hashCode()^(len()==0 && _any ? 1231 : 0); } + @Override public String str() { + boolean text=_storage==TypeInteger.U8; + for( long value : _ary ) + if( value<32 || value>126 ) { text=false; break; } + if( !text ) { + String range=elem().str(); + return range.startsWith("[") ? range : "["+range+"]"; + } + byte[] bytes = new byte[len()]; + for( int i=0; i<bytes.length; i++ ) bytes[i]=(byte)_ary[i]; + return "[\""+new String(bytes)+"\"]"; } @Override public boolean isHigh() { return this==TOP; } @@ -70,8 +86,6 @@ public abstract class TypeConAry<A> extends TypeScalar { // Meet-over-elements type public Type elem() { - if( _ary==null ) - return _any ? Type.TOP : BOTTOM; int len = len(); long min = Long.MAX_VALUE; if( len==0 ) return emptyElem(); @@ -83,23 +97,80 @@ public abstract class TypeConAry<A> extends TypeScalar { return TypeInteger.make(min,max,_widen); } private TypeInteger emptyElem() { - return log_size()==0 ? TypeInteger.make(0,255,_widen) - : TypeInteger.make(Integer.MIN_VALUE,Integer.MAX_VALUE,_widen); + TypeInteger storage=storageType(); + return _any ? TypeInteger.make(storage._min,storage._max,_widen) + : TypeInteger.make(storage._max,storage._min,_widen); + } + // Keep the empty payload distinct from its element bound. It sits just + // above the storage dual; its own dual sits just below the storage bound. + // This makes it a subtype of every constant array within those bounds, + // while preserving meet/join symmetry with ordinary integer ranges. + private Type emptyMeet(Type t) { + Type bound=emptyElem(); + if( t.isa(bound) && (_any || t!=bound) ) return this; + return bound.meet(t); } - public abstract long at8(int idx); - public abstract int len(); - @Override public abstract int log_size(); - public void write( BAOS baos ) { throw Utils.TODO("Should not reach here: abstract constant array cannot be written"); } + public TypeInteger storageType() { return _storage; } + public long at8(int idx) { return _ary[idx]; } + public int len() { return _ary.length; } + @Override public int log_size() { return _storage.log_size(); } + public void write( BAOS baos ) { + for( int i=0; i<len(); i++ ) { + long value=at8(i); + for( int b=0; b<(1<<log_size()); b++ ) baos.write((byte)(value >>> (8*b))); + } + } + + /** Canonical contents, independent of the node which discovered them. */ + public static TypeConAry make(TypeInteger storage, long[] values) { + long[] copy=values.clone(); + int shift=64-(8<<storage.log_size()); + for( int i=0; i<copy.length; i++ ) + copy[i]=storage._min<0 ? (copy[i]<<shift)>>shift : copy[i]&storage._max; + return new TypeConAry(false,(byte)0,storage,copy).intern(); + } + public static TypeConAry make(byte[] bytes) { + long[] values=new long[bytes.length]; + for( int i=0; i<bytes.length; i++ ) values[i]=bytes[i]&255; + return new TypeConAry(false,(byte)0,TypeInteger.U8,values).intern(); + } + public static TypeConAry make(String s) { return make(s.getBytes()); } + + // Keep the existing byte-array tag and encoding. An impossible Java array + // length escapes to a typed payload, without renumbering later type tags. + private static final int TYPED = -1; + private static final TypeInteger[] STORAGE={TypeInteger.BOOL,TypeInteger.I8,TypeInteger.U8, + TypeInteger.I16,TypeInteger.U16,TypeInteger.I32,TypeInteger.U32,TypeInteger.BOT}; // Reserve tags for u8 array @Override int TAGOFF() { return 1; } @Override public void packed( BAOS baos, HashMap<String,Integer> strs ) { - assert log_size()==0 && !_any; baos.write(TAGOFFS[TCONARY]+0); + if( storageType()!=TypeInteger.U8 || _any || _widen!=0 ) { + baos.packed4(TYPED); + int kind=0; + while( kind<STORAGE.length && STORAGE[kind]!=storageType() ) kind++; + assert kind<STORAGE.length; + baos.packed1(kind); + baos.packed1((_any ? 4 : 0)|_widen); + } baos.packed4(len()); - baos.write((byte[])_ary); + write(baos); } static TypeConAry packed( int tag, BAOS bais ) { - return TypeConAryB.make(bais.read(new byte[bais.packed4()])); + int len=bais.packed4(); + if( len!=TYPED ) return make(bais.read(new byte[len])); + TypeInteger storage=STORAGE[bais.packed1()]; + int flags=bais.packed1(); + long[] values=new long[bais.packed4()]; + int size=1<<storage.log_size(); + for( int i=0; i<values.length; i++ ) { + long value=0; + for( int b=0; b<size; b++ ) value|=(long)bais.read() << (8*b); + if( storage._min<0 && size<8 ) value=(value << (64-size*8)) >> (64-size*8); + values[i]=value; + } + TypeConAry ary=make(storage,values); + return flags==0 ? ary : ary._make((flags&4)!=0,(byte)(flags&3)).intern(); } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java b/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java deleted file mode 100644 index 84be5fb2..00000000 --- a/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.seaofnodes.simple.type; - -import com.seaofnodes.simple.util.BAOS; -import java.util.Arrays; - -public class TypeConAryB extends TypeConAry<byte[]> { - private TypeConAryB(boolean any, byte widen, byte[] bs) { super(any,widen,bs); } - @Override TypeConAryB _make( boolean any, byte widen ) { return new TypeConAryB(any,widen,_ary); } - public static TypeConAryB make( byte[] bs ) { return new TypeConAryB(false,(byte)0,bs).intern(); } - public static TypeConAryB make( String s ) { return make(s.getBytes()); } - static final TypeConAryB ABC = make("abc"); - static final TypeConAryB ABCD = make("abcd"); - @Override public long at8(int idx) { return _ary[idx]&255; } - @Override public int len() { return _ary.length; } - @Override public int log_size() { return 0; } - @Override public String str() { return "[\""+new String(_ary)+"\"]"; } - @Override boolean eq(Type t) { return t instanceof TypeConAryB ary && _widen==ary._widen && Arrays.equals(_ary,ary._ary); } - @Override int hash() { return Arrays.hashCode(_ary); } - @Override public void write( BAOS baos ) { baos.write(_ary); } -} diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java b/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java deleted file mode 100644 index b1b061de..00000000 --- a/src/main/java/com/seaofnodes/simple/type/TypeConAryI.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.seaofnodes.simple.type; - -import java.util.Arrays; - -public class TypeConAryI extends TypeConAry<int[]> { - private TypeConAryI(boolean any, byte widen, int[] bs) { super(any,widen,bs); } - public static TypeConAryI make( int[] bs ) { return new TypeConAryI(false,(byte)0,bs).intern(); } - @Override TypeConAryI _make( boolean any, byte widen ) { return new TypeConAryI(any,widen,_ary); } - static final TypeConAryI I123 = make(new int[]{1,2,3}); - @Override public long at8(int idx) { return _ary[idx]; } - @Override public int len() { return _ary.length; } - @Override public int log_size() { return 2; } - @Override public String str() { return "[i32]"; } - @Override boolean eq(Type t) { return t instanceof TypeConAryI ary && _widen==ary._widen && Arrays.equals(_ary,ary._ary); } - @Override int hash() { return Arrays.hashCode(_ary); } -} diff --git a/src/main/java/com/seaofnodes/simple/type/TypeFloat.java b/src/main/java/com/seaofnodes/simple/type/TypeFloat.java index d410e9cf..8d04fcbf 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeFloat.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeFloat.java @@ -107,7 +107,7 @@ public class TypeFloat extends TypeScalar { else { assert isConstant(); baos.write(TAGOFFS[_type] + 2); - baos.packed8(Double.doubleToLongBits(_con)); + baos.packed8(Double.doubleToRawLongBits(_con)); } } @@ -121,12 +121,12 @@ public class TypeFloat extends TypeScalar { } @Override - int hash() { return (int)(Double.hashCode(_con) ^ _sz ^ (1<<17)); } + int hash() { return Long.hashCode(Double.doubleToRawLongBits(_con)) ^ _sz ^ (1<<17); } @Override public boolean eq( Type t ) { TypeFloat i = (TypeFloat)t; // Contract - // Allow NaN to check for equality - return _sz==i._sz && Double.doubleToLongBits(_con)==Double.doubleToLongBits(i._con); + // Representation conversions observe NaN signs/payloads and signed zero. + return _sz==i._sz && Double.doubleToRawLongBits(_con)==Double.doubleToRawLongBits(i._con); } } 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/java/com/seaofnodes/simple/type/TypeStruct.java b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java index 7c9758bd..11ba3cc4 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java @@ -146,8 +146,7 @@ public class TypeStruct extends Type { Type len = TypeInteger.constant(con.len()); return make(name,false, Field.make("#" ,len , lenAlias,true), - Field.make("[]" ,con.elem(),bodyAlias,true), - Field.make("[con]",con ,bodyAlias,true)); + Field.make("[]" ,con ,bodyAlias,true)); } public TypeStruct makeInit() { @@ -190,6 +189,9 @@ public class TypeStruct extends Type { } @Override TypeStruct _close( String name, HashMap<String, Type> TYPES ) { assert (name==null) != (TYPES==null); // One or the other + // Primitive constant contents are already closed. Looking up their + // shared array name would discard the length and payload on import. + if( isConAry() ) return this; String namePlusFinal = isAry() ? (_name+(_fields[1]._final ? "~" : "!")).intern() : _name; TypeStruct ts = (TypeStruct)VISIT.get(namePlusFinal); if( ts!=null ) return ts; @@ -251,7 +253,7 @@ public class TypeStruct extends Type { public static final TypeStruct TEST= make("test",false,Field.TEST); private static final TypeStruct ARY = makeAry("[]i64",TypeInteger.U32,-1,TypeInteger.BOT,-2,false); private static final TypeStruct STR = makeAry("[]u8" ,TypeInteger.U32,-1,TypeInteger.U8 ,-4,false); - private static final TypeStruct ABC = makeAry("[]u8",-1,-4,TypeConAryB.ABC); + private static final TypeStruct ABC = makeAry("[]u8",-1,-4,TypeConAry.ABC); // Representative unresolved forms for the exhaustive lattice tests: // bare forward ref, forward ref constrained by a field use, and definition. private static final TypeStruct FREF0 = forward("%FREF"); @@ -462,6 +464,7 @@ public class TypeStruct extends Type { @Override public int log_size() { throw Utils.TODO("Should not reach here: structs have no scalar log size"); } @Override public int size() { return offset(_fields.length); } @Override public int alignment() { + if( isAry() ) return Math.max(2,aryScale()); // The length is always u32. int align = 0; for( Field f : _fields ) if( !f._extern ) align = Math.max(align, f._t.alignment()); @@ -607,7 +610,7 @@ public class TypeStruct extends Type { int align = _fields[1]._t.alignment(); _offs[1] = (4 + ((1<<align)-1)) & -(1<<align); if( _fields[1]._t instanceof TypeConAry ary ) // Array length is well-defined for constants, not defined for runtime arrays - _offs[2] = _offs[1]+ary.len(); + _offs[2] = _offs[1]+(ary.len()<<ary.log_size()); } else { int[] cnts = new int[5]; // Count of fields at log field size diff --git a/src/main/smp/sys.smp b/src/main/smp/sys.smp index 1201315b..3fb8d94b 100644 --- a/src/main/smp/sys.smp +++ b/src/main/smp/sys.smp @@ -2,12 +2,18 @@ // 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 integer = integer; // Integer bounds +val math = math; // Floating-point representation primitives 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/adt/bitset.smp b/src/main/smp/sys/adt/bitset.smp index d02cd2fe..a51a63e1 100644 --- a/src/main/smp/sys/adt/bitset.smp +++ b/src/main/smp/sys/adt/bitset.smp @@ -20,7 +20,7 @@ struct bitset { if( x < len ) return self; while( x >= len ) len *= 2; var bits = new i64[len]; - for( int i=0; i<_bits#; i++ ) + for( int i = 0; i < _bits#; i++ ) bits[i] = _bits[i]; _bits = bits; return self; @@ -51,7 +51,7 @@ struct bitset { }; val clear = { -> - for( int i=0; i<_bits#; i++ ) _bits[i] = 0; + for( int i = 0; i < _bits#; i++ ) _bits[i] = 0; return self; }; }; diff --git a/src/main/smp/sys/ary.smp b/src/main/smp/sys/ary.smp index c139cd33..b94ffa8f 100644 --- a/src/main/smp/sys/ary.smp +++ b/src/main/smp/sys/ary.smp @@ -3,12 +3,12 @@ // Geometric growth without wrapping a u32 length. Zero means too large. val capacity = { u32 current, i64 needed -> - if( needed < 0 || needed > 4294967295 ) return 0; + if( needed < 0 || needed > sys.integer.MAX_U32 ) return 0; if( needed <= current ) return current; i64 size = current ? current : 1; while( size < needed ) { size *= 2; - if( size > 4294967295 ) size = 4294967295; + if( size > sys.integer.MAX_U32 ) size = sys.integer.MAX_U32; } u32 result = size; return result; @@ -19,7 +19,7 @@ val copy = { u8[] dst, u32 doff, u8[~] src, u32 soff, u32 len -> if( doff > dst# || soff > src# ) return dst; if( len > dst# - doff ) len = dst# - doff; if( len > src# - soff ) len = src# - soff; - for( u32 i=0; i<len; i++ ) + for( u32 i = 0; i < len; i++ ) dst[i+doff] = src[i+soff]; return dst; }; @@ -33,7 +33,7 @@ val cmp = { u8[~] src1, u32 off1, u8[~] src2, u32 off2, u32 len -> u32 len1 = len > left1 ? left1 : len; u32 len2 = len > left2 ? left2 : len; u32 max = len1 > len2 ? len2 : len1; - for( u32 i=0; i<max; i++ ) + for( u32 i = 0; i < max; i++ ) if( src1[off1+i] != src2[off2+i] ) return src1[off1+i] - src2[off2+i]; // Bytes determine compare return len1 - len2; // Length determines compare diff --git a/src/main/smp/sys/aryi64.smp b/src/main/smp/sys/aryi64.smp index b9f1fbed..aba1b459 100644 --- a/src/main/smp/sys/aryi64.smp +++ b/src/main/smp/sys/aryi64.smp @@ -14,7 +14,7 @@ struct aryi64 { u32 newlen = ary.capacity(buf#,len+len2); if( !newlen ) libc._exit(1); // Length exceeds the array representation. i64[] !buf2 = new i64[newlen]; - for( u32 i=0; i<len; i++ ) + for( u32 i = 0; i < len; i++ ) buf2[i] = buf[i]; buf = buf2; self; @@ -32,7 +32,7 @@ struct aryi64 { val str = { -> // TODO: clunky default constructor syntax var str = new aryu8().add('['); - for( u32 i=0; i<len; i++ ) + for( u32 i = 0; i < len; i++ ) str.num(buf[i]).add(',').add(' '); if( len ) str.undo(2); return str.add(']'); @@ -79,7 +79,7 @@ val copy = { i64[] dst, u32 doff, i64[~] src, u32 soff, u32 len -> if( doff > dst# || soff > src# ) return dst; if( len > dst# - doff ) len = dst# - doff; if( len > src# - soff ) len = src# - soff; - for( u32 i=0; i<len; i++ ) + for( u32 i = 0; i < len; i++ ) dst[i+doff] = src[i+soff]; return dst; }; diff --git a/src/main/smp/sys/aryu8.smp b/src/main/smp/sys/aryu8.smp index 18543f13..3f462898 100644 --- a/src/main/smp/sys/aryu8.smp +++ b/src/main/smp/sys/aryu8.smp @@ -27,7 +27,7 @@ struct aryu8 { // Add all of a plain array to the aryu8 val addAry = { u8[~] xs -> - u32 n=xs#; + u32 n = xs#; _grow(n); ary.copy(buf,len,xs,0,n); len += n; @@ -60,7 +60,7 @@ val atoi = { u8[~] str -> while( x < str# && str[x] <= ' ' ) x++; if( x == str# ) return 0; bool neg = false; - if( str[x]=='-' ) { neg=true; x++; } + if( str[x] == '-' ) { neg = true; x++; } i64 n = 0; while( x < str# && '0' <= str[x] <= '9' ) n = n*10 + str[x++]-'0'; @@ -69,10 +69,7 @@ val atoi = { u8[~] str -> // Zero-terminate a string for libc, reserving one array element for the zero. { u8[~] -> u8[] } str0 = { u8[~] str -> - if( str# == 4294967295 ) libc._exit(1); - u8[] !strz = new u8[str#+1]; - u32 len=str#; - ary.copy(strz,0,str,0,len); - strz[str#] = '\0'; - strz; + if( str# == sys.integer.MAX_U32 ) libc._exit(1); + // Copy, terminate and return + return new u8[str#+1]{i64 i -> i < str# ? str[i] : '\0'; }; }; 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..2a8b6117 --- /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 = 0xff0000; +u32 GREEN = 0x00ff00; +u32 BLUE = 0x0000ff; + +// 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..c90ed2d3 --- /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..9ba3514a --- /dev/null +++ b/src/main/smp/sys/draw/Canvas.smp @@ -0,0 +1,149 @@ +struct Canvas { + u32 width; + u32 height; + u32[] pixels; + + new Canvas = { u32 w, u32 h -> + i64 count = w; + count *= h; + if( w <= 0 || h <= 0 || count > sys.integer.MAX_I32 ) 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; + }; +}; + +// An old-school 3x5 font, ASCII 32..127. Five octal row digits need u16. +// Lowercase uses uppercase shapes; unsupported characters use '?'. +val _glyphs = new u16[~96]{ + 000000,022202,055000,057575,061202,051245,061202,022000, // space..' + 012221,042224,005250,002720,000024,000700,000002,011244, // (../ + 075557,026227,061247,061216,055711,074616,034757,071222, // 0..7 + 075757,075716,002020,002024,012421,007070,042124,061202, // 8..? + 061202,025755,065656,034443,065556,074647,074644,034553, // @..G + 055755,072227,011152,055655,044447,057755,057775,025552, // H..O + 065644,025573,065655,034216,072222,055557,055552,055775, // P..W + 055255,055222,071247,032223,044211,062226,025000,000007, // X.._ + 042000,025755,065656,034443,065556,074647,074644,034553, // `..g + 055755,072227,011152,055655,044447,057755,057775,025552, // h..o + 065644,025573,065655,034216,072222,055557,055552,055775, // p..w + 055255,055222,071247,032423,022222,062126,003600,061202, // x..DEL +}; + +val _glyph = { u8 c -> + return 32 <= c < 128 ? _glyphs[c-32] : 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..ae203bd1 --- /dev/null +++ b/src/main/smp/sys/gfx.smp @@ -0,0 +1,44 @@ +// 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_ms -> simple_gfx_wait(timeout_ms); }; diff --git a/src/main/smp/sys/integer.smp b/src/main/smp/sys/integer.smp new file mode 100644 index 00000000..a839e597 --- /dev/null +++ b/src/main/smp/sys/integer.smp @@ -0,0 +1,6 @@ +// Integer bounds. + +i64 MIN_I64 = 1<<63; +i64 MAX_I64 = -(MIN_I64+1); +i32 MAX_I32 = (1<<31)-1; +u32 MAX_U32 = (1<<32)-1; diff --git a/src/main/smp/sys/io.smp b/src/main/smp/sys/io.smp index 14993429..6269cde5 100644 --- a/src/main/smp/sys/io.smp +++ b/src/main/smp/sys/io.smp @@ -21,7 +21,7 @@ val read = { i32 fd -> return null; } if( rez == 0 ) break; - if( buf.len+rez > 4294967295 ) { + if( buf.len+rez > sys.integer.MAX_U32 ) { libc.set_errno(libc.EOVERFLOW); return null; } diff --git a/src/main/smp/sys/libc.smp b/src/main/smp/sys/libc.smp index 7122bbd8..8e417461 100644 --- a/src/main/smp/sys/libc.smp +++ b/src/main/smp/sys/libc.smp @@ -53,9 +53,9 @@ i32 EOVERFLOW = "C"; // addr len prot flags fd off -> void* { i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C"; // mmap flags -i32 PROT_EXEC = 4; -i32 PROT_READ = 1; -i32 PROT_WRITE= 2; -i32 PROT_NONE = 0; +i32 PROT_EXEC = 4; +i32 PROT_READ = 1; +i32 PROT_WRITE = 2; +i32 PROT_NONE = 0; i32 MAP_PRIVATE = 2; i32 MAP_ANON = 32; diff --git a/src/main/smp/sys/math.smp b/src/main/smp/sys/math.smp new file mode 100644 index 00000000..a5e1887c --- /dev/null +++ b/src/main/smp/sys/math.smp @@ -0,0 +1,15 @@ +// Mathematical constants, rounded to the nearest f64 value. +val PI = 3.141592653589793; +val E = 2.718281828459045; + +// Fixed-semantics C bindings recognized by the compiler's intrinsic inliner. +// These preserve all 64 bits, including signed zero and NaN payloads. +// They are NOT numeric conversions (ordinary typed assignments still are). +{ f64 -> i64 } simple_f64_bits = "C"; +{ i64 -> f64 } simple_f64_from_bits = "C"; +val f64Bits = simple_f64_bits; +val f64FromBits = simple_f64_from_bits; + + +val max = { f64 x, f64 y -> return x > y ? x : y; }; +val min = { f64 x, f64 y -> return x < y ? x : y; }; diff --git a/src/main/smp/sys/rt/simple_args.smp b/src/main/smp/sys/rt/simple_args.smp index 3f630e54..8bfcc1a7 100644 --- a/src/main/smp/sys/rt/simple_args.smp +++ b/src/main/smp/sys/rt/simple_args.smp @@ -1,18 +1,20 @@ // Native startup support, compiled independently and linked with the CRT. // Raw native addresses stay i64; only the arrays allocated here are Simple objects. +// This startup object is built without sys imports. +u32 _MAX_U32 = (1<<32)-1; { i64 i64 -> i64 } simple_argv = "C"; { i64 -> i64 } strlen = "C"; { i64 i64 i64 -> i64 } memcpy = "C"; val simple_args = { i64 argc, i64 argv -> - if( argc < 0 || argc > 4294967295 ) return null; + if( argc < 0 || argc > _MAX_U32 ) return null; // Zero-filled reference arrays require nullable elements. Every slot is // filled before returning; the native entry bridge publishes u8[~][]. val args = new u8[~]?[argc]; - for( i64 i=0; i<argc; i++ ) { + for( i64 i = 0; i < argc; i++ ) { i64 src = simple_argv(argv,i); i64 len = strlen(src); - if( len < 0 || len > 4294967295 ) return null; + if( len < 0 || len > _MAX_U32 ) return null; val bytes = new u8[len]; i64 dst = bytes; memcpy(dst,src,len); diff --git a/src/main/smp/sys/text.smp b/src/main/smp/sys/text.smp index a414ff45..43698f22 100644 --- a/src/main/smp/sys/text.smp +++ b/src/main/smp/sys/text.smp @@ -5,78 +5,75 @@ val begin = { -> new aryu8(); }; val end = { aryu8 b -> - val bytes = new u8[b.len]; - ary.copy(bytes,0,b.buf,0,b.len); - u8[~] result = bytes; - result; + new u8[~b.len]{ i64 i -> b.buf[i]; }; }; val _pad = { !aryu8 b, i64 n, u8 c -> - for( i64 i=0; i<n; i++ ) b.add(c); + for( i64 i = 0; i < n; i++ ) b.add(c); b; }; val string = { !aryu8 b, u8[~] s, i64 spec, i64 width, i64 precision -> - i64 len=s#; - if( precision>=0 && precision<len ) len=precision; - i64 pad=width-len; + i64 len = s#; + if( precision >= 0 && precision < len ) len = precision; + i64 pad = width-len; if( !(spec&256) ) _pad(b,pad,' '); - for( i64 i=0; i<len; i++ ) b.add(s[i]); + for( i64 i = 0; i < len; i++ ) b.add(s[i]); if( spec&256 ) _pad(b,pad,' '); b; }; val integer = { !aryu8 b, i64 value, i64 spec, i64 width, i64 precision -> - i64 kind=spec&255; - if( kind=='b' ) return string(b,value ? "true" : "false",spec,width,-1); - if( kind=='c' ) { + i64 kind = spec&255; + if( kind == 'b' ) return string(b,value ? "true" : "false",spec,width,-1); + if( kind == 'c' ) { if( !(spec&256) ) _pad(b,width-1,' '); - u8 c=value; + u8 c = value; b.add(c); if( spec&256 ) _pad(b,width-1,' '); return b; } - bool signed=kind==0 || kind=='d' || kind=='i'; - i64 base=kind=='o' ? 8 : (kind=='x' || kind=='X' ? 16 : 10); - u8[~] alphabet=kind=='X' ? "0123456789ABCDEF" : "0123456789abcdef"; - val digits=new u8[22]; // Maximum: 22 octal digits for a 64-bit integer. - i64 count=0, n=value; + bool signed = kind == 0 || kind == 'd' || kind == 'i'; + i64 base = kind == 'o' ? 8 : (kind == 'x' || kind == 'X' ? 16 : 10); + u8[~] alphabet = kind == 'X' ? "0123456789ABCDEF" : "0123456789abcdef"; + val digits = new u8[22]; // Maximum: 22 octal digits for a 64-bit integer. + i64 count = 0, n = value; // Signed decimal stays nonpositive, including the minimum i64. - if( signed && n>0 ) n=-n; - if( n==0 && precision!=0 ) digits[count++]='0'; - while( n!=0 ) { - i64 q=0, r=0; - if( signed ) { q=n/base; r=q*base-n; } + if( signed && n > 0 ) n = -n; + if( n == 0 && precision != 0 ) digits[count++] = '0'; + while( n != 0 ) { + i64 q = 0, r = 0; + if( signed ) { q = n/base; r = q*base-n; } else { // Unsigned division without a u64 type. Halving first makes // the dividend nonnegative; the remainder needs at most one fix. - q=((n>>>1)/base)*2; - r=n-q*base; - if( r>=base ) { q++; r-=base; } + q = ((n>>>1)/base)*2; + r = n-q*base; + if( r >= base ) { q++; r -= base; } } - digits[count++]=alphabet[r]; - n=q; + digits[count++] = alphabet[r]; + n = q; } - u8[~] !prefix=""; + u8[~] !prefix = ""; if( signed ) { - if( value<0 ) prefix="-"; - else if( spec&512 ) prefix="+"; - else if( spec&1024 ) prefix=" "; - } else if( (spec&2048) && value!=0 ) { - if( kind=='x' ) prefix="0x"; - if( kind=='X' ) prefix="0X"; + if( value < 0 ) prefix = "-"; + else if( spec&512 ) prefix = "+"; + else if( spec&1024 ) prefix = " "; + } else if( (spec&2048) && value != 0 ) { + if( kind == 'x' ) prefix = "0x"; + if( kind == 'X' ) prefix = "0X"; } - i64 zeros=precision>count ? precision-count : 0; + i64 zeros = precision > count ? precision-count : 0; // Alternate octal always begins with a zero, including precision-zero 0. - if( kind=='o' && (spec&2048) && zeros==0 && (count==0 || digits[count-1]!='0') ) zeros=1; - i64 pad=width-prefix#-zeros-count; - bool left=(spec&256)!=0; - bool zero=(spec&4096)!=0 && precision<0 && !left; + if( kind == 'o' && (spec&2048) && zeros == 0 && (count == 0 || digits[count-1] != '0') ) zeros = 1; + i64 pad = width-prefix#-zeros-count; + bool left = (spec&256) != 0; + bool zero = (spec&4096) != 0 && precision < 0 && !left; if( !left && !zero ) _pad(b,pad,' '); b.addAry(prefix); if( zero ) _pad(b,pad,'0'); _pad(b,zeros,'0'); - while( count>0 ) b.add(digits[--count]); + while( count > 0 ) b.add(digits[--count]); if( left ) _pad(b,pad,' '); b; }; @@ -96,3 +93,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..a3e9b3a2 --- /dev/null +++ b/src/main/smp/sys/time.smp @@ -0,0 +1,219 @@ +// Time utilities; get the current time (both nanos and millis), sleep, time zones. + +// 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; +i64 MINUTE = 60*SECOND; +i64 HOUR = 60*MINUTE; +i64 DAY = 24*HOUR; +i64 NANOS_PER_MS = 1000*1000; + +// Gregorian leap days: every fourth year, except centuries not divisible by 400. +i64 _DAYS_PER_4_YEARS = 4*365+1; +i64 _DAYS_PER_CENTURY = 100*365+100/4-1; +i64 _DAYS_PER_ERA = 4*_DAYS_PER_CENTURY+1; +// Complete years before the Unix epoch and after the last supported date. +i64 _DAYS_BEFORE_EPOCH = 1969*365+1969/4-1969/100+1969/400; +i64 _DAYS_BEFORE_YEAR_10000 = 9999*365+9999/4-9999/100+9999/400; +// The March-based calendar algorithm starts at 0000-03-01, 306 days before 0001-01-01. +i64 _MARCH_EPOCH_DAYS = _DAYS_BEFORE_EPOCH+306; +i64 MIN = -_DAYS_BEFORE_EPOCH*DAY; // 0001-01-01T00:00:00.000Z +i64 MAX = (_DAYS_BEFORE_YEAR_10000-_DAYS_BEFORE_EPOCH)*DAY-1; // 9999-12-31T23:59:59.999Z + +// Access to the underlying LIBC functions +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 duration_ms -> simple_time_sleep(duration_ms,0); }; +val sleepUntil = { i64 deadline_ms -> simple_time_sleep(deadline_ms,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 > sys.integer.MAX_I64/unit || n < sys.integer.MIN_I64/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/NANOS_PER_MS; }; +val toNanos = { i64 ms -> _scale(ms,NANOS_PER_MS); }; + +// Clamp to zero once due, and saturate for widely separated i64 readings. +val remaining = { i64 deadline_ms, i64 current_ms -> + if( deadline_ms <= current_ms ) return 0; + if( current_ms < 0 && deadline_ms > sys.integer.MAX_I64+current_ms ) return sys.integer.MAX_I64; + return deadline_ms-current_ms; +}; + +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*_DAYS_PER_ERA+y*365+y/4-y/100+(153*mp+2)/5+day-1-_MARCH_EPOCH_DAYS; +}; + +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+_MARCH_EPOCH_DAYS, era = z/_DAYS_PER_ERA, doe = z-era*_DAYS_PER_ERA; + i64 y = (doe-doe/(_DAYS_PER_4_YEARS-1)+doe/_DAYS_PER_CENTURY-doe/(_DAYS_PER_ERA-1))/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 = { 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(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..53b6688e --- /dev/null +++ b/src/main/smp/sys/time/DateTime.smp @@ -0,0 +1,13 @@ +// 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). + +// All values default zero/false +struct DateTime { + int year, month, day; + int hour, minute, second, millisecond; + int weekday, offsetSeconds; + bool daylight; +}; + +// 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..7cca2e9c --- /dev/null +++ b/src/main/smp/sys/time/Timers.smp @@ -0,0 +1,80 @@ +// Cooperative timers. poll returns IDs; callbacks never run on another thread. +// IDs are u32; 0 means failure or no timer due. Millisecond values use _ms. + +// The + + +struct Timers { + // Collection of timers (as struct-of-arrays) + u32[] !_ids = new u32[8]; // Which timer id for this slot + i64[] !_due_ms = new i64[8]; // Monotonic deadline in milliseconds + i64[] !_period_ms = new i64[8]; // When does it repeat + // Next available timer ID; 0 means all IDs have been used. + u32 !_next = 1; + + // Add a new timer + { i64 i64 i64 -> u32 } _add = { i64 current_ms, i64 delay_ms, i64 period_ms -> + if( current_ms < 0 || delay_ms < 0 || current_ms > sys.integer.MAX_I64-delay_ms || _next == 0 ) return 0; + // Ugly linear scan for an available slot + int slot = 0; + while( slot < _ids# && _ids[slot] != 0 ) slot++; + // Double size for lots of timers + if( slot == _ids# ) { + if( _ids# > sys.integer.MAX_I32/2 ) return 0; + val ids = new u32[_ids#*2]; + val due_ms = new i64[_ids#*2], periods_ms = new i64[_ids#*2]; + for( int i = 0; i < _ids#; i++ ) { ids[i] = _ids[i]; due_ms[i] = _due_ms[i]; periods_ms[i] = _period_ms[i]; } + _ids = ids; _due_ms = due_ms; _period_ms = periods_ms; + // Retry against the newly published arrays after growing. + return _add(current_ms,delay_ms,period_ms); + } + // Next available timer id + u32 id = _next; + _next = id == sys.integer.MAX_U32 ? 0 : id+1; + _ids[slot] = id; _due_ms[slot] = current_ms+delay_ms; _period_ms[slot] = period_ms; + return id; + }; + + // Explicit-clock versions make game logic and tests deterministic. + { i64 i64 -> u32 } afterAt = { i64 current_ms, i64 delay_ms -> _add(current_ms,delay_ms,0); }; + { i64 i64 -> u32 } everyAt = { i64 current_ms, i64 period_ms -> period_ms <= 0 ? 0 : _add(current_ms,period_ms,period_ms); }; + { i64 -> u32 } after = { i64 delay_ms -> afterAt(time.monotonic(),delay_ms ); }; + { i64 -> u32 } every = { i64 period_ms -> everyAt(time.monotonic(),period_ms); }; + + // Remove a timer + val cancel = { u32 id -> + if( id == 0 ) return false; + for( int i = 0; i < _ids#; i++ ) if( _ids[i] == id ) { _ids[i] = 0; return true; } + return false; + }; + + // Earliest absolute monotonic deadline in milliseconds, or -1 when empty. + val nextDeadline = { -> + i64 next_ms = -1; + // Ugly linear scan again, but short + for( int i = 0; i < _ids#; i++ ) + if( _ids[i] != 0 && (next_ms < 0 || _due_ms[i] < next_ms) ) next_ms = _due_ms[i]; + return next_ms; + }; + + // Next firing timer. + + // Earliest deadline first; equal deadlines use registration order. + // Repeats skip missed ticks and retain their phase, avoiding bursts/drift. + { i64 -> u32 } poll = { i64 current_ms -> + int slot = -1; + for( int i = 0; i < _ids#; i++ ) + if( _ids[i] != 0 && _due_ms[i] <= current_ms && (slot < 0 || _due_ms[i] < _due_ms[slot] || + (_due_ms[i] == _due_ms[slot] && _ids[i] < _ids[slot])) ) slot = i; + if( slot < 0 ) return 0; + u32 id = _ids[slot]; + i64 period_ms = _period_ms[slot]; + if( period_ms == 0 ) _ids[slot] = 0; + else { + i64 steps = (current_ms-_due_ms[slot])/period_ms; + if( steps >= (sys.integer.MAX_I64-_due_ms[slot])/period_ms ) _ids[slot] = 0; + else _due_ms[slot] += (steps+1)*period_ms; + } + 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/ArithNodeTest.java b/src/test/java/com/seaofnodes/simple/ArithNodeTest.java index 1e6c6fc0..b29c9706 100644 --- a/src/test/java/com/seaofnodes/simple/ArithNodeTest.java +++ b/src/test/java/com/seaofnodes/simple/ArithNodeTest.java @@ -65,7 +65,7 @@ public class ArithNodeTest { TypePtr.gather(inputs); TypeMemPtr.gather(inputs); TypeFunPtr.gather(inputs); - inputs.add(TypeConAryB.make(new byte[]{1,2,3})); + inputs.add(TypeConAry.make(new byte[]{1,2,3})); for( long value : new long[]{Long.MIN_VALUE,Long.MIN_VALUE+1,-1,1,Long.MAX_VALUE} ) inputs.add(TypeInteger.constant(value)); for( double value : new double[]{-0.0,0.0,-1.0,1.0,16777216.0,16777217.0,Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY} ) diff --git a/src/test/java/com/seaofnodes/simple/ArrayInitTest.java b/src/test/java/com/seaofnodes/simple/ArrayInitTest.java new file mode 100644 index 00000000..d298828e --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/ArrayInitTest.java @@ -0,0 +1,211 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.ArrayInitNode; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import java.util.ArrayList; +import org.junit.Test; +import static org.junit.Assert.*; + +public class ArrayInitTest { + @Test public void constantListComputeIsMonotone() { + CodeGen code = new CodeGen("return 0;").parse(); + TypeStruct layout = TypeStruct.makeAry("[]i32",TypeInteger.U32,2,TypeInteger.I32,3,true); + Type array = TypeMemPtr.make(layout); + Node ctrl = ConstantNode.seed(Type.CONTROL); + Node ptr = ConstantNode.seed(Type.TOP); + Node len = ConstantNode.seed(Type.TOP); + Node x = ConstantNode.seed(Type.TOP), y = ConstantNode.seed(Type.TOP); + ArrayInitNode init = new ArrayInitNode(ctrl,code.ZERO,ptr,len,2,null,new Ary<>(new Node[]{x,y})); + Type[][] inputs = { + {Type.TOP,TypeMemPtr.TOP,array}, + {Type.TOP,TypeInteger.TOP,TypeInteger.TWO,TypeInteger.U32}, + {Type.TOP,TypeInteger.TOP,TypeInteger.constant(5),TypeInteger.BOT}, + {Type.TOP,TypeInteger.TOP,TypeInteger.constant(6),TypeInteger.BOT} + }; + var states = new ArrayList<Type[]>(); + for( Type p : inputs[0] ) + for( Type l : inputs[1] ) + for( Type a : inputs[2] ) + for( Type b : inputs[3] ) { + ptr._type = p; len._type = l; x._type = a; y._type = b; + states.add(new Type[]{p,l,a,b,init.compute()}); + } + for( Type[] before : states ) + for( Type[] after : states ) + if( before[0].isa(after[0]) && before[1].isa(after[1]) && + before[2].isa(after[2]) && before[3].isa(after[3]) ) + assertTrue("Array initializer rose from "+before[4]+" to "+after[4],before[4].isa(after[4])); + } + + private static void value(String src, long expected) { + CodeGen code=new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals(Long.toString(expected),Eval2.eval(code,0,100000)); + } + + private static void bad(String src, String message) { + try { + new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + fail("Expected: "+message); + } catch( com.seaofnodes.simple.parser.Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains(message)); + } + } + + @Test public void lists() { + value("val a=new u16[3]{025755,1,65538}; return a#*100000+a[0]+a[1]+a[2];",311248); + value("val a=new int[]{4,7,9,}; a[1]=2; return a#+a[0]+a[1]+a[2];",18); + value("val a=new int[]{}; return a#;",0); + bad("val a=new int[2]{1}; return a[0];","initializer has 1 elements"); + } + + @Test public void generators() { + value("int n=6; val a=new int[n]{int i -> i*i;}; return a[5]+a#;",31); + value("int base=7; val a=new int[3]{int i -> return base+i;}; return a[2];",9); + value("val a=new int[3]{int i -> if(i==1) return 9; i+1;}; return a[0]+a[1]+a[2];",13); + value("val a=new int[0]{int i -> 1/i;}; return a#;",0); + bad("val a=new int[]{int i -> i;}; return a#;","requires a length"); + bad("val f={-> int x=1; val a=new int[2]{int i -> x++;}; return a[0];}; return f();","captured local"); + } + + @Test public void referencesAndReadonly() { + value("struct S { int x; new S={i64 v -> x=v;}; }; val a=new S[~3]{int i -> new S(i+4);}; return a[2].x;",6); + value("val a=new u8[~]{1,2,3}; return a[1];",2); + value("val names=new u8[~][~2]{\"Alice\",\"Bob\"}; return names[1][0];",66); + value("val a=new int[ ~ ]{4,5}; return a[1];",5); + value("val a=new {i64 -> i64}[]{ {i64 x -> x+1;}, {i64 x -> x*2;} }; return a[1](7);",14); + value("struct S { int x=1; }; val a=new !S[1]{new S}; a[0].x=8; return a[0].x;",8); + bad("struct S { int x=1; }; val a=new !S[~1]{new S}; a[0].x=8; return a[0].x;","final field"); + value("struct S { int x; }; val a=new S?[~2]{u32 i -> null;}; return a[1]==null;",1); + bad("val a=new int[~2]{1,2}; a[0]=3; return a[0];","final field"); + bad("struct S { int x; }; val a=new S[1]{null}; return a#;","declared type"); + } + + @Test public void nativeInitializers() throws Exception { + TestC.runSF(""" + val main = { -> + int n=8; + val a=new u16[~n]{int i -> i*i+1;}; + val b=new u8[]{257,2,3}; + return a[7]==50 && b[0]==1 && a#==8 ? 0 : 1; + }; + ""","arrayInitializers","",-1); + } + + @Test public void nestedBodies() { + value(""" + val f={ -> + int offset=5; + val a=new int[4]{int i -> + int sum=offset; + for(int j=0;j<i;j++) { if(j==1) continue; sum+=j; } + if(i==2) return 20; + return sum; + }; + return a[0]+a[1]+a[2]+a[3]; + }; + return f(); + """,37); + value(""" + val a=new int[][3]{int i -> + new int[2]{int j -> i*10+j;}; + }; + return a[2][1]+a[0][0]; + """,21); + value(""" + val a=new int[2]{int i -> + val twice={i64 x -> return x*2;}; + twice(i); + }; + return a[1]; + """,2); + } + + @Test public void orderAndIdentity() throws Exception { + TestC.runSF(""" + struct Counter { + int calls=0; + val next={->calls++;}; + }; + struct Item { + int x; + new Item={i64 v -> x=v;}; + }; + val build_noInline={u32 n, !Counter c -> + new Item[~n]{u32 i -> new Item(c.next());}; + }; + val main={-> + val c=new Counter; + val empty=build_noInline(0,c); + if(empty#!=0 || c.calls!=0) return 1; + val a=build_noInline(4,c); + if(c.calls!=4 || a[0].x!=0 || a[3].x!=3 || a[0]==a[1]) return 2; + val b=new int[]{c.next(),c.next(),c.next()}; + if(b[0]!=4 || b[2]!=6) return 3; + val d=new int[c.next()]{i64 i -> c.next();}; + if(d#!=7 || d[0]!=8 || d[6]!=14 || c.calls!=15) return 4; + return 0; + }; + ""","arrayInitOrder","",-1); + } + + @Test public void diagnostics() { + bad("val f={u32 n -> new int[n]{1,2};}; return f(arg)#;","length must equal that constant"); + bad("val a=new int[2]{u8 i -> i;}; return a[0];","generator index"); + bad("val a=new int[2]{int return -> 0;}; return a[0];","index name"); + bad("val a=new int[2]{int i -> i++;}; return a[0];","final 'i'"); + bad("val f={->int x=0; return new int[2]{int i -> x=i;};}; return f()#;","captured local"); + bad("val a=new int[2]{int i -> break;}; return a[0];","No active loop"); + bad("val a=new int[-1]{}; return a#;","outside the u32 range"); + bad("val a=new int[1.5]{int i -> i;}; return a#;","integer"); + bad("val a=new int[~]{1,2}; val b=a; b[0]++; return b[0];","final field"); + for( String init : new String[]{"self","int i -> self;"} ) + bad("struct C { int x; new C={-> val a=new C[1]{"+init+"}; x=a#;}; }; return new C().x;", + "fully initialized elements"); + } + + @Test public void forwardReferencesAndLoopScopes() { + value(""" + val f={ -> + int sum=0; + for(int k=0;k<3;k++) { + val a=new int[2]{int i -> + if(i) return later(i); + later(3); + }; + if(k==1) continue; + sum+=a[0]+a[1]; + } + return sum; + }; + val later={i64 i -> i*2;}; + return f(); + """,16); + } + + @Test public void optimizationOrder() { + String src=""" + struct Box { int x; new Box={i64 v -> x=v;}; }; + val f={u32 n -> + int base=7; + val a=new Box[~n]{u32 i -> + int x=base; + for(int j=0;j<i;j++) x+=j; + if(i==2) return new Box(20); + new Box(x); + }; + return n==0 ? 0 : a[n-1].x; + }; + return f(arg&7); + """; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck); + assertEquals("0",Eval2.eval(code,0)); + assertEquals("20",Eval2.eval(code,3)); + assertEquals("13",Eval2.eval(code,5)); + } + } +} diff --git a/src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java b/src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java new file mode 100644 index 00000000..0e4c230a --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/AssemblyDumpTest.java @@ -0,0 +1,61 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.BAOS; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HexFormat; +import org.junit.Test; +import static org.junit.Assert.*; + +public class AssemblyDumpTest { + @Test public void libraryUnitsAndGlyphs() throws Exception { + Path root=Path.of("build/objs/assembly-dump"); + Files.createDirectories(root); + Path build=Files.createTempDirectory(root,"sys-"); + CodeGen code=new CodeGen("src/main/smp",build.toString(),null,"sys",null,123,true,TypeInteger.BOT) + .driver(CodeGen.Phase.Serialize,TestC.CPU_PORT,TestC.CALL_CONVENTION) + .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION); + var entry=code._encoding; + String before=code.asm(); + assertSame("Dumping another unit must not change the active encoding",entry,code._encoding); + for( var unit : code._compunits.values() ) + if( unit._src!=null ) { + String heading="=== Compilation unit: "+unit._cname+" ==="; + assertEquals("Exactly one heading for "+unit._cname,2,before.split(heading,-1).length); + } + int start=before.indexOf("=== Compilation unit: sys.draw.Canvas ==="); + assertTrue(start>=0); + int end=before.indexOf("=== Compilation unit:",start+1); + String canvas=before.substring(start,end<0 ? before.length() : end); + assertTrue(canvas.contains("Constant Pool")); + assertTrue(canvas.contains("sys.draw.Canvas._glyphs: []u16")); + TypeMemPtr ptr=(TypeMemPtr)code._compunits.get("sys/draw/Canvas")._clz.field("_glyphs")._t; + TypeConAry glyphs=(TypeConAry)ptr._obj.field("[]")._t; + assertEquals(96,glyphs.len()); + BAOS bytes=new BAOS(); + glyphs.write(bytes); + assertTrue("The complete font follows its u32 length header", + canvas.contains("60000000"+HexFormat.of().formatHex(bytes.toByteArray()))); + code.exportELF(false,false); + com.seaofnodes.simple.codegen.PrintRegTestSupport.checkDataAliases( + build.resolve("sys/draw/Canvas.o"),196,"sys.draw.Canvas._glyphs"); + assertSame(entry,code._encoding); + assertEquals("Export must not truncate the dump to its entry unit",before,code.asm()); + Files.writeString(build.resolve("sys.asm"),before); + } + + @Test public void combinedEncodingAndEarlierPhase() { + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) { + CodeGen code=new CodeGen("val a=new u16[~]{5,6,7}; return a[arg&1];") + .driver(CodeGen.Phase.RegAlloc,cpu,"SystemV"); + assertFalse(code.asm().contains("Constant Pool")); + code.driver(cpu,"SystemV",true,false); + String dump=code.asm(); + assertFalse(dump.contains("=== Compilation unit:")); + assertTrue(dump.contains("Constant Pool")); + assertTrue(dump.contains("03000000050006000700")); + } + } +} 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/BreakoutTest.java b/src/test/java/com/seaofnodes/simple/BreakoutTest.java new file mode 100644 index 00000000..7f3e56d4 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/BreakoutTest.java @@ -0,0 +1,216 @@ +package com.seaofnodes.simple; + +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import javax.imageio.ImageIO; +import org.junit.Test; +import static org.junit.Assert.*; + +// Exercise the actual Simple game and rasterizer in native code, without a window. +public class BreakoutTest { + private static String game() throws Exception { + String source = Files.readString(Path.of("docs/examples/Breakout.smp")); + // Physics/rendering tests supply their own entry point. Input and + // sound are exercised by GraphicsTest with the complete program. + int end = source.indexOf("// Short, fading triangle waves"); + assertTrue("Breakout physics/rendering section",end >= 0); + String physics = source.substring(0,end); + // The shared square-root helper now follows the input code. + int sqrt = source.indexOf("val _sqrt ="); + if( sqrt >= end ) { + int close = source.indexOf("\n};",sqrt); + assertTrue("Square-root helper end",close >= 0); + physics += source.substring(sqrt,close+4); + } + return physics; + } + + @Test public void firstServeMovesBall() throws Exception { + TestC.runSYS(game()+""" + val main = { -> + val g = new Game; g._reset(); + if( g.speed != 320.0 || g.paddle != 504.0 ) return 1; + g._serve(); + f64 x = g.x, y = g.y; + g._tick(0); + if( g.phase != PLAY || g.x <= x || g.y >= y ) return 2; + g._paddle(2000.0); + if( g.paddle != RIGHT-PADDLE_HALF ) return 3; + return 0; + }; + ""","breakoutServe","",-1); + } + + @Test public void paddleLivesAndPause() throws Exception { + TestC.runSYS(game()+""" + val main={ -> + val g=new Game; g._reset(); + if( g.remaining!=192 || g.score!=0 || g.lives!=3 || g.phase!=READY ) return 1; + g._tick(1); + if( g.paddle<510.39 || g.paddle>510.41 || g.x!=g.paddle ) return 2; + g._paddle(-100.0); if( g.paddle != 80.0 ) return 3; + g._paddle(2000.0); if( g.paddle != 928.0 ) return 4; + g._reset(); + g.x=g.paddle-50.0; g._rebound(); + if( g.dx > -0.78 || g.dy>=0.0 ) return 5; + g.x=g.paddle+50.0; g._rebound(); + if( g.dx < 0.78 || g.dy>=0.0 ) return 6; + g.x=g.paddle; g._rebound(); + if( g.dy > -0.99 || g.dx*g.dx+g.dy*g.dy<0.999999 ) return 7; + g._serve(); g.paused=true; + f64 x=g.x, y=g.y, paddle=g.paddle; + g._tick(1); + if( g.x!=x || g.y!=y || g.paddle!=paddle ) return 8; + g.paused=false; + for( int i=0; i<3; i++ ) { + g._serve(); g.x=40.0; g.y=723.0; g.dx=0.0; g.dy=1.0; + g._tick(0); + if( g.lives!=2-i || g.backWall || g.sound!=5 ) return 9; + } + if( g.phase!=LOST ) return 10; + g._serve(); if( g.phase!=LOST ) return 11; + g._reset(); + if( g.phase!=READY || g.lives!=3 || g.boosted || g.paused || g.speed!=320.0 ) return 12; + return 0; + }; + ""","breakoutControls","",-1); + } + + @Test public void collisionsAndBoost() throws Exception { + TestC.runSYS(game()+""" + val main={ -> + val g=new Game; g._reset(); + g._serve(); + g.x=29.1; g.y=400.0; g.dx=-1.0; g.dy=0.0; + g._tick(0); + if( g.dx<=0.0 || g.x<29.0 || g.sound!=1 ) return 1; + g.x=978.9; g.dx=1.0; g._tick(0); + if( g.dx>=0.0 || g.x>979.0 ) return 2; + // Front of the bottom row: one point, no acceleration or boost. + g.x=44.0; g.y=330.0; g.dx=0.0; g.dy=-1.0; + g._tick(0); + if( g.bricks[168]!=0 || g.score!=1 || g.dy<=0.0 || g.boosted || g.soundDepth!=1 ) return 3; + // Top of a brick alone does not arm the back-wall bonus. + g.x=44.0; g.y=146.0; g.dy=1.0; + g._tick(0); + if( g.bricks[0]!=0 || g.score!=9 || g.speed!=565.0 || g.boosted || g.soundDepth!=8 ) return 4; + // Back wall arms it; hitting a side still must not trigger it. + g.x=84.0; g.y=89.1; g.dy=-1.0; + g._tick(0); + if( !g.backWall || g.dy<=0.0 || g.boosted ) return 5; + g.x=60.0; g.y=160.0; g.dx=1.0; g.dy=0.0; + g._tick(0); + if( g.bricks[1]!=0 || g.dx>=0.0 || g.boosted ) return 6; + g.x=124.0; g.y=146.0; g.dx=0.0; g.dy=1.0; + g._tick(0); + if( !g.boosted || g.speed<932.24 || g.speed>932.26 || g.dy>=0.0 || g.sound!=3 || g.soundDepth!=8 ) return 7; + // Another back hit cannot multiply the speed a second time. + g.x=164.0; g.y=146.0; g.dy=1.0; + g._tick(0); + if( g.speed>932.26 || g.bricks[3]!=0 ) return 8; + // A boosted front hit cannot tunnel through the brick either. + g.x=84.0; g.y=331.0; g.dy=-1.0; + g._tick(0); + if( g.bricks[169]!=0 || g.dy<=0.0 || g.speed<932.24 ) return 9; + // Actual paddle contact disarms the trip and chooses its angle. + g.x=g.paddle+50.0; g.y=668.0; g.dy=1.0; g.dx=0.0; + g._tick(0); + if( g.dy>=0.0 || g.dx<0.78 || g.backWall || !g.boosted ) return 10; + // A lost ball clears both kinds of acceleration, preserving the wall/score. + int score=g.score, remaining=g.remaining; + g.x=40.0; g.y=723.0; g.dx=0.0; g.dy=1.0; + g._tick(0); + if( g.phase!=READY || g.lives!=2 || g.speed!=320.0 || g.depth!=1 || g.boosted ) return 11; + if( g.score!=score || g.remaining!=remaining ) return 12; + g._serve(); + if( g.speed!=320.0 ) return 13; + // The new ball can earn its own boost. + g.sound=0; g.backWall=true; + g.x=204.0; g.y=146.0; g.dx=0.0; g.dy=1.0; + g._tick(0); + if( !g.boosted || g.speed<932.24 || g.sound!=3 || g.soundDepth!=8 ) return 14; + return 0; + }; + ""","breakoutCollisions","",-1); + } + + @Test public void gridEdgesAndGaps() throws Exception { + TestC.runSYS(game()+""" + val main = { -> + val g = new Game; g._reset(); + val c = new sys.draw.Canvas(WIDTH,HEIGHT); + g._draw(c); + g._serve(); + // The center lies below the last grid row and between columns. + // Both neighboring bricks overlap the ball; the left one wins. + g.x = 64.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0; + g._tick(0); + if( g.bricks[168] != 0 || g.bricks[169] != 1 || g.score != 1 || g.dy <= 0.0 ) return 1; + // A cleared neighbor must not hide the other candidate. + g.x = 64.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0; + g._tick(0); + if( g.bricks[169] != 0 || g.score != 2 || g.dy <= 0.0 ) return 2; + // Rightmost column, then a position entirely below the wall. + g.x = 978.0; g.y = 330.0; g.dx = 0.0; g.dy = -1.0; + g._tick(0); + if( g.bricks[191] != 0 || g.score != 3 || g.dy <= 0.0 ) return 3; + g.x = 44.0; g.y = 400.0; g.dx = 0.0; g.dy = -1.0; + g._tick(0); + if( g.score != 3 || g.dy >= 0.0 ) return 4; + // Overlapping rows: the first box contains the center but no + // face was crossed; continue to the next row's top face. + g.x = 44.0; g.y = 168.0; g.dx = 0.0; g.dy = 1.0; + g._tick(0); + if( g.bricks[0] != 1 || g.bricks[24] != 0 || g.score != 10 || g.dy >= 0.0 ) return 5; + // Contact just above the first row still belongs to that row. + g.x = 44.0; g.y = 146.0; g.dx = 0.0; g.dy = 1.0; + g._tick(0); + if( g.bricks[0] != 0 || g.score != 18 || g.dy >= 0.0 ) return 6; + return 0; + }; + ""","breakoutGrid","",-1); + } + + @Test public void clearEveryBrickAndRender() throws Exception { + TestC.runSYS(game()+""" + val main={ -> + val g=new Game; g._reset(); + val c=new sys.draw.Canvas(WIDTH,HEIGHT); + g._draw(c); + for( int row=0; row<ROWS; row++ ) + if( c.pixels[(BRICK_TOP+row*BRICK_DY+4)*WIDTH+28]!=COLORS[row] ) return 1; + if( c.pixels[675*WIDTH+504]!=CYAN || c.pixels[663*WIDTH+504]!=WHITE ) return 2; + // Save a native-rendered preview for manual visual review. + int fd=sys.io.create("build/objs/breakout-preview.bgra"); + i64 pixels=c.pixels; + if( fd<0 || sys.libc.write(fd,pixels,c.pixels#*4)!=c.pixels#*4 ) return 3; + sys.io.close(fd); + g._serve(); + // Drive through every brick's front, bottom row first. + for( int i=COLS*ROWS-1; i>=0; i-- ) { + g.x=LEFT+20.0+(i-i/COLS*COLS)*BRICK_DX; + g.y=BRICK_TOP+1.0+(i/COLS)*BRICK_DY+BRICK_H+RADIUS; + g.dx=0.0; g.dy=-1.0; + g._tick(0); + if( g.bricks[i]!=0 ) return 4; + } + if( g.phase!=WON || g.remaining!=0 || g.score!=864 || g.sound!=6 ) return 5; + f64 y=g.y; g._tick(0); + if( g.y!=y ) return 6; + g._reset(); + for( int i=0; i<g.bricks#; i++ ) if( g.bricks[i]!=1 ) return 7; + if( g.score!=0 || g.remaining!=192 ) return 8; + return 0; + }; + ""","breakoutClear","",-1); + byte[] pixels=Files.readAllBytes(Path.of("build/objs/breakout-preview.bgra")); + assertEquals(1008*760*4,pixels.length); + var image=new BufferedImage(1008,760,BufferedImage.TYPE_INT_RGB); + var bytes=ByteBuffer.wrap(pixels).order(ByteOrder.LITTLE_ENDIAN); + for(int y=0;y<760;y++) for(int x=0;x<1008;x++) image.setRGB(x,y,bytes.getInt()); + ImageIO.write(image,"png",Path.of("build/objs/breakout-preview.png").toFile()); + } +} diff --git a/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java b/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java index 31c0302b..fd816c68 100644 --- a/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java +++ b/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java @@ -4,10 +4,35 @@ import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.XInt; import org.junit.Test; import static org.junit.Assert.*; public class BulkMemPhiTest { + @Test public void testUnescapedSliceKeepsItsOwnEscapes() { + CodeGen code = new CodeGen(""); + code._phase = CodeGen.Phase.Lower; + Node defaults = code.con(TypeMem.START); + Node method = code.con(TypeMem.make(55,Type.BOTTOM,false,false,false,XInt.make(4),XInt.EMPTY)); + MemMergeNode fields = new MemMergeNode(null,defaults); + fields.alias(55,method); + fields.init(); + assertSame("The bulk excludes this unescaped field",TypeMem.START,fields._type); + Node ctrl = new CProjNode(code._start,0,"Ctrl").init(); + Node iff = new IfNode(ctrl,code.con(TypeInteger.BOOL)).init(); + RegionNode region = new RegionNode(null,null, + new CProjNode(iff,0,"True").init(), + new CProjNode(iff,1,"False").init()).init(); + BulkMemPhiNode bulk = new BulkMemPhiNode("$mem",region,fields,defaults).init(); + MemMergeNode split = (MemMergeNode)bulk.idealize(); + Node precise = split.alias(55); + assertTrue("A new slice must not inherit an escape bound that excludes its method", + precise.compute().isa(precise._type)); + precise.peephole(); + assertTrue(XInt.bit(((TypeMem)precise._type)._escFs,4)); + } + // Reduced from Capitalize's Lower-phase failure. A precise MemMerge slot // may still be supplied by bulk memory which covers the requested alias. @Test public void testBulkInPreciseSlot() { 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/Chapter17bTest.java b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java index 12e54193..8a66dcc5 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java +++ b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java @@ -568,8 +568,8 @@ struct _A { return new _A(new !B?[0]).array_of_b; """); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("return []*B?;", code.print()); - assertEquals("*B {}?[]", Eval2.eval(code,0)); + assertEquals("return []*Test.B?;", code.print()); + assertEquals("*Test.B {}?[]", Eval2.eval(code,0)); } // --------------------------------------------------------------- diff --git a/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/Chapter22Test.java b/src/test/java/com/seaofnodes/simple/Chapter22Test.java index 9bebee23..c9791de4 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter22Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter22Test.java @@ -332,9 +332,7 @@ return 0; public void testFinalArray() { String src = """ int N=4; -i32[] !is = new i32[N]; -for( int i=0; i<N; i++ ) - is[i] = i*i; +i32[~] !is = new i32[~N]{ int i -> i*i; }; val _sum = { i32[~] is -> // final array int sum=0; for( int i=0; i<is#; i++ ) diff --git a/src/test/java/com/seaofnodes/simple/Chapter23Test.java b/src/test/java/com/seaofnodes/simple/Chapter23Test.java index 89e41efa..8667c0bd 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter23Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter23Test.java @@ -279,7 +279,7 @@ return _s.peek('q'); CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); assertEquals("return Phi(Region,0,1);", code.print()); assertEquals("1", Eval2.eval(code, 0)); - testCPU(src,"x86_64_v2", "win64",31,null); + testCPU(src,"x86_64_v2", "win64",23,null); }; @Test @@ -303,8 +303,7 @@ return s.skip().x; try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(); } catch( Exception e ) { - // Bad error message, but basically requires a mutable 'x' field. - org.junit.Assert.assertTrue(e.getMessage().startsWith("Argument #0 isa")); + assertEquals("Receiver for method 'skip' is read-only Test._Scan; expected writable Test._Scan",e.getMessage()); } }; diff --git a/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/src/test/java/com/seaofnodes/simple/Chapter25Test.java index 67e7f982..85f273a1 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/integer", "sys/io", "sys/libc", "sys/math", "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/ConstantArrayTest.java b/src/test/java/com/seaofnodes/simple/ConstantArrayTest.java new file mode 100644 index 00000000..c641487b --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/ConstantArrayTest.java @@ -0,0 +1,123 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.StaticData; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.parser.Parser; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; + +public class ConstantArrayTest { + private static StaticData.ObjectData array(CodeGen code, TypeInteger storage, int len) { + var arrays=code._encoding._data.entries().stream().filter(o -> + o.type instanceof TypeStruct ts && ts.isConAry() && + ts.field("[]")._t instanceof TypeConAry c && c.storageType()==storage && c.len()==len).toList(); + assertEquals(1,arrays.size()); + assertTrue(arrays.getFirst().readOnly); + return arrays.getFirst(); + } + + @Test public void typedDataAndSharing() throws Exception { + String source=""" + val a=new u16[~]{025755,65535,65538}; + val b=new u16[~]{025755,65535,65538}; + val fresh={ -> new u16[~]{025755,65535,65538}; }; + val main={ -> + if(a!=b || a!=fresh() || a#!=3 || a[0]!=025755 || a[1]!=65535 || a[2]!=2) return 1; + val bytes=new u8[~]{65,66,67}; + if(bytes!="ABC") return 2; + val signed=new i16[~]{-32768,-1,32767}; + if(signed[0]!=-32768 || signed[1]!=-1 || signed[2]!=32767) return 3; + val ints=new i32[~]{-2147483648,2147483647}; + if(ints[0]!=-2147483648 || ints[1]!=2147483647) return 4; + val uints=new u32[~]{0,4294967295}; + if(uints[1]!=4294967295) return 5; + val longs=new i64[~]{1<<63,0x7fffffffffffffff}; + if(longs[0]!=(1<<63) || longs[1]!=0x7fffffffffffffff) return 6; + val empty=new u16[~]{}; + if(empty#!=0) return 7; + return 0; + }; + """; + TestC.runSF(source,"constantArrayWidths","",-1); + CodeGen code=new CodeGen(source).driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION); + StaticData.ObjectData obj=array(code,TypeInteger.U16,3); + assertEquals("Test.a, Test.b",obj.label()); + assertEquals(12,obj.size); // Four-byte length, six data bytes, alignment padding. + byte[] bytes=code._encoding._cpool.buf(); + assertEquals(3,bytes[obj.offset]); + assertEquals(025755&255,bytes[obj.offset+4]&255); + assertEquals(025755>>>8,bytes[obj.offset+5]&255); + assertEquals(255,bytes[obj.offset+6]&255); + assertEquals(255,bytes[obj.offset+7]&255); + assertNull(code._stop.walk(n -> n instanceof NewNode ? n : null)); + com.seaofnodes.simple.codegen.PrintRegTestSupport.checkDataAliases( + Path.of("build/objs/constantArrayWidths.o"),12,"constantArrayWidths.a","constantArrayWidths.b"); + } + + @Test public void arbitraryProducerUsesStaticData() { + for(String cpu:new String[]{"x86_64_v2","arm","riscv"}) { + CodeGen code=new CodeGen("u16[~]? unused=null; return 0;").parse(); + TypeStruct layout=(TypeStruct)Parser.TYPES.get("[]u16"); + TypeConAry contents=TypeConAry.make(TypeInteger.U16,new long[]{1,65535}); + TypeStruct array=TypeStruct.makeAry(layout._name,layout.field("#")._alias,layout.field("[]")._alias,contents); + Type ptr=TypeMemPtr.make((byte)2,array,true).makeRO(); + // No initializer or allocation node: only compute() supplies the type. + Node producer=new ReadOnlyNode(code.ZERO) { + @Override public String label() { return "constant-array producer"; } + @Override public Type compute() { return ptr; } + }; + ReturnNode ret=code._stop.walk(n -> n instanceof ReturnNode r && r.fun().isClz() ? r : null); + assertNotNull(ret); + ret.setDef(2,producer.peephole()); + code.add(ret); + code.driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + StaticData.ObjectData obj=array(code,TypeInteger.U16,2); + assertEquals(8,obj.size); + } + } + + @Test public void sideEffectsAndMutableArrays() throws Exception { + CodeGen eval=new CodeGen(""" + val same_noInline={u16[~] a -> a==new u16[~]{1,2};}; + return same_noInline(new u16[~]{1,2}); + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(eval,0)); + TestC.runSF(""" + val next={int[] counter -> counter[0]++; return 7;}; + val main={ -> + val count=new int[1]; + val a=new int[~]{next(count),next(count)}; + if(count[0]!=2 || a[0]!=7 || a[1]!=7) return 1; + val b=new int[]{7,7}, c=new int[]{7,7}; + if(b==c) return 2; + int[~] view=b; + b[0]=9; + if(view[0]!=9 || c[0]!=7) return 3; + return 0; + }; + ""","constantArrayEffects","",-1); + } + + @Test public void importedTypedConstants() throws Exception { + Path root=Path.of("build/objs/separate"); + Files.createDirectories(root); + Path lib=Files.createTempDirectory(root,"constant-arrays-"); + new CodeGen(null,lib.toString(),null,"Table",""" + val small=new u16[~]{1,65535}; + val wide=new i64[~]{1<<63,0x7fffffffffffffff}; + val uints=new u32[~]{4294967295}; + val signed=new i32[~]{-2147483648}; + """,126,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,"TableClient",""" + return Table.small[1]==65535 && Table.wide[0]==(1<<63) && + Table.wide[1]==0x7fffffffffffffff && Table.uints[0]==4294967295 && Table.signed[0]==-2147483648; + """,126,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } +} diff --git a/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java b/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java index 96d73f26..bdda3171 100644 --- a/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java +++ b/src/test/java/com/seaofnodes/simple/ConvertNodeTest.java @@ -28,7 +28,7 @@ public class ConvertNodeTest { for( boolean arrayRO : new boolean[]{false,true} ) inputs.add(TypeMemPtr.make(TypeStruct.makeAry("[]test",len,-1, TypeMemPtr.TEST.withAccess(elementRO),-2,false)).withAccess(arrayRO)); - inputs.add(TypeConAryB.make(new byte[]{1,2,3})); + inputs.add(TypeConAry.make(new byte[]{1,2,3})); for( long value : new long[]{Long.MIN_VALUE,-129,-128,-1,0,1,127,128,255,256,Long.MAX_VALUE} ) inputs.add(TypeInteger.constant(value)); for( double value : new double[]{-0.0,0.0,1.0,16777217.0,Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY} ) 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..1ca161cf --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/DrawTest.java @@ -0,0 +1,229 @@ +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(turn)*128, y=232-_sin(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(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(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(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(c,23,59,58); + if( c.pixels[214*720+254]!=GREEN || c.pixels[262*720+470]!=GREEN ) return 3; + c.clear(BACKGROUND); _analog(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(0.25)<0.999 || _sin(0.75)>-0.999 || _sin(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).text(0,0,":/",2,6); + for( int y=0; y<10; y++ ) + for( int x=0; x<16; x++ ) { + int row=y/2, col=x/2; + var colon=col==1 && (row==1 || row==3); + var slash=(row<2 && col==6) || (row==2 && col==5) || (row>2 && col==4); + if( c.pixels[y*16+x]!=((colon || slash) ? 6 : 0) ) return 9; + } + 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/EscapeFlowTest.java b/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java index 5a20388b..12634f24 100644 --- a/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java +++ b/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java @@ -7,6 +7,37 @@ import org.junit.Test; import static org.junit.Assert.*; public class EscapeFlowTest { + @Test public void testArrayGeneratorDoesNotPublishUnrelatedFunctions() { + for( long seed : new long[]{1,123,987654321} ) + for( String access : new String[]{"","~"} ) { + CodeGen code=new CodeGen(""" + val _hidden_noInline={i64 x -> x+1;}; + val data=new i32[%s(arg&7)]{int i -> i*i;}; + return data#; + """.formatted(access),seed,true).parse(); + int hidden=named(code,"_hidden_noInline").sig().fidx(); + code.driver(CodeGen.Phase.TypeCheck); + TypeMem outside=((TypeTuple)code._start._type).mem(); + assertFalse(XInt.bit(outside._escFs,hidden)); + assertEquals("0",Eval2.eval(code,0)); + assertEquals("4",Eval2.eval(code,4)); + } + } + + @Test public void testArrayGeneratorPublishesItsCallbacks() { + CodeGen code=new CodeGen(""" + val _callback_noInline={i64 x -> x+1;}; + val callbacks=new {i64 -> i64}[~3]{int i -> _callback_noInline;}; + return callbacks; + """).parse(); + FunNode callback=named(code,"_callback_noInline"); + int fidx=callback.sig().fidx(); + code.driver(CodeGen.Phase.TypeCheck); + assertTrue(XInt.bit(((TypeTuple)code._start._type).mem()._escFs,fidx)); + assertFalse(callback.isDead()); + assertTrue(callback.unknownCallers()); + } + // A cofinite outside set must not revive private allocation helpers. @Test public void testOpaqueRoundTrip() { for( long seed : new long[]{1,123,987654321} ) { diff --git a/src/test/java/com/seaofnodes/simple/Eval2.java b/src/test/java/com/seaofnodes/simple/Eval2.java index 8d0e3d5b..eef05f93 100644 --- a/src/test/java/com/seaofnodes/simple/Eval2.java +++ b/src/test/java/com/seaofnodes/simple/Eval2.java @@ -110,6 +110,7 @@ public abstract class Eval2 { // Class pointers and initializer self parameters share one object per run. private static final HashMap<String,Object[]> CLASSES = new HashMap<>(); + private static final IdentityHashMap<TypeStruct,Object[]> ARRAYS = new IdentityHashMap<>(); public static String eval( CodeGen code, long arg ) { return eval(code,arg,1000); } public static String eval( CodeGen code, long arg, int timeout ) { @@ -122,6 +123,7 @@ public abstract class Eval2 { // selection. Objects loaded later are scheduled independently. code.driver(CodeGen.Phase.LocalSched); CLASSES.clear(); + ARRAYS.clear(); // Timer for when we run too long. Ticks once per backedge or function call int loopCnt = 0; @@ -334,6 +336,8 @@ public abstract class Eval2 { case RoundF32Node rnd -> (double)(float)d(rnd.in(1)); case ToIntegerNode cvt -> (long)d(cvt.in(1)); case ToFloatNode toflt-> (double)x(toflt.in(1)); + case Intrinsic.F64BitsNode cast -> Double.doubleToRawLongBits(d(cast.in(1))); + case Intrinsic.F64FromBitsNode cast -> Double.longBitsToDouble(x(cast.in(1))); case XorNode xor -> x(xor.in(1)) ^ x(xor.in(2)); default -> throw Utils.TODO(); }; @@ -361,9 +365,12 @@ public abstract class Eval2 { if( tmp._obj.isAry() ) { // Constant array ptr Type elem = tmp._obj.field("[]")._t; if( elem instanceof TypeConAry con ) { + Object[] old=ARRAYS.get(tmp._obj); + if( old!=null ) yield old; Object[] xs = new Object[con.len()]; for( int i=0; i<con.len(); i++ ) xs[i] = con.at8(i); + ARRAYS.put(tmp._obj,xs); yield xs; } else { // Generic constant array, used as a default input to a @@ -388,7 +395,7 @@ public abstract class Eval2 { TypeMem mem = (TypeMem)con._type; Object val = con(mem._t); TypeMemPtr tmp = (TypeMemPtr)esc.self()._type; - int idx = tmp._obj.findAlias(esc.fld()._alias); + int idx = layout(tmp._obj).findAlias(esc.fld()._alias); Object[] fs = (Object[])val(esc.self()); if( tmp._obj.isAry() ) { assert idx==0; // length field @@ -443,10 +450,20 @@ public abstract class Eval2 { private static Object[] classObject(TypeStruct type) { Object[] ptr = CLASSES.get(type._name); - if( ptr==null ) CLASSES.put(type._name,ptr=alloc(type)); + if( ptr==null ) CLASSES.put(type._name,ptr=alloc(layout(type))); return ptr; } + // Imported pointer views may retain parsing-only forward-reference fields. + // Like native field offsets, evaluator slots use the module's final layout. + private static TypeStruct layout(TypeStruct type) { + if( TypeStruct.startsClzPrefix(type._name) ) { + CompUnit cu=CODE._compunits.get(type._name.substring(6).replace('.','/')); + if( cu!=null ) return cu._clz; + } + return type; + } + private static Object not( NotNode not ) { Object n = val(not.in(1)); return ( n==null || (n instanceof Double D && D==0.0) || n instanceof Long L && L==0 ) @@ -465,7 +482,7 @@ public abstract class Eval2 { TypeMemPtr tmp = (TypeMemPtr)ld.ptr()._type; int idx = tmp._obj.isAry() ? offToIdx(x(ld.off()),tmp._obj) - : tmp._obj.find(ld._name); + : layout(tmp._obj).find(ld._name); return fs[idx]; } @@ -476,7 +493,7 @@ public abstract class Eval2 { Object[] fs = (Object[])val(st.ptr()); int idx = tmp._obj.isAry() ? offToIdx(x(st.off()),tmp._obj) - : tmp._obj.find(st._name); + : layout(tmp._obj).find(st._name); Object val = val(st.val()); // Idealization can remove conversions already performed by the store. Type type = st.declaredType(); 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/FloatNegationNativeTest.java b/src/test/java/com/seaofnodes/simple/FloatNegationNativeTest.java new file mode 100644 index 00000000..dbbe353a --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/FloatNegationNativeTest.java @@ -0,0 +1,21 @@ +package com.seaofnodes.simple; + +import org.junit.Test; + +public class FloatNegationNativeTest extends TestC { + @Test public void negateRuntimeValues() throws Exception { + // Keep negation out of constant folding, including both signs of zero. + runSYS(""" + val neg_noInline={f64 x -> -x;}; + val main={ -> + if( neg_noInline(2.5)!=-2.5 || neg_noInline(-3.75)!=3.75 ) return 1; + if( 1.0/neg_noInline(0.0)!=-1e300*1e300 ) return 2; + if( 1.0/neg_noInline(-0.0)!=1e300*1e300 ) return 3; + if( neg_noInline(1e300*1e300)!=-1e300*1e300 ) return 4; + f64 nan=neg_noInline(1e300*1e300-1e300*1e300); + if( nan==nan ) return 5; + return 0; + }; + ""","floatNegation","",-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..110a6a38 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/GraphicsTest.java @@ -0,0 +1,194 @@ +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, breakout; + + @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); + breakout = TestC.compile(Files.readString(Path.of("docs/examples/Breakout.smp")), + "graphics_breakout",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 breakoutWithoutBackend() throws Exception { + assertTrue(run(breakout,"off",1).contains("disabled")); + } + + @Test public void breakoutInputAndFrames() throws Exception { + Path source=Path.of("build/objs/breakout_input.c"); + Path library=Path.of("build/objs/breakout_input"+(TestC.OS.startsWith("Windows") ? ".dll" : ".so")); + Files.writeString(source,""" + #include "simple_graphics.h" + #include <stdio.h> + #include <unistd.h> + static int frames, previous_ball, previous_paddle; + static int32_t open_window(const char *s,uint32_t w,uint32_t h) { + return w==1008 && h==760 ? 0 : -1; + } + static int32_t present(const uint32_t *p,uint32_t n) { + if(n!=1008*760) return -1; + int paddle=-1,ball=-1; + for(int x=24;x<984;x++) if(p[675*1008+x]==0x4ce0ef) { paddle=x; break; } + for(int y=560;y<674 && ball<0;y++) + for(int x=24;x<984;x++) if(p[y*1008+x]==0xeef7ff) { ball=y; break; } + int ok=paddle>=0 && ball>=0; + if(frames==0) ok=ok && paddle==448 && ball==658; + if(frames==1 || frames==6 || frames==9) ok=ok && ball<previous_ball; + if(frames==2 || frames==3) ok=ok && paddle>previous_paddle; + if(frames==4 || frames==5) ok=ok && paddle==previous_paddle && ball==previous_ball; + if(frames==7) ok=ok && paddle==584; + if(frames==8) ok=ok && paddle==448 && ball==658; + if(!ok) { + printf("bad frame %d: paddle %d (was %d), ball %d (was %d)\\n", + frames,paddle,previous_paddle,ball,previous_ball); + return -1; + } + previous_ball=ball; previous_paddle=paddle; + if(++frames==10) puts("breakout input smoke passed"); + return 0; + } + static int32_t poll_events(int32_t n) { return 0; } + static int32_t mouse_x(void) { return frames>=7 ? 640 : 504; } + static int32_t mouse_y(void) { return 680; } + static int32_t key(int32_t k) { + return (k==' ' && (frames==1 || frames==9)) || + (k==262 && (frames==2 || frames==3)) || + (k=='P' && (frames==4 || frames==6)) || + (k=='R' && (frames==8 || frames==9)); + } + static int32_t wait_events(int64_t ms) { usleep(ms*1000); return 0; } + static void close_window(void) {} + static const struct simple_graphics_api api={SIMPLE_GRAPHICS_VERSION, + open_window,present,poll_events,mouse_x,mouse_y,poll_events,key,close_window,wait_events}; + 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()); + var pb=new ProcessBuilder(Path.of(breakout).toAbsolutePath().toString(),"smoke").redirectErrorStream(true); + pb.environment().put("SIMPLE_MINIFB",library.toAbsolutePath().toString()); + pb.environment().put("SIMPLE_MINIAUDIO","off"); + Process child=pb.start(); + try { + assertTrue("Breakout timed out",child.waitFor(10,TimeUnit.SECONDS)); + String output=new String(child.getInputStream().readAllBytes(),java.nio.charset.StandardCharsets.UTF_8); + assertEquals(output,0,child.exitValue()); + assertEquals("breakout input smoke passed\n",output); + } finally { if(child.isAlive()) child.destroyForcibly(); } + } + + @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/IntrinsicTest.java b/src/test/java/com/seaofnodes/simple/IntrinsicTest.java new file mode 100644 index 00000000..e3b30efb --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/IntrinsicTest.java @@ -0,0 +1,188 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; + +public class IntrinsicTest { + private static final String DECL=""" + { f64 -> i64 } simple_f64_bits="C"; + { i64 -> f64 } simple_f64_from_bits="C"; + """; + + @Test public void constantsAndRawNaNs() { + long[] patterns={0,Long.MIN_VALUE,1,0x0010000000000000L,0x3ff0000000000000L, + 0x7fefffffffffffffL,0x7ff0000000000000L,0xfff0000000000000L, + 0x7ff8000000000001L,0x7ff8000000000002L,0xfff800000000002aL,0x7ff0000000000001L}; + for(long bits:patterns) { + String literal=bits==Long.MIN_VALUE ? "(-9223372036854775807-1)" : Long.toString(bits); + CodeGen code=new CodeGen(DECL+"return simple_f64_bits(simple_f64_from_bits("+literal+"));") + .driver(CodeGen.Phase.TypeCheck); + assertNull(code._stop.walk(n -> n instanceof CallNode ? n : null)); + assertEquals(Long.toString(bits),Eval2.eval(code,0)); + } + assertNotSame(TypeFloat.constant(Double.longBitsToDouble(patterns[8])), + TypeFloat.constant(Double.longBitsToDouble(patterns[9]))); + } + + @Test public void runtimeNodesAndMemory() { + CodeGen code=new CodeGen(DECL+""" + val bits=simple_f64_bits; + val a=new f64[1]; + a[0]=simple_f64_from_bits(arg); + if( arg==0 ) a[0]=-0.0; + return bits(a[0]); + """).driver(CodeGen.Phase.TypeCheck); + assertNotNull(code._stop.walk(n -> n instanceof Intrinsic.BitCastNode ? n : null)); + assertNull(code._stop.walk(n -> n instanceof CallNode ? n : null)); + assertEquals(Long.toString(Long.MIN_VALUE),Eval2.eval(code,0)); + assertEquals("9221120237041090602",Eval2.eval(code,0x7ff800000000002aL)); + } + + @Test public void ordinaryFunctionWithSameName() { + CodeGen code=new CodeGen(""" + val simple_f64_bits={ f64 x -> 42; }; + return simple_f64_bits(1.0); + """); + assertEquals("42",Eval2.eval(code,0)); + } + + @Test public void wrongSignature() { + try { + new CodeGen("{ i64 -> i64 } simple_f64_bits=\"C\"; return simple_f64_bits(arg);") + .driver(CodeGen.Phase.TypeCheck); + fail("The reserved C symbol has fixed semantics and a fixed signature"); + } catch(RuntimeException e) { + assertTrue(e.getMessage(),e.getMessage().contains("Intrinsic 'simple_f64_bits' requires")); + } + } + + @Test public void nativeRepresentations() throws Exception { + TestC.runSYS(""" + val bits_noInline={ f64 x -> sys.math.f64Bits(x); }; + val float_noInline={ i64 x -> sys.math.f64FromBits(x); }; + val main={ -> + val patterns=new i64[~]{0,sys.integer.MIN_I64,1,0x0010000000000000, + 0x3ff0000000000000,0x7fefffffffffffff,0x7ff0000000000000, + 0x7ff8000000000001,0x7ff8000000000002,-2251799813685206,0x7ff0000000000001}; + for( int i=0; i<patterns#; i++ ) + if( bits_noInline(float_noInline(patterns[i]))!=patterns[i] ) return 1; + if( bits_noInline(-0.0)!=sys.integer.MIN_I64 ) return 2; + if( float_noInline(0x4008000000000000)!=3.0 ) return 3; + int numeric=3.75; + if( numeric!=3 ) return 4; + return 0; + }; + ""","intrinsicNative","",-1); + } + + @Test public void indirectCallsUseCFallback() throws Exception { + String source=DECL+""" + { f64 -> i64 } llround="C"; + val apply_noInline={ { f64 -> i64 } f, f64 x -> f(x); }; + val main={ -> + if( apply_noInline(simple_f64_bits,-0.0)!=(-9223372036854775807-1) ) return 1; + if( apply_noInline(llround,3.75)!=4 ) return 2; + if( apply_noInline(simple_f64_bits,1.0)!=0x3ff0000000000000 ) return 3; + if( apply_noInline(simple_f64_bits,simple_f64_from_bits(0x7ff800000000002a))!=0x7ff800000000002a ) return 4; + return 0; + }; + """; + CodeGen code=new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + assertNotNull("The indirect C call must survive",code._stop.walk(n -> + n instanceof CallNode c && c.external() && !c.tfp().isConstant() ? n : null)); + TestC.runSYS(source,"intrinsicFallback","",-1); + } + + @Test public void importedNodesAndNaNConstants() throws Exception { + Path dir=Files.createTempDirectory(Path.of("build/objs"),"intrinsic-import-"); + String library=DECL+""" + val bits_noInline={ f64 x -> simple_f64_bits(x); }; + val float_noInline={ i64 x -> simple_f64_from_bits(x); }; + val nan=simple_f64_from_bits(0x7ff8000000000001); + val otherNaN=simple_f64_from_bits(0x7ff8000000000002); + """; + new CodeGen(null,dir.toString(),null,"Bits",library,126,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + var paths=new Ary<>(new String[]{dir.toString()}); + String[] clients={ + "return Bits.bits_noInline(Bits.float_noInline(arg));", + "return Bits.bits_noInline(Bits.nan);", + "return Bits.bits_noInline(Bits.otherNaN);" + }; + long[] expected={0x7ff800000000002aL,0x7ff8000000000001L,0x7ff8000000000002L}; + for(int i=0;i<clients.length;i++) { + CodeGen client=new CodeGen(null,dir.toString(),paths,"Client",clients[i],126,true,TypeInteger.BOT); + assertEquals(Long.toString(expected[i]),Eval2.eval(client,expected[0])); + } + } + + @Test public void allBackendsSelectRepresentationMoves() { + // The array store/load and branch keep both directions alive, rather + // than allowing the inverse-cast peephole to erase the whole test. + String source=DECL+""" + val a=new f64[1]; + a[0]=simple_f64_from_bits(arg); + if( arg==0 ) a[0]=-0.0; + return simple_f64_bits(a[0]); + """; + for(String cpu:new String[]{"x86_64_v2","arm","riscv"}) { + CodeGen code=new CheckedCodeGen(source).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + assertNotNull(code._stop.walk(n -> n instanceof MachConcreteNode m && m.op().equals("f64bits") ? n : null)); + assertNotNull(code._stop.walk(n -> n instanceof MachConcreteNode m && m.op().equals("f64frombits") ? n : null)); + } + } + + @Test public void invalidArgumentIsNotInlinedAway() { + try { + new CodeGen(DECL+"return simple_f64_bits(1);").driver(CodeGen.Phase.TypeCheck); + fail("Intrinsic inlining must not hide the call's type error"); + } catch(RuntimeException e) { + assertTrue(e.getMessage(),e.getMessage().contains("Argument #1 isa")); + } + } + + @Test public void discardedValueKeepsArgumentEffects() throws Exception { + TestC.runSYS(DECL+""" + val next={ i64[] a -> a[0]++; return 1.0; }; + val main={ -> + val a=new i64[1]; + simple_f64_bits(next(a)); + return a[0]!=1; + }; + ""","intrinsicEffects","",-1); + } + + @Test public void emulatedRepresentationMoves() throws Exception { + long[] patterns={0,Long.MIN_VALUE,1,0x3ff0000000000000L,0x7ff0000000000000L, + 0xfff0000000000000L,0x7ff800000000002aL,0x7ff0000000000001L}; + for(boolean toBits:new boolean[]{true,false}) { + String source=toBits + ? "val cast={ f64 x -> { f64 -> i64 } simple_f64_bits=\"C\"; return simple_f64_bits(x); };" + : "val cast={ i64 x -> { i64 -> f64 } simple_f64_from_bits=\"C\"; return simple_f64_from_bits(x); };"; + var arm=TestArm64.build("cast",source,0,-1,false); + var risc=TestRisc5.build(source,"cast",0,-1,false); + int armStart=arm._pc, riscStart=risc._pc; + for(long bits:patterns) { + arm._pc=armStart; risc._pc=riscStart; + if(toBits) { + arm.fregs[0]=Double.longBitsToDouble(bits); + risc.fregs[10]=Double.longBitsToDouble(bits); + } else { + arm.regs[0]=bits; + risc.regs[10]=bits; + } + assertEquals(0,arm.step(1000)); + assertEquals(0,risc.step(1000)); + assertEquals(bits,toBits ? arm.regs[0] : Double.doubleToRawLongBits(arm.fregs[0])); + assertEquals(bits,toBits ? risc.regs[10] : Double.doubleToRawLongBits(risc.fregs[10])); + } + } + } +} diff --git a/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java b/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java index adeba136..6d81af85 100644 --- a/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java +++ b/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java @@ -8,6 +8,31 @@ import static org.junit.Assert.*; // Execution regressions are separate from the fixed spill-measurement cohorts. public class MemoryEncodingTest { + @Test public void testIntegerMemoryOperandsAcrossCalls() throws java.io.IOException { + TestC.runSF(""" + val touch_noInline={i64[] a, i64 i -> a[i]+=1;}; + val scan_noInline={i64[] a, i64 limit, i64 delta -> + int below=0, above=0; + for(int i=0;i<a#;i++) { + touch_noInline(a,i); + if(a[i]<limit) below++; + if(limit<a[i]) above++; + a[i]+=delta; + } + return below*10+above; + }; + val main={-> + val a=new i64[]{0x7ffffffffffffffe,-2,0}; + if(scan_noInline(a,0x7fffffffffffffff,0)!=20) return 1; + if(a[0]!=0x7fffffffffffffff || a[1]!=-1 || a[2]!=1) return 2; + val b=new i64[]{-3,-2,0}; + if(scan_noInline(b,-1,0x100000000)!=11) return 3; + if(b[0]!=0xfffffffe || b[1]!=0xffffffff || b[2]!=0x100000001) return 4; + return 0; + }; + ""","integerMemoryAcrossCalls","",-1); + } + @Test public void testLoopLoadSearch() { String[] sources = { """ diff --git a/src/test/java/com/seaofnodes/simple/MethodAccessTest.java b/src/test/java/com/seaofnodes/simple/MethodAccessTest.java new file mode 100644 index 00000000..7275b151 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/MethodAccessTest.java @@ -0,0 +1,118 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.parser.Parser; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MethodAccessTest { + @Test public void readersAcceptReadOnlyReceivers() throws Exception { + String source = """ + struct Child { int n = 7; }; + struct Box { + !Child child = new Child; + int[] values = new int[]{3,5}; + int count = 2; + { i64 } read = { -> count+child.n+values[1]; }; + val delegate = { -> read(); }; + val recurse = { i64 n -> n == 0 ? read() : recurse(n-1); }; + val draw = { !Child canvas -> canvas.n = count; }; + val outer = { -> later(count); }; + }; + val later = { int n -> n+1; }; + val check = { Box box -> + val canvas = new Child; + box.draw(canvas); + return box.read()+box.delegate()+box.recurse(3)+canvas.n+box.outer(); + }; + return check(new Box); + """; + assertEquals("47",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0)); + TestC.runSF("val main = { -> "+source.replace("return check(new Box);","return check(new Box) != 47;")+" };", + "readonlyMethods","",-1); + } + + @Test public void writesThroughReceiverRequireAccess() { + String[] methods = { + "val act = { -> x = 2; };", + "val act = { -> self.x++; };", + "val act = { -> val nested = { -> x++; }; nested(); };", + "val act = { -> val alias = self; alias.x += 1; };", + "val act = { -> child.n = 2; };", + "val act = { -> values[0]++; };", + "val act = { -> write(); }; val write = { -> x++; };", + "val act = { -> val f = x == 0 ? left : right; f(); }; val left = { -> x++; }; val right = { -> x--; };", + "val act = { -> child.write(); };", + "val act = { -> write(self); };", + "{ int } act = \"C\";" + }; + for( String method : methods ) { + String source = """ + struct Child { int n; val write = { -> n++; }; }; + struct Box { + int x; + !Child child = new Child; + int[] values = new int[1]; + """+method+""" + }; + val write = { !Box box -> box.x++; }; + val check = { Box box -> box.act(); return 0; }; + return check(new Box); + """; + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Accepted writer: "+method); + } catch( Parser.ParseException e ) { + assertTrue(method+": "+e.getMessage(),e.getMessage().contains("read-only")); + } + new CodeGen(source.replace("val check = { Box box", "val check = { !Box box")) + .driver(CodeGen.Phase.TypeCheck); + } + } + + @Test public void returningWritableAliasesRequiresAccess() { + String source = """ + struct Box { int x; val alias = { -> self; }; }; + val check = { Box box -> box.alias(); }; + return check(new Box); + """; + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Read-only receiver exposed as writable"); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("read-only")); + } + source = source.replace("val alias = { -> self; }", "val alias = { -> Box view = self; view; }"); + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + } + + @Test public void importedReceiverAccess() throws Exception { + var root = Files.createDirectories(Path.of("build/objs/separate")); + var lib = Files.createTempDirectory(root,"method-access-"); + new CodeGen(null,lib.toString(),null,"Counters",""" + struct Counter { + int n = 7; + val read = { -> n; }; + val write = { -> n++; }; + }; + val make = { -> new Counter; }; + """,126,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + var paths = new Ary<>(new String[]{lib.toString()}); + String source = "val box = Counters.make(); val check = { Counters.Counter c -> c.read(); }; return check(box);"; + var code = new CodeGen(null,"build/objs",paths,"CounterReader",source,126,true, + TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("7",Eval2.eval(code,0)); + try { + new CodeGen(null,"build/objs",paths,"CounterWriter",source.replace("c.read()","c.write()"),126,true, + TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + fail("Imported writer accepted a read-only receiver"); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("read-only")); + } + } +} diff --git a/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java b/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java new file mode 100644 index 00000000..3fe5c28b --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/MethodFieldsTest.java @@ -0,0 +1,131 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.parser.Parser; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MethodFieldsTest { + @Test public void nestedFunctionStillRejectsLocalCapture() { + String source = """ + struct Game { + int count; + val input = { int amount -> + val action = { -> count += amount; }; + action(); + }; + }; + return 0; + """; + try { + new CodeGen(source).driver(CodeGen.Phase.Parse); + fail("Captured a nonconstant local parameter"); + } catch( Parser.ParseException e ) { + assertEquals("Variable 'amount' is out of function scope and must be a final constant",e.getMessage()); + } + } + + @Test public void nestedFunctionReceiver() throws Exception { + String source = """ + struct Game { + bool held, paused; + int count; + val debounce = { bool pressed, bool wasHeld, { bool } action -> + if( pressed && !wasHeld ) action(); + return pressed; + }; + val bump = { -> count++; return false; }; + val input = { bool pressed -> + if( true ) { + held = debounce(pressed,held,{ -> + paused = !paused; + bump(); + val again = { -> count += 2; return false; }; + again(); + return false; + }); + } + }; + val named = { -> debounce(true,false,bump); }; + }; + val a = new Game, b = new Game; + a.input(true); a.input(true); + if( a.count != 3 || !a.paused || b.count != 0 || b.paused ) return 1; + a.input(false); a.input(true); + if( a.count != 6 || a.paused ) return 2; + b.input(true); b.named(); + if( b.count != 4 || !b.paused || a.count != 6 ) return 3; + return 0; + """; + assertEquals("0",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0)); + TestC.runSF("val main = { -> "+source+" };","nestedFunctionReceiver","",-1); + } + + @Test public void laterSiblingParameterType() throws Exception { + String source = """ + struct Game { + val input = { !Controls keys -> keys.presses++; return keys.presses; }; + val read = { Controls keys -> keys.presses; }; + }; + struct Controls { int presses = 4; }; + val game = new Game, keys = new Controls; + if( game.input(keys) != 5 || game.read(keys) != 5 ) return 1; + return 0; + """; + assertEquals("0",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0)); + TestC.runSF("val main = { -> "+source+" };","forwardMethodType","",-1); + } + + private static final String FIELDS = """ + int count = 100; + struct Counter { + val step = { int n -> + count = count+n; + ++count; + count += 2; + speed *= 2.0; + return read(); + }; + val local = { int count -> count+1; }; + val read = { -> count; }; + int count = 3; + f64 speed = 1.5; + }; + val a = new Counter, b = new Counter; + if( a.step(4) != 10 || b.read() != 3 || a.speed != 3.0 ) return 1; + if( a.local(20) != 21 || a.read() != 10 || count != 100 ) return 2; + if( b.step(1) != 7 || a.read() != 10 ) return 3; + return 0; + """; + + @Test public void laterFieldsAndMethods() throws Exception { + assertEquals("0",Eval2.eval(new CodeGen(FIELDS).driver(CodeGen.Phase.LocalSched),0)); + TestC.runSF("val main = { -> "+FIELDS+" };","methodFields","",-1); + } + + @Test public void laterGlobalAndRecursiveMethod() { + String source = """ + struct Counter { + val step = { i64 n -> + if( n == 0 ) return value; + value += increment; + return step(n-1); + }; + int value = 1; + }; + val increment = 2; + return new Counter.step(3); + """; + assertEquals("7",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.LocalSched),0)); + } + + @Test public void laterFinalFieldStillRejectsWrites() { + String source = "struct C { val change = { -> x = 2; }; int ~x = 1; }; return new C.change();"; + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Method wrote a final field"); + } catch( Parser.ParseException e ) { + assertEquals("Cannot reassign final 'x'",e.getMessage()); + } + } +} diff --git a/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java index 2b7eea78..d342892c 100644 --- a/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java +++ b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java @@ -10,6 +10,57 @@ import org.junit.Test; import static org.junit.Assert.*; public class ParserDiagnosticTest { + @Test public void testMissingForwardTypeDiagnostic() { + String source = """ + val read = { Missing value -> + return value.field; + }; + return 0; + """; + var error = rejected(source); + assertEquals("Unknown type 'Test.Missing'",error.getMessage()); + assertTrue(error.diagnostic(),error.diagnostic().startsWith("Test.smp:2:")); + assertTrue(error.diagnostic(),error.diagnostic().contains("return value.field;")); + } + + @Test public void testReadOnlyReceiverDiagnostic() { + String source = """ + struct Box { + int x = 1; + val bump = { -> x++; x; }; + }; + val draw = { Box b -> + return b.bump(); + }; + return draw(new Box); + """; + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Read-only receiver accepted by a writable method"); + } catch( Parser.ParseException e ) { + assertEquals("Receiver for method 'bump' is read-only Test.Box; expected writable Test.Box",e.getMessage()); + assertTrue(e.diagnostic(),e.diagnostic().startsWith("Test.smp:6:18: error: ")); + assertFalse(e.getMessage().contains("{i32")); + } + } + + @Test public void testReadOnlyArgumentDiagnostic() { + String source = """ + struct Box { int x = 1; }; + val write = { !Box b -> b.x = 2; }; + val draw = { Box b -> write(b); }; + return draw(new Box); + """; + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Read-only argument accepted by a writable parameter"); + } catch( Parser.ParseException e ) { + assertEquals("Argument #1 is read-only Test.Box; expected writable Test.Box",e.getMessage()); + } + new CodeGen(source.replace("val draw = { Box b", "val draw = { !Box b")) + .driver(CodeGen.Phase.TypeCheck); + } + private static Parser.ParseException rejected(String source) { try { new CodeGen(source).driver(CodeGen.Phase.Parse); @@ -30,7 +81,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() { @@ -123,6 +185,24 @@ public class ParserDiagnosticTest { +"\n return x;\n ^",error.diagnostic()); } + @Test public void testJUnitReportsSourceDiagnostic() { + var error=rejected("val dist=new i64[3]{int i -> i;}\ndist[0]=0;"); + String expected="Test.smp:2:1: error: Syntax error, expected `;` but found `dist`" + +"\n dist[0]=0;\n ^"+System.lineSeparator(); + var test=org.junit.runner.Description.createTestDescription(getClass(),"missingSemicolon"); + assertEquals(expected,new org.junit.runner.notification.Failure(test,error).getTrace()); + + var bytes=new java.io.ByteArrayOutputStream(); + error.printStackTrace(new java.io.PrintStream(bytes,true,StandardCharsets.UTF_8)); + assertEquals(expected,bytes.toString(StandardCharsets.UTF_8)); + assertEquals("Syntax error, expected `;` but found `dist`",error.getMessage()); + + // Compiler implementation failures must still include Java frames. + String trace=new org.junit.runner.notification.Failure(test,new IllegalStateException("compiler bug")).getTrace(); + assertTrue(trace,trace.contains("java.lang.IllegalStateException: compiler bug")); + assertTrue(trace,trace.contains("ParserDiagnosticTest.testJUnitReportsSourceDiagnostic")); + } + @Test public void testEOF() { var error=rejected("return 1"); assertEquals("Test.smp:1:9: error: Syntax error, expected `;` but found `EOF`" 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..dfffc1b9 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/TimeTest.java @@ -0,0 +1,183 @@ +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)!=0 || q.nextDeadline()!=-1 ) return 1; + u32 a=q.afterAt(100,20), b=q.afterAt(100,10), c=q.afterAt(100,10); + if( a==0 || b==0 || c==0 ) return 10; + if( q.nextDeadline()!=110 || q.poll(109)!=0 || q.poll(110)!=b || q.poll(110)!=c || q.poll(110)!=0 ) return 2; + if( !q.cancel(a) || q.cancel(a) || q.cancel(0) || q.nextDeadline()!=-1 ) return 3; + u32 repeat=q.everyAt(100,10); + if( q.poll(135)!=repeat || q.nextDeadline()!=140 || q.poll(135)!=0 ) 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; + u32 prev=0; + for( int i=0; i<20; i++ ) { u32 id=q.poll(300); if( id<=prev ) return 6; prev=id; } + if( q.nextDeadline()!=-1 || q.poll(1000)!=0 ) return 7; + if( q.afterAt(-1,1)!=0 || q.afterAt(0,-1)!=0 || q.everyAt(0,0)!=0 || q.afterAt(9223372036854775807,1)!=0 ) return 8; + u32 last=q.everyAt(9223372036854775806,1); + if( last==0 || q.poll(9223372036854775807)!=last || q.nextDeadline()!=-1 ) return 9; + // Exhaust IDs without wrapping or letting an old ID cancel a new timer. + q._next=0xfffffffe; + u32 high=q.afterAt(0,10), max=q.afterAt(0,10); + if( high!=0xfffffffe || max!=0xffffffff || q.afterAt(0,0)!=0 ) return 12; + if( q.cancel(a) || q.poll(10)!=high || q.poll(10)!=max || q.poll(10)!=0 ) return 13; + if( q.afterAt(0,0)!=0 || q.nextDeadline()!=-1 ) return 14; + ""","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/TypeTest.java b/src/test/java/com/seaofnodes/simple/TypeTest.java index 16b75fdc..52e857e9 100644 --- a/src/test/java/com/seaofnodes/simple/TypeTest.java +++ b/src/test/java/com/seaofnodes/simple/TypeTest.java @@ -7,6 +7,26 @@ import static org.junit.Assert.assertSame; public class TypeTest { + @Test public void testConstantArrayElements() { + TypeConAry range=TypeConAry.make(TypeInteger.U16,new long[]{5,6,7}); + Assert.assertEquals("[5-7]",range.toString()); + Assert.assertEquals("[5-7]",TypeConAry.make(TypeInteger.U8,new long[]{5,6,7}).toString()); + Assert.assertEquals("[\"abc\"]",TypeConAry.make("abc").toString()); + for( TypeInteger storage : new TypeInteger[]{TypeInteger.BOOL,TypeInteger.I8,TypeInteger.U8, + TypeInteger.I16,TypeInteger.U16,TypeInteger.I32,TypeInteger.U32,TypeInteger.BOT} ) { + TypeConAry empty=TypeConAry.make(storage,new long[0]); + assertSame(storage.dual(),empty.elem()); + assertSame(storage,((TypeConAry)empty.dual()).elem()); + for( long[] values : new long[][]{{0},{1},{0,1},{storage._min,storage._max}} ) { + TypeConAry array=TypeConAry.make(storage,values); + Assert.assertTrue(empty.isa(array)); + assertSame(array,empty.meet(array)); + assertSame(empty,array.join(empty)); + check_symmetric(empty,array); + } + } + } + @Test public void testOpaqueExternalEscapes() { TypeMem defaults = TypeMem.START.makeFrom(XInt.make(2),XInt.make(3)); TypeMem prior = TypeMem.START.makeFrom(XInt.make(5),XInt.make(7)); diff --git a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java index b255bcf7..190cf17c 100644 --- a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java +++ b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java @@ -3,6 +3,30 @@ import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.TypeInteger; import static org.junit.Assert.*; public class PrintRegTestSupport { + public static void checkDataAliases(java.nio.file.Path file, int size, String... names) { + ElfReader elf=ElfReader.load(file.toFile(),null); + assertNotNull(elf); + ElfReader.Section symbols=java.util.Arrays.stream(elf._sections) + .filter(s -> ".symtab".equals(s._name)).findFirst().orElseThrow(); + ElfReader.Section strings=elf._sections[symbols._link]; + var bytes=java.nio.ByteBuffer.wrap(elf._buf).order(java.nio.ByteOrder.LITTLE_ENDIAN); + var wanted=new java.util.HashSet<>(java.util.Arrays.asList(names)); + Long address=null; + for( int i=0; i<symbols._size/symbols._esize; i++ ) { + int off=(int)symbols._offset+i*symbols._esize; + String name=strings.str(bytes.getInt(off)); + if( !wanted.remove(name) ) continue; + assertTrue("Debug aliases precede global symbols",i<symbols._info); + assertEquals("Local object symbol",1,bytes.get(off+4)&255); + assertEquals(".rodata",elf._sections[bytes.getShort(off+6)&65535]._name); + assertEquals(size,bytes.getLong(off+16)); + long value=bytes.getLong(off+8); + if( address!=null ) assertEquals("Aliases share storage",address.longValue(),value); + address=value; + } + assertTrue("Missing debug symbols: "+wanted,wanted.isEmpty()); + } + public static void check() throws Exception { var code = new CodeGen("return 0;").parse(); var alloc = new RegAlloc(code); @@ -61,7 +85,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(); |
