summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-11-08 14:10:02 -0800
committerCliff Click <cliffc@acm.org>2026-11-08 14:10:02 -0800
commit06fb5aceeb65a75b1cd643debd667c52fdcafcb9 (patch)
treec99813eaf27d98f840245302c4d6e923c964307e
parent6560a92433a38825fd1d44d9c66ffba9f26990ff (diff)
downloadSimple-06fb5aceeb65a75b1cd643debd667c52fdcafcb9.tar.gz
Simple-06fb5aceeb65a75b1cd643debd667c52fdcafcb9.zip
Better error messagesch30
Better error messages. Buffs to the std lib; getch() to fetch a single character; streaming I/O handles retries, and large sizes, with better error reporting. Formatted strings. Default int moved to i32. TicTacToe, the game.
-rw-r--r--README.md4
-rw-r--r--chapter28b/ainotes.txt34
-rw-r--r--chapter29/ainotes.txt34
-rw-r--r--chapter30/Makefile18
-rw-r--r--chapter30/README.ja.md33
-rw-r--r--chapter30/README.md221
-rw-r--r--chapter30/ainotes.txt34
-rw-r--r--chapter30/docs/30-grammar.md27
-rw-r--r--chapter30/docs/examples/A_helloWorld.smp (renamed from chapter30/src/test/smp/examples/A_helloWorld.smp)0
-rw-r--r--chapter30/docs/examples/BubbleSort.smp (renamed from chapter30/src/test/smp/examples/BubbleSort.smp)8
-rw-r--r--chapter30/docs/examples/Capitalize.smp (renamed from chapter30/src/test/smp/examples/Capitalize.smp)3
-rw-r--r--chapter30/docs/examples/Dijkstra.smp (renamed from chapter30/src/test/smp/examples/Dijkstra.smp)8
-rw-r--r--chapter30/docs/examples/FileIO.smp (renamed from chapter30/src/test/smp/examples/FileIO.smp)6
-rw-r--r--chapter30/docs/examples/README.md71
-rw-r--r--chapter30/docs/examples/TicTacToe.smp133
-rw-r--r--chapter30/src/main/c/simple_crt.c124
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java2
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java2
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/codegen/IFG.java8
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java8
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/codegen/Machine.java12
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java21
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java51
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/codegen/SourceError.java16
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/CallNode.java2
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/FldOffNode.java2
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/FormatNode.java33
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/LoadNode.java3
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/ScopeNode.java1
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java15
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java3
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java15
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java3
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java15
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java4
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/Allocations.java20
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java4
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/Declarations.java21
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/Expressions.java41
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/Functions.java13
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/Lexer.java139
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/Parser.java23
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/References.java46
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/Statements.java27
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/Strings.java59
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/Structs.java6
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/parser/Types.java6
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/type/TypeConAry.java15
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java2
-rw-r--r--chapter30/src/main/java/com/seaofnodes/simple/type/TypeInteger.java10
-rw-r--r--chapter30/src/main/smp/sys.smp2
-rw-r--r--chapter30/src/main/smp/sys/ary.smp26
-rw-r--r--chapter30/src/main/smp/sys/aryi64.smp25
-rw-r--r--chapter30/src/main/smp/sys/aryu8.smp45
-rw-r--r--chapter30/src/main/smp/sys/io.smp75
-rw-r--r--chapter30/src/main/smp/sys/libc.smp25
-rw-r--r--chapter30/src/main/smp/sys/rt.smp3
-rw-r--r--chapter30/src/main/smp/sys/rt/simple_args.smp22
-rw-r--r--chapter30/src/main/smp/sys/text.smp98
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/BoundaryTest.java12
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/BrainFuckTest.java2
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter04Test.java4
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter05Test.java6
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter07Test.java18
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter08Test.java6
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter09Test.java4
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter10Test.java9
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter12Test.java2
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter13Test.java8
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java6
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter15Test.java4
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter16Test.java4
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter17bTest.java23
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter18Test.java46
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter19Test.java36
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter20Test.java14
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter21Test.java27
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter22Test.java11
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter23Test.java2
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter24Test.java7
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Chapter25Test.java26
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java14
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java10
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java8
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java10
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java10
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java6
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Int32Test.java94
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/MergeSortTest.java13
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/MinusNodeTest.java2
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java191
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/PartialReadTest.java6
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/RuntimeArgsTest.java29
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java32
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java8
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/Simple.java30
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/StreamIOTest.java138
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/StringFormatTest.java158
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/TerminalInputTest.java184
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/TestC.java53
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/TicTacToeTest.java92
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java70
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/progs/extern_data.c7
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/progs/int32.c26
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/progs/runtime_args.c35
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/progs/sort.c4
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/progs/stream_io.c49
-rw-r--r--chapter30/src/test/java/com/seaofnodes/simple/progs/stringHash.c2
-rw-r--r--chapter30/src/test/smp/separate/client/Main.smp2
-rw-r--r--docs/roadmap.ja.md26
-rw-r--r--docs/roadmap.md211
-rw-r--r--skills/chapter25-codex-notes.md39
112 files changed, 2909 insertions, 654 deletions
diff --git a/README.md b/README.md
index d2f516fa..b66fba22 100644
--- a/README.md
+++ b/README.md
@@ -77,8 +77,8 @@ The Simple language is styled after a subset of C or Java.
* [Chapter 27](chapter27/README.md): Escape Analysis and Unknown Callers
* [Chapter 28a](chapter28a/README.md): Modules and Lazy Class Initialization
* [Chapter 28b](chapter28b/README.md): Separate Compilation
-* [Chapter 29](chapter29/README.md): Tech Debt Cleanup
-* [Chapter 30](chapter30/README.md): Work in Progress
+* [Chapter 29](chapter29/README.md): Tech Debt Cleanup, Partial Objects
+* [Chapter 30](chapter30/README.md): Text Formatting and Diagnostics
Appendices: [Native libraries](docs/appendices/native-libraries.md),
[optimizer and backend tuning](docs/appendices/optimizer-tuning.md).
diff --git a/chapter28b/ainotes.txt b/chapter28b/ainotes.txt
deleted file mode 100644
index 5678e686..00000000
--- a/chapter28b/ainotes.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-
-Historical design notes from the former combined Chapter 25, now Chapter 28b.
-The cleanup record is in ../skills/stage-audit.md; remaining work is in
-../docs/roadmap.md. The original intent below is preserved.
-
-Meta-info: I intend to keep extending and re-editing this "ainotes.txt" file so
-I can reprompt the you (the AI) between AI restarts, As such this file will
-contain a mix of high-level design decisions, goals, intents and wisdom, and
-then taper down to more detailed problems and solutions - which probably keep
-changing.
-
-AI is allowed access to all files recursively in "C:/Users/cliffc/Desktop/Simple"
-directory.
-
-Meta-info: all the tests run with `make tests` currently run, and should again
-run before we accept any changes.
-
-
-The overall project is rooted in the `Simple/` directory,
-and is a compiler tutorial broken up into chapters.
-The chapters build on each other.
-
-Read the code in this chapter25; the goal in this chapter is to allow separate
-compilation. I want to compile a "standard library", the start of which is in
-`src/main/smp/sys.smp` and subdirectories and end up with either an archive or
-a "fat ELF .o" file.
-
-Then I want to compile a 1-liner "hello world" program:
- `sys.io.p("Hello World!");`
-and have the symbols `sys`, `io` and `p` be found in the standard library.
-
-I am using a test case to represent this seperate compilation strategy in
-`Chapter25Test.testModules`. There are comments there to help drive my
-thinking.
diff --git a/chapter29/ainotes.txt b/chapter29/ainotes.txt
deleted file mode 100644
index 6dd4b14f..00000000
--- a/chapter29/ainotes.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-
-Historical design notes from the former combined Chapter 25, carried forward from Chapter 28b.
-The cleanup record is in ../skills/stage-audit.md; remaining work is in
-../docs/roadmap.md. The original intent below is preserved.
-
-Meta-info: I intend to keep extending and re-editing this "ainotes.txt" file so
-I can reprompt the you (the AI) between AI restarts, As such this file will
-contain a mix of high-level design decisions, goals, intents and wisdom, and
-then taper down to more detailed problems and solutions - which probably keep
-changing.
-
-AI is allowed access to all files recursively in "C:/Users/cliffc/Desktop/Simple"
-directory.
-
-Meta-info: all the tests run with `make tests` currently run, and should again
-run before we accept any changes.
-
-
-The overall project is rooted in the `Simple/` directory,
-and is a compiler tutorial broken up into chapters.
-The chapters build on each other.
-
-Read the code in this chapter25; the goal in this chapter is to allow separate
-compilation. I want to compile a "standard library", the start of which is in
-`src/main/smp/sys.smp` and subdirectories and end up with either an archive or
-a "fat ELF .o" file.
-
-Then I want to compile a 1-liner "hello world" program:
- `sys.io.p("Hello World!");`
-and have the symbols `sys`, `io` and `p` be found in the standard library.
-
-I am using a test case to represent this seperate compilation strategy in
-`Chapter25Test.testModules`. There are comments there to help drive my
-thinking.
diff --git a/chapter30/Makefile b/chapter30/Makefile
index ec59a033..e4537ca4 100644
--- a/chapter30/Makefile
+++ b/chapter30/Makefile
@@ -155,7 +155,19 @@ 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_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)
+TICTACTOE:= $(GAME_DIR)/TicTacToe$(if $(filter Windows_NT,$(OS)),.exe,)
+
+# Build a runnable game without starting it. Native outputs are ABI-specific;
+# WSL can use CLZDIR=build/classes-linux to keep its Java build separate too.
+.PHONY: tictactoe
+tictactoe: $(TICTACTOE)
+
+$(TICTACTOE): docs/examples/TicTacToe.smp $(CLZDIR)/test/.ttag $(RELEASE_SYS) $(RELEASE_CRT) Makefile
+ @echo "building " $@
+ @$(JVM) com.seaofnodes.simple.Simple --link --cpu $(CPU_PORT) --abi $(CALL_CONV) -L $(dir $(RELEASE_SYS)) -o $(GAME_DIR)/TicTacToe.o $<
$(TEST_SYS): $(CLZDIR)/main/.mtag $(simple_driver) $(SYS_SMPS) | $(CLZDIR)/test/.ttag
@echo "simping " $@
@@ -167,7 +179,11 @@ $(RELEASE_SYS): $(OBJS)/lib_$(CPU_ABI)/sys.o
@cp $< $@
@cp -r $(dir $<)sys $(dir $@)
-$(RELEASE_CRT): src/main/c/simple_crt.c
+$(RELEASE_ARGS): src/main/smp/sys/rt/simple_args.smp $(CLZDIR)/main/.mtag $(simple_driver) Makefile | $(CLZDIR)/test/.ttag
+ @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
@echo "compiling " $@
@[ -d $(dir $@) ] || mkdir -p $(dir $@)
@gcc -c -O2 -DCALL_CONV=$(C_CALL_CONV) -o $@ $<
diff --git a/chapter30/README.ja.md b/chapter30/README.ja.md
index 167b4ade..9e0771dc 100644
--- a/chapter30/README.ja.md
+++ b/chapter30/README.ja.md
@@ -1,8 +1,35 @@
-# 第30章:作業中
+# 第30章:テキスト整形と診断
[English](README.md) | 日本語
[前の章:第29章 — 技術的負債の整理](../chapter29/README.ja.md)
-この章は第29章の完成したコンパイラを出発点としています。次のテーマは未定です。
-詳細とビルド手順は[英語版](README.md)を参照してください。
+ASCII の[三目並べ](docs/examples/TicTacToe.smp)を題材に、コンパイラと
+標準ライブラリを改善します。ゲーム自体ではなく、実装中に必要になった
+言語機能、診断、ランタイムが章のテーマです。実行可能な例は
+[`docs/examples`](docs/examples/)にあります。
+
+- `sys.io.getch()` による Enter 不要の1バイト入力と、端末モードの復元。
+- EOFまでの可変長読み込みと、部分書き込み・割り込みの再試行。
+ 読み込みエラーは `null`、書き込みエラーは `-1` と errno で通知します。
+ `aryu8` と `aryi64` の長さは `u32` とし、拡張時の桁あふれを防ぎます。
+- `file:line:column`、ソース行、位置マーカーを含む構文・型エラーの診断。
+- `int` を `i32` の別名に変更。型推論と中間演算の規則は維持し、
+ 代入時に32ビットへ縮小します。ポインタの幅は変わりません。
+- `"CPU plays %(reply+1).\n"` のような文字列補間と、`sys.text` の型別変換。
+ 整数などの整形は Simple で実装し、浮動小数点変換だけ C を呼び出します。
+- `sys.rt.simple_args` がネイティブの起動引数を Simple の配列へコピー。
+- 関数ごとのレジスタ割り当てと再試行。レジスタを使わない依存を生存解析から
+ 除外し、レジスタ破壊をまたぐ必要な転送を分割時に保持します。
+
+リポジトリのルートからゲームをビルドします。
+
+```sh
+make -C chapter30 tictactoe CTAGS=
+```
+
+Windows では `chapter30/build/examples/x86_64_v2_win64/TicTacToe.exe`、
+Linux では `chapter30/build/examples/x86_64_v2_SystemV/TicTacToe` を実行します。
+詳しい書式指定、テスト、WSL でのビルドは[英語版](README.md)を、
+バックエンドなどの未完了の作業は[ロードマップ](../docs/roadmap.ja.md)
+を参照してください。
diff --git a/chapter30/README.md b/chapter30/README.md
index 843ccc69..3e27c874 100644
--- a/chapter30/README.md
+++ b/chapter30/README.md
@@ -1,12 +1,209 @@
-# Chapter 30: Work in Progress
+# Chapter 30: Text Formatting and Diagnostics
English | [日本語](README.ja.md)
[Previous: Chapter 29 — Tech Debt Cleanup](../chapter29/README.md)
-This chapter starts from the completed Chapter 29 compiler. Its next topic is
-not yet selected. The parser organization, constructor checks, partial field
-views, and regression tests are inherited unchanged.
+This chapter uses a small game to drive improvements in the compiler and the
+`sys` standard library. The game provides a concrete workload; the chapter's
+subject is the language and library work uncovered.
+
+The first program is ASCII [tic-tac-toe](docs/examples/TicTacToe.smp).
+The player moves first with single keystrokes; the CPU responds, occupied
+squares are rejected, and play ends with a win or draw.
+Runnable programs, including sorting, file I/O, and shortest-path examples,
+live in [`docs/examples`](docs/examples/).
+
+## Terminal input and diagnostics
+
+[`sys.io.getch()`](src/main/smp/sys/io.smp) reads one byte without waiting for
+Enter or echoing it. A C adapter changes and restores the terminal mode;
+redirected files and pipes also work. It returns `0..255`, or `-1` with
+`sys.libc.errno()` set to `sys.libc.EOF` or an OS error. Escape and UTF-8
+sequences arrive as multiple bytes.
+
+The game echoes accepted moves, ignores Enter, and quits on `q`, Escape,
+Ctrl-C, Ctrl-D, or end of input.
+
+We also exposed a parser bug: branch guards from one function could affect the
+next function. Function boundaries now isolate those guards. Loop-path merges
+also preserve forward references discovered after a break or continue.
+
+The CLI reports parser and type errors as `file:line:column: error: message`,
+followed by the source line and an underscore-and-caret pointer - suitable for
+older text editors (VI, Emacs). Language errors now exit with status 1 instead
+of throwing an exception; unexpected compiler errors keep their Java stack
+traces. Quite a few error reporting points are cleaned up: malformed numbers,
+invalid escapes, unterminated comments or quoted text are a few.
+
+## Stream I/O and resizable arrays
+
+`sys.io.read(fd)` grows its buffer and continues through short reads until
+EOF. It returns an exact-sized byte array, empty for immediate EOF, or `null`
+on error with `sys.libc.errno()` preserved. Partial input is discarded on an
+error. `stdin()` uses the same behavior.
+
+`write`, `writeOff`, and `aryu8.write` write the complete requested data,
+retrying partial writes. Reads and writes retry `EINTR`; other errors return
+to the caller. Writes return a 64-bit byte count, or `-1` with errno set; a
+failure can follow a successfully written prefix. A zero-progress write
+reports `EIO`. `writeOff` rejects offsets past the array end with `EINVAL`,
+caps the length at the remaining bytes, and accepts empty slices at the end.
+
+The buffering and retry loops are Simple code. C supplies access to errno and
+its platform constants; the native read/write bindings use 64-bit counts and
+results on the supported targets. Tests cover files, pipes, empty and large
+inputs, and injected short transfers, interruptions, and failures.
+
+`aryu8` and `aryi64` store their lengths as `u32`, matching built-in array
+lengths. Growth handles empty backing arrays, uses existing capacity when it
+fits exactly, and calculates the next capacity in 64-bit arithmetic before
+narrowing. Requests beyond the `u32` length limit are rejected: stream reads
+return `null` with `EOVERFLOW`, while direct resizable-array growth exits with
+status 1. The tests check arithmetic at the unsigned boundary without allocating
+multi-gigabyte buffers.
+
+## Default integer type
+
+`int` now aliases `i32`. Its fields and array elements occupy four bytes;
+assignments and updates use the existing signed 32-bit narrowing rules.
+Explicit `i64` remains available for wide arithmetic and native addresses.
+Literal inference, `var`/`val` inference, and intermediate arithmetic retain
+their existing rules: `int x=2147483647; return x+1;` produces `2147483648`,
+whereas assigning that sum back to `x` produces `-2147483648`.
+
+The standard library spells out `i64` where it needs full-width formatting or
+native addresses/sizes. Resizable-array lengths, growth counts, and indices
+use explicit `u32`.
+
+Function entries normalize narrow signed/unsigned parameters, including
+stack arguments, using the backend extension operations. This also applies
+to internal Simple calls. Loop type widening preserves signed and unsigned
+32-bit bounds of values narrowed on every assignment.
+
+## Strings and formatting
+
+Strings can embed expressions from the surrounding scope:
+
+```java
+sys.io.p("CPU plays %(reply+1).\n");
+sys.io.p("score=%04d(score), average=%.2f(average)\n");
+sys.io.p("name=%-12.8s(name), key=%c(key)\n");
+```
+
+`%(expression)` selects decimal integer, floating-point, or byte-string
+formatting from the expression's type. Booleans are integer aliases, so the
+default prints `0` or `1`; `%b(expression)` prints `false` or `true`.
+Expressions execute once, from left to right. Each value is formatted before
+evaluating the next expression. The result is an ordinary immutable `u8[~]`
+with its exact byte length. Strings without embedded expressions remain
+constants.
+
+An explicit format has the form `%[flags][width][.precision]kind(expression)`:
+
+| Kind | Argument and output |
+| --- | --- |
+| `d`, `i` | Integer, signed decimal |
+| `u`, `o`, `x`, `X` | Integer, unsigned decimal, octal, or hexadecimal; negative values use their 64-bit representation |
+| `f`, `F`, `e`, `E`, `g`, `G`, `a`, `A` | Float, using the corresponding C format |
+| `c` | Integer, one byte from its low eight bits |
+| `s` | Byte string, including embedded zero bytes |
+| `b` | Integer, `false` for zero and `true` otherwise |
+
+Flags are `-` (left alignment), `+` (explicit sign), space (positive sign
+padding), `#` (alternate form), and `0` (zero padding), where applicable.
+Width and precision are literal decimal integers. String precision limits
+bytes; integer precision supplies a minimum digit count. `c` and `b` allow
+width and left alignment, but no precision. Default floating-point formatting
+uses `g` with 17 significant digits; explicit floating formats use C defaults
+unless a precision is given. Positional arguments, `*` widths, C length
+modifiers, and `%n` are not supported.
+
+Write `%%` or `\%` for a literal percent. Every other `%` must introduce a
+complete format with parentheses. Inside those parentheses the normal
+expression parser resumes, so nested strings work directly:
+
+```java
+val text = "outer %("inner %(value)")";
+```
+
+[`sys.text`](src/main/smp/sys/text.smp) provides explicitly named converters:
+`int`, `flt`, `f32`, `f64`, `char`, `bool`, and `hex`. For example,
+`sys.text.int(17)` returns `"17"`. Primitive type names are allowed as member
+names and in member declarations; they remain reserved for local variables and
+parameters. `sys.io.p` continues to require a byte string.
+
+`Strings` builds ordinary calls to `sys.text`, with normal module lookup,
+class initialization, and control/memory edges. A small `FormatNode`
+selects the default typed formatter as normal type flow resolves an expression,
+including forward references; it folds away in valid programs.
+
+The formatting library is written in Simple: `begin` creates an `aryu8`,
+`integer` and `string` append formatted values, and `end` copies the populated
+bytes into the immutable result. The integer formatter covers signed and
+unsigned 64-bit values, including minimum `i64`, and handles signs, prefixes,
+width, and precision. Character and boolean formatting use the same builder.
+`aryu8.num` shares the integer formatter, including its full-range handling.
+Only binary floating-point conversion calls C, through one fixed-signature
+adapter returning a byte string. There is no boxing or variadic Simple call.
+
+`sys.text` uses no interpolated strings, including in its named converters, so
+the library builds from source without a staged bootstrap. Non-float
+formatting can also run in the IR evaluator. The lexer shares its string and
+format scanner with deferred constructor-body and loop-update scanning.
+
+## Runtime support in Simple
+
+Native startup also uses Simple: [`simple_args.smp`](src/main/smp/sys/rt/simple_args.smp)
+allocates the argument array and copies each argument into a byte array. The
+C entry adapter calls this compiled routine before `simple_main`; C supplies
+only access to `argv[i]`, `strlen`, and `memcpy`. The converter fills every
+nullable array slot before publishing the result as `u8[~][]`, including the
+program name at index zero. It builds independently of `sys`, so even programs
+without standard-library imports use it.
+
+The `sys.rt` module holds Run Time support, like Java's `rt.jar` or C's `crt.o`.
+Make emits `simple_args.o` alongside `simple_crt.o`; the native linker driver
+includes both runtime objects automatically.
+
+## Register allocation
+
+The allocator handles each function independently, retaining its completed
+register assignments while reusing scratch space for the next function.
+Building live ranges, constructing interferences, coalescing, coloring, and
+splitting all run on that function's scheduled blocks. The eight-round limit
+applies per function. A hard register-mask conflict in one function therefore
+cannot prevent another function from reaching coloring or consume its retry
+budget.
+
+Two changes avoid unnecessary retries. First, liveness ignores inputs whose
+operand register mask is null: these are metadata or scheduling dependencies,
+not runtime register uses. In particular, a static call's unused hidden class
+argument must not keep its class-pointer constant live across other calls and
+trigger rematerialization.
+
+Second, splitting retains both transfers when a value must move out of a fixed
+register before a kill and back into it afterward. A buggy attempt at early
+coalescing would undo a split needed to dodge the kill. Interference-aware
+coalescing decides when they can be joined.
+
+Loop self-conflicts initially try cold transfers. If that does not separate
+the ranges, the fallback also splits backedge inputs. Distinct backedge nodes
+can still share a range through two-address instructions; copying only the Phi
+outputs leaves that conflict intact.
+
+The spill-ranking heuristic and calling conventions are unchanged. Saving a
+callee-save register can still require several coloring attempts as pressure
+changes. The [allocator regressions](src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java)
+check function independence in both scheduling orders, preservation across
+register kills, and dependencies that do not consume registers.
+
+## Related follow-up work
+
+[Narrow integer instruction selection](../docs/roadmap.md#integer-instruction-widths)
+and [per-function calling conventions](../docs/roadmap.md#per-function-calling-conventions)
+remain separate backend projects. C pointer types and compact references are
+also deferred; the `int` change does not shrink pointers.
The [grammar and code map](docs/30-grammar.md),
[source layout](src/main/java/com/seaofnodes/simple/README.md), and
@@ -21,6 +218,22 @@ From the repository root:
make lib tests release CHAPTERS=chapter30 CTAGS=
```
+Build the game without starting it:
+
+```sh
+make -C chapter30 tictactoe CTAGS=
+```
+
+The Windows/Cygwin build produces
+`chapter30/build/examples/x86_64_v2_win64/TicTacToe.exe`, which can be launched
+from Cygwin or PowerShell. Linux produces
+`chapter30/build/examples/x86_64_v2_SystemV/TicTacToe`.
+When building in WSL alongside Windows, keep Java build outputs separate:
+
+```sh
+make -C chapter30 tictactoe CLZDIR=build/classes-linux CTAGS=
+```
+
IntelliJ has a `chapter30` module and a **chapter30 tests** run configuration.
Build the native runtime and `sys` library with Make before debugging tests
that need them; see [the IntelliJ setup](../docs/intellij.md).
diff --git a/chapter30/ainotes.txt b/chapter30/ainotes.txt
deleted file mode 100644
index 6dd4b14f..00000000
--- a/chapter30/ainotes.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-
-Historical design notes from the former combined Chapter 25, carried forward from Chapter 28b.
-The cleanup record is in ../skills/stage-audit.md; remaining work is in
-../docs/roadmap.md. The original intent below is preserved.
-
-Meta-info: I intend to keep extending and re-editing this "ainotes.txt" file so
-I can reprompt the you (the AI) between AI restarts, As such this file will
-contain a mix of high-level design decisions, goals, intents and wisdom, and
-then taper down to more detailed problems and solutions - which probably keep
-changing.
-
-AI is allowed access to all files recursively in "C:/Users/cliffc/Desktop/Simple"
-directory.
-
-Meta-info: all the tests run with `make tests` currently run, and should again
-run before we accept any changes.
-
-
-The overall project is rooted in the `Simple/` directory,
-and is a compiler tutorial broken up into chapters.
-The chapters build on each other.
-
-Read the code in this chapter25; the goal in this chapter is to allow separate
-compilation. I want to compile a "standard library", the start of which is in
-`src/main/smp/sys.smp` and subdirectories and end up with either an archive or
-a "fat ELF .o" file.
-
-Then I want to compile a 1-liner "hello world" program:
- `sys.io.p("Hello World!");`
-and have the symbols `sys`, `io` and `p` be found in the standard library.
-
-I am using a test case to represent this seperate compilation strategy in
-`Chapter25Test.testModules`. There are comments there to help drive my
-thinking.
diff --git a/chapter30/docs/30-grammar.md b/chapter30/docs/30-grammar.md
index d65abe60..3bddfb87 100644
--- a/chapter30/docs/30-grammar.md
+++ b/chapter30/docs/30-grammar.md
@@ -8,6 +8,9 @@ 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.
+
```antlrv4
grammar SimpleLanguage;
@@ -58,11 +61,14 @@ constructorParameter : type '^'? bindingModifier? IDENTIFIER ;
// 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 ';' ;
+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
@@ -132,17 +138,17 @@ unaryExpression
// and array elements. All updates require an assignable operand.
updateOperator : '++' | '--' ;
postfixExpression : primaryExpression postfixSuffix* updateOperator? ;
-postfixSuffix : '.' IDENTIFIER | '[' assignment ']' | '#' | callSuffix | methodCallSuffix ;
+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 : '.' IDENTIFIER callSuffix ;
+methodCallSuffix : '.' memberName callSuffix ;
primaryExpression
: INTEGER_LITERAL
| FLOAT_LITERAL
- | STRING_LITERAL
+ | stringExpression
| CHARACTER_LITERAL
| 'true'
| 'false'
@@ -177,9 +183,17 @@ INTEGER_LITERAL : '0' | [1-9] 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_LITERAL : '"' (ESCAPE | ~["\\])* '"' ;
+// String text uses a lexical mode: whitespace/comments are literal here.
+// Interpolation re-enters the ordinary expression grammar, recursively.
+stringExpression : '"' (STRING_TEXT | ESCAPE | '%%' | interpolation)* '"' ;
+interpolation : '%' format? '(' assignment ')' ;
+format : FORMAT_FLAG* FORMAT_WIDTH? ('.' FORMAT_WIDTH?)? FORMAT_KIND ;
+// Flags: - + space # 0. Width/precision: decimal digits, no expressions.
+// Kinds: d i u o x X f F e E g G a A c s b.
+// Duplicate/inapplicable flags and precision on c/b are rejected.
+// STRING_TEXT excludes double quote, backslash, and percent.
CHARACTER_LITERAL : '\'' (ESCAPE | ~['\\]) '\'' ;
-fragment ESCAPE : '\\' [0ntr\\'"] ;
+fragment ESCAPE : '\\' [0ntr\\'"%] ;
IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ;
fragment NON_DIGIT : [a-zA-Z_] ;
fragment DIGIT : [0-9] ;
@@ -206,6 +220,7 @@ there is no inheritance chain, AST, or additional compiler pass.
| `postfixExpression`, member selection and updates | `Expressions.parsePostfix` / `parsePostfixMethod` dispatch to [`References.parseField`](../src/main/java/com/seaofnodes/simple/parser/References.java); `References.parseName` handles bare names, implicit receivers, and forward references |
| `functionExpression`, `parameterList`, calls, `argumentList` | [`Functions`](../src/main/java/com/seaofnodes/simple/parser/Functions.java): `parseFunction`, `parseConstructor`, shared `parameters` and `arguments`, then `parseBody` |
| `newExpression` | [`Allocations.parseNew`](../src/main/java/com/seaofnodes/simple/parser/Allocations.java); the allocation-helper graph builder follows the expression routines |
+| `stringExpression`, `interpolation` | [`Strings.parse`](../src/main/java/com/seaofnodes/simple/parser/Strings.java), using `Lexer.stringPart` / `stringFormat`; normal expression parsing resumes inside each interpolation |
| Literals, identifiers, comments, token matching | [`Lexer`](../src/main/java/com/seaofnodes/simple/parser/Lexer.java); it also supplies source-location snapshots used by diagnostics and serialized IR |
`ControlFlow` shares guarded scope merging between `if`, `?:`, `&&`, and `||`.
diff --git a/chapter30/src/test/smp/examples/A_helloWorld.smp b/chapter30/docs/examples/A_helloWorld.smp
index 4ed6abdf..4ed6abdf 100644
--- a/chapter30/src/test/smp/examples/A_helloWorld.smp
+++ b/chapter30/docs/examples/A_helloWorld.smp
diff --git a/chapter30/src/test/smp/examples/BubbleSort.smp b/chapter30/docs/examples/BubbleSort.smp
index ca6906b5..6bdb13a9 100644
--- a/chapter30/src/test/smp/examples/BubbleSort.smp
+++ b/chapter30/docs/examples/BubbleSort.smp
@@ -8,9 +8,11 @@ val sort = { !sys.aryi64 nums ->
sys.aryi64?! more = nums;
while( more ) {
more = null;
- for( var i = 0; i < nums.len-1; i++ )
- if( nums.buf[i] > nums.buf[i+1] )
- more = nums.swap(i,i+1);
+ 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);
+ }
}
};
diff --git a/chapter30/src/test/smp/examples/Capitalize.smp b/chapter30/docs/examples/Capitalize.smp
index 8e31c18f..00fdd481 100644
--- a/chapter30/src/test/smp/examples/Capitalize.smp
+++ b/chapter30/docs/examples/Capitalize.smp
@@ -14,7 +14,8 @@ val main = { u8[~][] args ->
u8[~] input = args[1];
var output = new u8[input# + 1];
- sys.ary.copy(output,0,input,0,input#);
+ 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);
diff --git a/chapter30/src/test/smp/examples/Dijkstra.smp b/chapter30/docs/examples/Dijkstra.smp
index fd595eb5..768bdd08 100644
--- a/chapter30/src/test/smp/examples/Dijkstra.smp
+++ b/chapter30/docs/examples/Dijkstra.smp
@@ -26,7 +26,8 @@ val shortestPath = { !sys.aryi64 matrix, int nodes, int src, int dst ->
if( nearest == dst ) return best;
visited.set(nearest);
for( int node=0; node<nodes; node++ ) {
- i64 weight = matrix.get(nearest*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;
}
@@ -49,7 +50,7 @@ val main = { u8[~][] args ->
if( !matrix ) return usage();
if( !matrix.len ) return usage();
// All numbers are positive
- for( int i=0; i<matrix.len; i++ )
+ for( u32 i=0; i<matrix.len; i++ )
if( matrix.get(i) < 0 ) return usage();
// Matrix is square
int nodes = 0;
@@ -63,7 +64,8 @@ val main = { u8[~][] args ->
if( src < 0 || src >= nodes || dst < 0 || dst >= nodes )
return usage(); // Source and destination land within the graph
- i64 cost = shortestPath(matrix,nodes,src,dst);
+ 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;
diff --git a/chapter30/src/test/smp/examples/FileIO.smp b/chapter30/docs/examples/FileIO.smp
index e48d435b..59b3159e 100644
--- a/chapter30/src/test/smp/examples/FileIO.smp
+++ b/chapter30/docs/examples/FileIO.smp
@@ -15,7 +15,8 @@ val main = { u8[~][] args ->
i32 fd = sys.io.create(FILE);
if( fd == -1 ) return _error();
// Write
- int wrote = sys.io.writeOff(fd,str.buf,0,str.len);
+ 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
@@ -26,12 +27,13 @@ val main = { u8[~][] args ->
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,str.len) )
+ if( sys.ary.cmp(got,0,str.buf,0,len) )
return _error2(-4);
// Remove
diff --git a/chapter30/docs/examples/README.md b/chapter30/docs/examples/README.md
new file mode 100644
index 00000000..767cdc3e
--- /dev/null
+++ b/chapter30/docs/examples/README.md
@@ -0,0 +1,71 @@
+# Runnable examples
+
+These programs live beside the documentation so readers can find and run them.
+Their native regression tests still run as part of `make tests`.
+
+From `chapter30`, 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 `chapter30`
+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/chapter30/docs/examples/TicTacToe.smp b/chapter30/docs/examples/TicTacToe.smp
new file mode 100644
index 00000000..a710e440
--- /dev/null
+++ b/chapter30/docs/examples/TicTacToe.smp
@@ -0,0 +1,133 @@
+// Player is X, CPU is O. Squares are numbered left-to-right, top-to-bottom.
+val main = { u8[~][] args ->
+ sys.io.p("Tic-tac-toe!\nYou are X; the CPU is O. You go first.\n");
+ sys.io.p("Press a square's number (no Enter needed), or q/Esc to quit.\n");
+ sys.io.p("Squares:\n 1 | 2 | 3 \n---+---+---\n 4 | 5 | 6 \n---+---+---\n 7 | 8 | 9 \n\n");
+ val board = new u8[9];
+ for( int i=0; i<board#; i++ ) board[i]=' ';
+ // Draw empty board
+ _draw(board);
+
+ while( true ) {
+ sys.io.p("Your move (1-9): ");
+ // Valid user move 1-9, or 0 for quitting
+ int square = _validMove(board) - 1;
+ if( square < 0 ) return 0;
+ // Set user choice; print board
+ board[square] = 'X';
+ sys.io.p("\n");
+ _draw(board);
+ // A winner?
+ if( _finished(board) ) return 0;
+ // Compute CPU move
+ int reply = _cpu(board);
+ // Set CPU choice; print board
+ board[reply]='O';
+ sys.io.p("CPU plays %(reply+1).\n");
+ _draw(board);
+ // A winner?
+ if( _finished(board) ) return 0;
+ }
+};
+
+// Return with a valid user move 1-9, or 0 to exit
+val _validMove = { u8[~] board ->
+ while( true ) {
+ int key = sys.io.getch();
+ if( key == -1 && sys.libc.errno() != sys.libc.EOF ) {
+ sys.io.err("\nUnable to read the keyboard.\n");
+ return 0;
+ }
+ // Various quitting conditions
+ if( key == -1 || key == 'q' || key == 'Q' || key == 27 || key == 3 || key == 4 ) {
+ sys.io.p("\nGoodbye!\n");
+ return 0;
+ }
+ // Ignore CR/LF
+ if( key == '\r' || key == '\n' ) continue;
+
+ // Valid choice
+ if( key < '1' || key > '9' ) {
+ sys.io.p("\nChoose a square from 1 to 9: ");
+ continue;
+ }
+ if( board[key-'1'] != ' ' ) {
+ sys.io.p("\nThat square is occupied. Your move (1-9): ");
+ continue;
+ }
+ sys.io.p("%c(key)");
+ return key-'0';
+ }
+};
+
+// Return the winning mark, or zero.
+val _winner = { u8[~] board ->
+ for( int i=0; i<3; i++ ) {
+ int row = i*3;
+ if( board[row]!=' ' && board[row]==board[row+1] && board[row]==board[row+2] )
+ return board[row];
+ if( board[i]!=' ' && board[i]==board[i+3] && board[i]==board[i+6] )
+ return board[i];
+ }
+ if( board[4]!=' ' ) {
+ if( board[0]==board[4] && board[4]==board[8] ) return board[4];
+ if( board[2]==board[4] && board[4]==board[6] ) return board[4];
+ }
+ return 0;
+};
+
+val _finished = { u8[~] board ->
+ int winner = _winner(board);
+ if( winner=='X' ) { sys.io.p("You win!\n"); return true; }
+ if( winner=='O' ) { sys.io.p("CPU wins!\n"); return true; }
+ for( int i=0; i<board#; i++ )
+ if( board[i]==' ' ) return false;
+ sys.io.p("It's a draw!\n");
+ return true;
+};
+
+// Try each empty square, restoring it before returning.
+val _threat = { u8[] board, u8 mark ->
+ for( int i=0; i<board#; i++ ) {
+ if( board[i]==' ' ) {
+ board[i]=mark;
+ bool wins = _winner(board)==mark;
+ board[i]=' '; // Restore board
+ if( wins ) return i;
+ }
+ }
+ return -1;
+};
+
+// Win if possible, otherwise block, then prefer center, corners, and edges.
+// Called only after _finished has ruled out a full board.
+val _cpu = { u8[] board ->
+ // If O can win, do so
+ int win = _threat(board,'O');
+ if( win>=0 ) return win;
+ // If X will win next move, block
+ int block = _threat(board,'X');
+ if( block>=0 ) return block;
+ // If center is open, take it
+ if( board[4]==' ' ) return 4;
+ // If a corner is open, take it
+ for( int i=0; i<board#; i+=2 )
+ if( board[i]==' ' ) return i;
+ // Take whatever is available
+ for( int i=0; i<board#; i++ )
+ if( board[i]==' ' ) return i;
+ return -1;
+};
+
+val _draw = { u8[~] board ->
+ val out = new sys.aryu8();
+ for( int row=0; row<3; row++ ) {
+ for( int col=0; col<3; col++ ) {
+ out.add(' ').add(board[row*3+col]).add(' ');
+ if( col<2 ) out.add('|');
+ }
+ out.add('\n');
+ if( row<2 ) out.addAry("---+---+---\n");
+ }
+ out.write(sys.libc.STDOUT);
+};
diff --git a/chapter30/src/main/c/simple_crt.c b/chapter30/src/main/c/simple_crt.c
index 8656a3cc..438ae5f9 100644
--- a/chapter30/src/main/c/simple_crt.c
+++ b/chapter30/src/main/c/simple_crt.c
@@ -1,58 +1,106 @@
#include <stdint.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
+#include <termios.h>
+#include <unistd.h>
// 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; }
+// Export platform errno constants; the buffering and retry loops are Simple.
+// C identifiers avoid the macros; linker names match the Simple bindings.
+int32_t simple_EINTR __asm__("EINTR") = EINTR;
+int32_t simple_EIO __asm__("EIO") = EIO;
+int32_t simple_EINVAL __asm__("EINVAL") = EINVAL;
+int32_t simple_EOVERFLOW __asm__("EOVERFLOW") = EOVERFLOW;
-// Native process arguments converted to ordinary Simple array layouts.
-// The outer array is mutable; every contained byte array is published as
-// immutable and non-null to Simple code.
+// Read one stdin byte, without terminal line editing or echo. POSIX terminals
+// include Cygwin consoles; redirected files/pipes need no mode change.
+// Return 0..255, or -1 with errno set to EOF or an OS error. Control characters,
+// including Ctrl-C, are returned as bytes so the mode is restored first.
+int32_t simple_getch(void) {
+ struct termios saved, mode;
+ int terminal = tcgetattr(STDIN_FILENO, &saved) == 0;
+ if (!terminal && errno != ENOTTY)
+ return -1;
+ if (terminal) {
+ mode = saved;
+ mode.c_lflag &= ~(ICANON | ECHO | ECHONL | ISIG | IEXTEN);
+ mode.c_iflag &= ~IXON;
+ mode.c_cc[VMIN] = 1;
+ mode.c_cc[VTIME] = 0;
+ if (tcsetattr(STDIN_FILENO, TCSANOW, &mode) != 0)
+ return -1;
+ }
+ unsigned char byte;
+ ssize_t count = read(STDIN_FILENO, &byte, 1);
+ int read_errno = errno;
+ if (terminal) {
+ int restored;
+ do {
+ restored = tcsetattr(STDIN_FILENO, TCSANOW, &saved);
+ } while (restored != 0 && errno == EINTR);
+ if (restored != 0)
+ return -1;
+ }
+ if (count < 0) {
+ errno = read_errno;
+ return -1;
+ }
+ if (count == 0) {
+ errno = EOF;
+ return -1;
+ }
+ return byte;
+}
+
+// Simple byte-array layout, used only by the floating-point formatter.
typedef struct SimpleBytes {
uint32_t len;
unsigned char bytes[];
} SimpleBytes;
-typedef struct SimpleArgs {
- uint32_t len;
- uint32_t pad;
- SimpleBytes *args[];
-} SimpleArgs;
+// Binary floating-point conversion is the only native formatting primitive.
+// Buffering, integer conversion, string precision and padding live in sys.text.
+SimpleBytes *simple_text_float(double value, int64_t spec,
+ int64_t width, int64_t precision) {
+ char kind = spec & 255;
+ if (!kind) { kind = 'g'; precision = 17; }
+ if (!strchr("fFeEgGaA", kind) || (spec & ~8191LL) ||
+ width < 0 || width > INT32_MAX || precision < -1 || precision > INT32_MAX)
+ abort();
+ char fmt[64], *p = fmt;
+ *p++ = '%';
+ const char *flags = "-+ #0";
+ for (int i=0; i<5; i++) if (spec & (256LL<<i)) *p++ = flags[i];
+ if (width) p += sprintf(p, "%d", (int)width);
+ if (precision >= 0) p += sprintf(p, ".%d", (int)precision);
+ *p++ = kind;
+ *p = 0;
+ int len = snprintf(NULL, 0, fmt, value);
+ if (len < 0) abort();
+ SimpleBytes *str = malloc(sizeof(*str) + (size_t)len + 1);
+ if (!str) abort();
+ str->len = (uint32_t)len;
+ snprintf((char *)str->bytes, (size_t)len + 1, fmt, value);
+ return str;
+}
+// Simple does not yet dereference raw native pointers. Keep just the argv
+// element access here; allocation and array construction live in sys/rt/simple_args.smp.
+const char *simple_argv(char **argv, int64_t index) {
+ return argv[index];
+}
__attribute__((CALL_CONV))
-extern int64_t simple_main(SimpleArgs *args);
-
-static SimpleArgs *simple_args(int argc, char **argv) {
- if (argc < 0 || (uint64_t)argc > UINT32_MAX)
- return NULL;
-
- SimpleArgs *args = calloc(1, sizeof(*args) + (size_t)argc * sizeof(args->args[0]));
- if (args == NULL)
- return NULL;
- args->len = (uint32_t)argc;
-
- for (int i = 0; i < argc; i++) {
- size_t len = strlen(argv[i]);
- if (len > UINT32_MAX) {
- free(args);
- return NULL;
- }
- SimpleBytes *str = malloc(sizeof(*str) + len);
- if (str == NULL) {
- for (int j = 0; j < i; j++) free(args->args[j]);
- free(args);
- return NULL;
- }
- str->len = (uint32_t)len;
- memcpy(str->bytes, argv[i], len);
- args->args[i] = str;
- }
- return args;
-}
+extern void *simple_args(int64_t argc, char **argv);
+__attribute__((CALL_CONV))
+extern int64_t simple_main(void *args);
int main(int argc, char **argv) {
- SimpleArgs *args = simple_args(argc, argv);
+ // simple_args fills every slot before publishing this as u8[~][].
+ void *args = simple_args(argc, argv);
if (args == NULL)
return 1;
// The language-level hidden receiver is not part of the emitted ABI for
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java b/chapter30/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java
index 0b314978..8d0237ea 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java
@@ -16,7 +16,7 @@ abstract public class BuildLRG {
// true if no hard-conflicts, although we still might not color.
public static boolean run(int round, RegAlloc alloc) {
FunNode fun = null;
- for( Node bb : alloc._code._cfg ) {
+ for( Node bb : alloc._cfg ) {
if( bb instanceof FunNode fun2 )
fun = fun2;
for( Node n : bb.outs() ) {
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java b/chapter30/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java
index 689ceceb..dbde2f8b 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java
@@ -13,7 +13,7 @@ abstract public class Coalesce {
// Walk all the splits, looking for coalesce chances
boolean progress = false;
- for( CFGNode bb : alloc._code._cfg ) {
+ for( CFGNode bb : alloc._cfg ) {
outer:
for( int j=0; j<bb.nOuts(); j++ ) {
Node n = bb.out(j);
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/codegen/IFG.java b/chapter30/src/main/java/com/seaofnodes/simple/codegen/IFG.java
index d15e66da..6ad7c0ad 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/codegen/IFG.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/codegen/IFG.java
@@ -72,7 +72,7 @@ abstract public class IFG {
WORK.clear();
// Last block has nothing live out
- for( CFGNode bb : alloc._code._cfg )
+ for( CFGNode bb : alloc._cfg )
if( bb.blockHead())
WORK.push( bb );
@@ -164,6 +164,10 @@ abstract public class IFG {
// Also record use-side spills for biased coloring.
// Then make all inputs live.
for( int i=1; i<n.nIns(); i++ ) {
+ // Metadata and scheduling dependencies do not consume registers.
+ // In particular, a static call's hidden class argument has no ABI
+ // location and must not keep its constant live across other calls.
+ if( ((MachNode)n).regmap(i)==null ) continue;
Node def = n.in(i);
if( def==null ) continue;
LRG lrg1 = alloc.lrg(def);
@@ -480,7 +484,7 @@ abstract public class IFG {
private static int functionArea(FunNode fun, RegAlloc alloc) {
int area=0;
- for( CFGNode bb : alloc._code._cfg )
+ for( CFGNode bb : alloc._cfg )
if( bb.fun()==fun )
area += bb.nOuts();
return area;
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java b/chapter30/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java
index 5fc002a1..2595b4ad 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java
@@ -85,13 +85,7 @@ class InstSelect {
}
// Produce a machine node from n; map it to flag as done so stops cycles.
- if( n instanceof MemOpNode mem && mem.off() instanceof ExternOffsetNode off ) {
- // External fields replace base+offset with symbol+0. Keep the ideal
- // declaring type on the address for machine load/store width selection.
- TypeMemPtr ptr = (TypeMemPtr)mem.ptr()._type;
- n._inputs.set(2,new ExternNode(TypeMemPtr.make((byte)2,ptr._obj,true),off._extern));
- n._inputs.set(3,ConstantNode.raw(TypeInteger.ZERO));
- }
+ if( n instanceof MemOpNode mem ) Machine.externalAddress(mem);
map.put(n, x=_code._mach.instSelect(n) );
// Carry loop-tree and pre-order info across the ideal->mach transition
if( n instanceof CFGNode ncfg && x instanceof CFGNode xcfg ) {
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/codegen/Machine.java b/chapter30/src/main/java/com/seaofnodes/simple/codegen/Machine.java
index 3eee8fc3..6ccf6b19 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/codegen/Machine.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/codegen/Machine.java
@@ -3,6 +3,7 @@ package com.seaofnodes.simple.codegen;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeFunPtr;
import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeMemPtr;
abstract public class Machine {
// Human readable machine name. Something like "x86-64" or "arm" or "risc5"
@@ -46,6 +47,17 @@ abstract public class Machine {
// Instruction select from ideal nodes
public abstract Node instSelect( Node n );
+ // Lower external fields before selecting a memory operation, including
+ // loads absorbed into another instruction rather than selected separately.
+ public static void externalAddress(MemOpNode mem) {
+ if( mem.off() instanceof ExternOffsetNode off ) {
+ // Retain the declaring type for load/store width selection.
+ TypeMemPtr ptr = (TypeMemPtr)mem.ptr()._type;
+ mem._inputs.set(2,new ExternNode(TypeMemPtr.make((byte)2,ptr._obj,true),off._extern));
+ mem._inputs.set(3,ConstantNode.raw(TypeInteger.ZERO));
+ }
+ }
+
/** Stack Slot Numbering
RA gives all registers a number, starting at 0. Stack slots continue this
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java b/chapter30/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
index 5f8a0ec0..3306ff8f 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
@@ -140,7 +140,7 @@ public abstract class ParseAll {
fref._name = TypeStruct.addClzPrefix(xcunit._cname);
cunit.addDep(xcunit);
} else {
- throw new RuntimeException("Undefined name '" + fref._name +"'");
+ throw fref.err();
}
} catch( IOException ioe ) {
throw new RuntimeException(ioe);
@@ -297,14 +297,6 @@ public abstract class ParseAll {
Parser.TYPES.put(cunit._cname,ptr._obj);
}
- // Symbol not found in source code (or we would not be here).
- // Search the module for the fref
- public static CompUnit findCompUnitOrThrow(CodeGen code, CompUnit cu, String symbol) {
- CompUnit cu2 = findCompUnit(code,cu,symbol);
- if( cu2 != null ) return cu2;
- throw new RuntimeException("Undefined name '" + symbol +"'");
- }
-
public static CompUnit findCompUnit(CodeGen code, CompUnit cu, String symbol) {
// Symbol not found in source code (or we would not be here).
// Search the module for the fref
@@ -328,8 +320,15 @@ public abstract class ParseAll {
// However if A/B contains "Y", the search would be for A.B.Y then A.Y both of
// which do not exist.
private static CompUnit findCUnitModule( CodeGen code, CompUnit cunit, String symbol ) throws IOException {
- if( cunit == null ) // Ran off top of module scope?
- return null; // Failed search
+ if( cunit == null ) {
+ // The module root is also a namespace. In particular, a root
+ // client can resolve sys.text from source, without installed .o's.
+ String fname=symbol.replace('.','/');
+ if( !new File(code._modDir+"/"+fname+".smp").exists() ) return null;
+ CompUnit root=makeCUnit(code,null,fname);
+ if( root._clz==null && WORK.find(root)==-1 ) enqueue(root);
+ return root;
+ }
// Check self
if( cunit._name.equals(symbol) )
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java b/chapter30/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
index f1aa18ef..3991cd48 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
@@ -54,6 +54,8 @@ public class RegAlloc {
// Top-level program graph structure
final CodeGen _code;
+ // Scheduled blocks of the function currently being allocated.
+ Ary<CFGNode> _cfg;
public int _spills, _spillScaled;
@@ -161,7 +163,7 @@ public class RegAlloc {
}
// -----------------------
- RegAlloc( CodeGen code ) { _code = code; }
+ RegAlloc( CodeGen code ) { _code = code; _cfg = code._cfg; }
public void regAlloc() {
// Insert callee-save registers
@@ -186,18 +188,41 @@ public class RegAlloc {
// Expensive assert: useful when chasing cross-function allocator edges.
//assert verifyFunctionLocalEdges();
- // Top driver: repeated rounds of coloring and splitting.
- byte round=0;
- while( !graphColor(round) ) {
- split(round);
- if( round >= 7 ) // Really expect to be done soon
- throw new IllegalStateException("Register allocation made no progress after eight rounds");
- round++;
+ // Each function owns its retry budget. Keep completed assignments while
+ // reusing the live-range and interference scratch space for the next.
+ IdentityHashMap<Node,LRG> colored = new IdentityHashMap<>();
+ _cfg = new Ary<>(CFGNode.class);
+ for( CFGNode bb : _code._cfg ) {
+ if( bb instanceof FunNode && !_cfg.isEmpty() ) {
+ allocateFunction();
+ colored.putAll(_lrgs);
+ _cfg.clear();
+ }
+ _cfg.add(bb);
}
+ if( !_cfg.isEmpty() ) {
+ allocateFunction();
+ colored.putAll(_lrgs);
+ }
+ _lrgs.clear();
+ _lrgs.putAll(colored);
+ _cfg = _code._cfg;
postColor(); // Remove no-op spills
_done = true;
}
+ private void allocateFunction() {
+ _deferredLoopSelf.clear();
+ for( byte round=0; !graphColor(round); round++ ) {
+ split(round);
+ if( round >= 7 ) {
+ FunNode fun = _cfg.at(0).fun();
+ throw new IllegalStateException("Register allocation failed after eight rounds in "+
+ (fun==null ? "<root>" : fun.label()));
+ }
+ }
+ }
+
boolean verifyFunctionLocalEdges() {
for( CFGNode bb : _code._cfg )
for( Node use : bb._outputs ) {
@@ -477,7 +502,10 @@ public class RegAlloc {
split.killOrdered();
}
for( int i=1; i<(deferred.containsKey(phi) ? 2 : phi.nIns()); i++ )
- if( i==1 || sharedPhiInput(phi,i) )
+ // Backedges may be joined indirectly through two-address
+ // chains, even when their input nodes are distinct. After
+ // the cold-only attempt, isolate those inputs as well.
+ if( i==1 || phi.region() instanceof LoopNode || sharedPhiInput(phi,i) )
insertBefore(phi,i,"use/self/phi",round,lrg);
}
// Split before two-address ops which extend the live range
@@ -653,9 +681,8 @@ public class RegAlloc {
return;
}
makeSplit(def,kind,round,lrg,umask).insertBefore(n, i);
- // Skip split-of-split same block
- if( skip && def instanceof SplitNode && cfg==def.cfg0() )
- n.in(i).setDefOrdered(1,def.in(1));
+ // Keep the def-side transfer: a kill between the two copies may be
+ // exactly why this range split. Coalescing can join them when safe.
}
void insertBefore(Node n, int i, String kind, byte round, LRG lrg) {
insertBefore(n,i,kind,round,lrg,true);
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/codegen/SourceError.java b/chapter30/src/main/java/com/seaofnodes/simple/codegen/SourceError.java
index f2f46146..5be0585f 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/codegen/SourceError.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/codegen/SourceError.java
@@ -16,6 +16,10 @@ public abstract class SourceError {
source=cu._src!=null ? cu._src.getBytes(StandardCharsets.UTF_8)
: file==null ? null : Files.readAllBytes(Path.of(file));
} catch( IOException ex ) { source=null; }
+ return format(name,source,pos,message);
+ }
+
+ public static String format(String name, byte[] source, int pos, String message) {
if( source==null || pos<0 || pos>source.length )
return name+": error: "+message+" (source unavailable at byte offset "+pos+")";
@@ -28,22 +32,24 @@ public abstract class SourceError {
int point=Math.min(pos,end);
String before=new String(source,start,point-start,StandardCharsets.UTF_8);
String text=new String(source,start,end-start,StandardCharsets.UTF_8);
- int column=before.codePointCount(0,before.length())+1;
- SB sb=new SB().p(name).p(':').p(line).p(':').p(column).p(": error: ").p(message).p("\n ");
+ SB excerpt=new SB();
int width=0, caret=0;
for( int i=0; i<text.length(); ) {
if( i==before.length() ) caret=width;
int ch=text.codePointAt(i);
if( ch=='\t' ) {
int spaces=8-(width%8);
- for( int j=0; j<spaces; j++ ) sb.p(' ');
+ for( int j=0; j<spaces; j++ ) excerpt.p(' ');
width+=spaces;
- } else { sb.p(new String(Character.toChars(ch))); width++; }
+ } else { excerpt.p(new String(Character.toChars(ch))); width++; }
i+=Character.charCount(ch);
}
if( before.length()==text.length() ) caret=width;
+ // Emacs compilation mode defaults to display columns, with 8-column tabs.
+ SB sb=new SB().p(name).p(':').p(line).p(':').p(caret+1).p(": error: ").p(message)
+ .p("\n ").p(excerpt.toString());
sb.p("\n ");
- for( int i=0; i<caret; i++ ) sb.p('-');
+ for( int i=0; i<caret; i++ ) sb.p('_');
return sb.p('^').toString();
}
}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/CallNode.java b/chapter30/src/main/java/com/seaofnodes/simple/node/CallNode.java
index 91d27330..ac5e4eee 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/node/CallNode.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/CallNode.java
@@ -242,7 +242,7 @@ public class CallNode extends CFGNode {
if( !tfp.notNull() )
return Parser.error( "Might be null calling "+tfp, _loc);
if( nargs() != tfp.nargs() )
- return Parser.error( "Expecting "+tfp.nargs()+" arguments, but found "+nargs(), _loc);
+ return Parser.error( "Expecting "+(tfp.nargs()-1)+" arguments, but found "+(nargs()-1), _loc);
// Check for args
for( int i=0; i<tfp.nargs(); i++ )
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/FldOffNode.java b/chapter30/src/main/java/com/seaofnodes/simple/node/FldOffNode.java
index 09e5eac8..e49162f4 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/node/FldOffNode.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/FldOffNode.java
@@ -49,7 +49,7 @@ public class FldOffNode extends TypeNode {
@Override public Parser.ParseException err() {
if( in(1)._type instanceof TypeMemPtr tmp && tmp._obj._fref )
- return Parser.error("Unknown struct type '"+tmp._obj._name+"'",null);
+ return Parser.error("Unknown struct type '"+tmp._obj._name+"'",_loc);
return null;
}
}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/FormatNode.java b/chapter30/src/main/java/com/seaofnodes/simple/node/FormatNode.java
new file mode 100644
index 00000000..d34b342d
--- /dev/null
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/FormatNode.java
@@ -0,0 +1,33 @@
+package com.seaofnodes.simple.node;
+
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.type.*;
+
+/** Resolve default formatting through normal type flow, including forward
+ * references. Valid programs fold this selector before serialization/codegen. */
+public final class FormatNode extends Node {
+ private final Lexer _site;
+ public FormatNode(Node value, Node ints, Node floats, Node strings, Lexer site) {
+ super(null,value,ints,floats,strings);
+ _site=site;
+ }
+ @Override public String label() { return "Format"; }
+ private int choice() {
+ Type t=in(1)._type;
+ if( t instanceof TypeInteger ) return 2;
+ if( t instanceof TypeFloat ) return 3;
+ if( t instanceof TypeMemPtr p && !p.nullable() && p._obj.isAry() &&
+ p._obj._name.equals("[]u8") ) return 4;
+ return 0;
+ }
+ @Override public Type compute() {
+ if( in(1)._type.isHigh() ) return Type.TOP;
+ int c=choice();
+ return c==0 ? in(2)._type.meet(in(3)._type).meet(in(4)._type) : in(c)._type;
+ }
+ @Override public Node idealize() { int c=choice(); return c==0 ? null : in(c); }
+ @Override public Parser.ParseException err() {
+ return Parser.error("Cannot format "+in(1)._type+"; expected an integer, float, or byte string",_site);
+ }
+}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/chapter30/src/main/java/com/seaofnodes/simple/node/LoadNode.java
index 85ab29f9..eb78ed1f 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/node/LoadNode.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/LoadNode.java
@@ -395,6 +395,9 @@ public class LoadNode extends MemOpNode {
// load will need to zero/sign-extend.
private Node extend(Node val) {
if( !(declaredType() instanceof TypeInteger ti) ) return val;
+ // A typed load can already know its width while the stored value is
+ // still unresolved. Do not replace it with an untyped shift/and chain.
+ if( !(val._type instanceof TypeInteger) ) { addDep(val); return null; }
if( ti._min==0 ) // Unsigned
return new AndNode(null,val,con(ti._max));
// Signed extension
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter30/src/main/java/com/seaofnodes/simple/node/ScopeNode.java
index 1130cf44..80d2aa0a 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/node/ScopeNode.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/ScopeNode.java
@@ -638,6 +638,7 @@ public class ScopeNode extends MemMergeNode {
int i = _guards._len;
while( i > 0 ) {
Node cast = _guards.at(--i);
+ if( cast instanceof FunNode ) break; // Function boundary
if( cast instanceof CFGNode ) continue; // Marker between guard sets
Node xpred = _guards.at(--i);
if( xpred == pred )
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java
index 814a82c2..da9afe71 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java
@@ -6,14 +6,21 @@ import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.util.SB;
-// Convert a narrow C return register to Simple's full-width integer value.
+// Normalize narrow parameters and C return values to full-width integers.
public class ExtendARM extends MachConcreteNode {
final int _bits;
final boolean _signed;
ExtendARM(ProjNode prj, TypeInteger ret) {
- super(prj);
- _inputs.set(0,null);
- _inputs.add(new ProjARM(prj));
+ this(prj,new ProjARM(prj),ret);
+ }
+ ExtendARM(ParmNode parm, TypeInteger decl) {
+ this(parm,new ParmARM(parm),decl);
+ }
+ private ExtendARM(Node ideal, Node value, TypeInteger ret) {
+ super(ideal);
+ _inputs.clear();
+ _inputs.add(null);
+ _inputs.add(value);
_bits = 8 << ret.log_size();
_signed = ret._min < 0;
}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java
index 3c965da5..ee7b0fc3 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java
@@ -332,7 +332,8 @@ public class arm extends Machine {
case NewNode nnn -> new NewARM(nnn);
case NotNode not -> new NotARM(not);
case OrNode or -> or(or);
- case ParmNode parm -> new ParmARM(parm);
+ case ParmNode parm -> parm._declaredType instanceof TypeInteger ti && ti.log_size()<3
+ ? new ExtendARM(parm,ti) : new ParmARM(parm);
case BulkMemPhiNode phi -> new BulkMemPhiNode(phi);
case MemPhiNode phi -> new MemPhiNode(phi);
case PhiNode phi -> new PhiNode(phi);
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java
index 5bd3ec1c..11182133 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java
@@ -6,14 +6,21 @@ import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.util.SB;
-// Convert a narrow C return register to Simple's full-width integer value.
+// Normalize narrow parameters and C return values to full-width integers.
public class ExtendRISC extends MachConcreteNode {
final int _bits;
final boolean _signed;
ExtendRISC(ProjNode prj, TypeInteger ret) {
- super(prj);
- _inputs.set(0,null);
- _inputs.add(new ProjRISC(prj));
+ this(prj,new ProjRISC(prj),ret);
+ }
+ ExtendRISC(ParmNode parm, TypeInteger decl) {
+ this(parm,new ParmRISC(parm),decl);
+ }
+ private ExtendRISC(Node ideal, Node value, TypeInteger ret) {
+ super(ideal);
+ _inputs.clear();
+ _inputs.add(null);
+ _inputs.add(value);
_bits = 8 << ret.log_size();
_signed = ret._min < 0;
}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java
index 0195542c..378955a5 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java
@@ -236,7 +236,8 @@ public class riscv extends Machine {
case NewNode nnn -> nnn(nnn);
case NotNode not -> new NotRISC(not);
case OrNode or -> or(or);
- case ParmNode parm -> new ParmRISC(parm);
+ case ParmNode parm -> parm._declaredType instanceof TypeInteger ti && ti.log_size()<3
+ ? new ExtendRISC(parm,ti) : new ParmRISC(parm);
case BulkMemPhiNode phi-> new BulkMemPhiNode(phi);
case MemPhiNode phi -> new MemPhiNode(phi);
case PhiNode phi -> new PhiNode(phi);
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java
index 1a6ae123..9f4da6b8 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java
@@ -6,14 +6,21 @@ import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.util.SB;
-// Convert a narrow C return register to Simple's full-width integer value.
+// Normalize narrow parameters and C return values to full-width integers.
public class ExtendX86 extends MachConcreteNode {
final int _bits;
final boolean _signed;
ExtendX86(ProjNode prj, TypeInteger ret) {
- super(prj);
- _inputs.set(0,null);
- _inputs.add(new ProjX86(prj));
+ this(prj,new ProjX86(prj),ret);
+ }
+ ExtendX86(ParmNode parm, TypeInteger decl) {
+ this(parm,new ParmX86(parm),decl);
+ }
+ private ExtendX86(Node ideal, Node value, TypeInteger ret) {
+ super(ideal);
+ _inputs.clear();
+ _inputs.add(null);
+ _inputs.add(value);
_bits = 8 << ret.log_size();
_signed = ret._min < 0;
}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
index 3dc93bf6..3d71b4ea 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
@@ -306,7 +306,8 @@ public class x86_64_v2 extends Machine {
case NewNode nnn -> new NewX86(nnn);
case NotNode not -> new NotX86(not);
case OrNode or -> or(or);
- case ParmNode parm -> new ParmX86(parm);
+ case ParmNode parm -> parm._declaredType instanceof TypeInteger ti && ti.log_size()<3
+ ? new ExtendX86(parm,ti) : new ParmX86(parm);
case BulkMemPhiNode phi-> new BulkMemPhiNode(phi);
case MemPhiNode phi -> new MemPhiNode(phi);
case PhiNode phi -> new PhiNode(phi);
@@ -555,6 +556,7 @@ public class x86_64_v2 extends Machine {
private static int off, scale, imm;
private static Node idx, val;
private <N extends MemOpNode > N address(N mop) {
+ externalAddress(mop);
off = scale = imm = 0; // Reset
idx = val = null;
Node base = mop.ptr();
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/Allocations.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/Allocations.java
index be68e10e..861f95b6 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/Allocations.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/Allocations.java
@@ -14,28 +14,30 @@ final class Allocations {
Parse an allocation
*/
Node parseNew() {
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
Type t = P.types.parseType();
if( t==null ) {
String tname = P._lexer.matchId();
if( tname==null || Parser.KEYWORDS.contains(tname) )
- throw P.error("Expected a type");
+ throw Parser.error("Expected a type after 'new'",site);
t = P.types.forwardRefType(tname);
}
// Parse ary[ length_expr ]
if( P.match("[") ) {
if( !t.makeZero().isa(t) )
- throw P.error("Cannot allocate a non-nullable, since arrays are always zero/null filled");
+ throw Parser.error("Cannot allocate a non-nullable, since arrays are always zero/null filled",site);
Node len = P.expressions.parseAssignment().keep();
P.require("]");
return allocArray(P.types.typeAry(t,false),len);
} else {
if( !(t instanceof TypeMemPtr tmp) )
- throw P.error("Cannot allocate a "+t.str());
- return allocStruct(tmp._obj);
+ throw Parser.error("Cannot allocate a "+t.str(),site);
+ return allocStruct(tmp._obj,site);
}
}
- private Node allocStruct(TypeStruct ts) {
+ private Node allocStruct(TypeStruct ts, Lexer site) {
Ary<Node> ctorArgs = constructorArgs();
// A field declaration can leave a short forward type name in TYPES.
// Allocation happens in lexical context, so resolve that short name
@@ -45,7 +47,7 @@ final class Allocations {
if( resolved instanceof TypeStruct rts ) ts = rts;
TypeStruct clz = (TypeStruct)Parser.TYPES.get(TypeStruct.addClzPrefix(typeName));
if( clz==null )
- throw P.error("Unknown struct type '" + ts._name + "'");
+ throw Parser.error("Unknown struct type '" + ts._name + "'",site);
Field afld = clz.field("<new>");
Node alloc;
if( afld != null ) {
@@ -56,7 +58,7 @@ final class Allocations {
: new FunPtrNode(tfp,P._code._start,fun.ret()).peephole();
} else {
if( !ts._fref )
- throw P.error("Constructor arguments for '" + ts._name + "' but no constructor is defined");
+ throw Parser.error("Constructor arguments for '" + ts._name + "' but no constructor is defined",site);
Type[] sig = new Type[ctorArgs._len];
for( int i = 0; i<ctorArgs._len; i++ )
sig[i] = ctorArgs.at(i)._type;
@@ -111,12 +113,12 @@ final class Allocations {
return self.unkeep();
}
- Node newString(String s) {
+ Node newString(byte[] s) {
TypeStruct ts = P.types.typeAry(TypeInteger.U8,true);
int lenAlias = ts.field("#" )._alias;
int elemAlias = ts.field("[]")._alias;
TypeConAryB body = TypeConAryB.make(s);
- TypeInteger slen = TypeInteger.constant(s.length());
+ 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
// array body.
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java
index caa73076..6c626669 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java
@@ -14,6 +14,8 @@ final class ControlFlow {
// Parse a conditional expression, merging results.
Node conditional( Node pred, String fside ) {
+ Lexer site=P.loc();
+ if( !fside.equals("else") ) site._position-=fside.equals(":") ? 1 : 2;
// IfNode takes current control and predicate
pred.keep();
Node ifNode = new IfNode(P.ctrl(), pred).peephole();
@@ -88,7 +90,7 @@ final class ControlFlow {
Node ret = P.peep(new PhiNode("",r,lhs.unkeep(),rhs.unkeep()));
// Immediately fail e.g. `arg ? 7 : ptr`
ParseException err;
- if( !fside.equals("else") && (err = ret.err()) !=null ) throw err;
+ if( !fside.equals("else") && (err = ret.err()) !=null ) throw Parser.error(err.getMessage(),site);
ret.keep();
r.peephole();
return ret.unkeep();
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/Declarations.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/Declarations.java
index 91955197..13fa59b8 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/Declarations.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/Declarations.java
@@ -47,15 +47,17 @@ final class Declarations {
boolean inferType = t==Type.TOP || t==Type.BOTTOM;
boolean xfinal = bindingFinal(t);
Lexer loc = P.loc();
- String name = P.requireId();
+ String name = P.requireId(P._scope.klast() instanceof Kind.Func fun && FunNode.isInit(fun._name));
// Optional initializing expression follows
boolean fld_final = false; // Field is final, but not deeply final
boolean uninit = false; // Required non-null constructor field
Node expr;
if( P.match("=") ) {
+ P._lexer.skipWhiteSpace();
+ Lexer initializer=P.loc();
if( isExternDecl() ) {
if( !(t instanceof TypeFunPtr) ) {
- Node ptr = externData(name,t);
+ Node ptr = externData(name,t,loc);
// The lexical binding is a fixed address; its contents are mutable.
if( !P._scope.define(name,t,true,ptr,loc) )
throw Parser.error("Redefining name '"+name+"'",loc);
@@ -72,8 +74,12 @@ final class Declarations {
// var/val, then type comes from expression
if( inferType ) {
if( expr._type==Type.NIL )
- throw P.error("a not-null/non-zero expression");
+ throw Parser.error("Cannot infer a type from null; declare a nullable type explicitly",initializer);
t = expr._type;
+ // A typed expression may still await a forward callee's flow
+ // type (e.g. interpolation while sys.text is parsed). Its
+ // checked conversion already supplies a binding/storage type.
+ if( t==Type.BOTTOM && expr instanceof ConvertNode convert ) t=convert.dst();
if( !xfinal ) t = Conversions.mutableInferredType(t);
// Instance fields may change until the constructor finishes.
// Keep the declared family/access, not the default's constant
@@ -133,8 +139,7 @@ final class Declarations {
// Just after parsing "type foo = " can parse `"C"`
private boolean isExternDecl( ) {
int old = P.pos();
- String s = P._lexer.parseString();
- if( "C".equals(s) ) return true;
+ if( P.match("\"C\"") ) return true;
P.pos(old);
return false;
}
@@ -151,14 +156,14 @@ final class Declarations {
// A lexical C binding has a singleton namespace with one external field.
// The namespace occupies no storage; its field offset supplies the address.
- private Node externData(String name, Type t) {
+ private Node externData(String name, Type t, Lexer site) {
if( !(t instanceof TypeInteger) && !(t instanceof TypeFloat) )
- throw P.error("C data binding requires an integer or float type");
+ throw Parser.error("C data binding requires an integer or float type",site);
String cname = ("C:"+name).intern();
TypeStruct ts = TypeStruct.make(cname,false,Field.make(name,t,P._code.externAlias(name),false,true));
Type old = Parser.TYPES.putIfAbsent(cname,ts);
if( old!=null && old!=ts )
- throw P.error("Conflicting types for C data '"+name+"'");
+ throw Parser.error("Conflicting types for C data '"+name+"'",site);
return P.con(TypeMemPtr.make((byte)2,ts,true));
}
}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/Expressions.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/Expressions.java
index 93c07d8a..247d974e 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/Expressions.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/Expressions.java
@@ -15,6 +15,8 @@ final class Expressions {
/** Parse: [name '='] expr
*/
Node parseAssignment() {
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
int old = P.pos();
String name = P._lexer.matchId();
// Just a plain expression, no assignment.
@@ -24,13 +26,16 @@ final class Expressions {
// Find variable to update
Var def = P._scope.lookup(name);
- if( def==null )
+ if( def==null ) {
+ P.pos(old);
+ P._lexer.skipWhiteSpace();
throw P.error("Undefined name '" + name + "'");
+ }
if( def._extern!=0 ) // Parse the C binding as a memory lvalue
{ P.pos(old); return parseExpression(); }
- P.references.checkAssignment(def);
+ P.references.checkAssignment(def,site);
return P.references.assign(def,parseAssignment());
}
@@ -117,6 +122,8 @@ final class Expressions {
Node cmp = makeCompBool(dir,lhs.unkeep(),rhs).keep(); // Convert to a bool
// Stacked compares?
+ P._lexer.skipWhiteSpace();
+ Lexer operator=P.loc();
int dir0 = parseCompDir();
if( dir0 == 0 ) {
rhs.unkill();
@@ -137,7 +144,7 @@ final class Expressions {
// compares (LHS:b < RHS:c) then (LHS:c < RHS:d), etc.
while( dir0 != 0 ) {
if( Math.abs(dir) != Math.abs(dir0) )
- throw P.error("Mixing relational directions in a chained relational test");
+ throw Parser.error("Mixing relational directions in a chained relational test",operator);
// True side parses next arm of test
P.ctrl(ifT.unkeep());
lhs = rhs; // Reuse the previous RHS exactly once
@@ -150,6 +157,8 @@ final class Expressions {
P.ctrl(ifF);
fail.mergeScopes(P._scope.dup(), P.loc()).peephole();
// Check next stacked compare
+ P._lexer.skipWhiteSpace();
+ operator=P.loc();
dir0 = parseCompDir();
}
rhs.unkill();
@@ -185,9 +194,11 @@ final class Expressions {
private Node parseShift() {
Node lhs = parseAddition();
while( true ) {
- if( P.match("<<") ) lhs = new ShlNode(P.loc(),lhs,null);
- else if( P.match(">>>")) lhs = new ShrNode(P.loc(),lhs,null);
- else if( P.match(">>") ) lhs = new SarNode(P.loc(),lhs,null);
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
+ if( P.match("<<") ) lhs = new ShlNode(site,lhs,null);
+ else if( P.match(">>>")) lhs = new ShrNode(site,lhs,null);
+ else if( P.match(">>") ) lhs = new SarNode(site,lhs,null);
else break;
lhs.setDef(2,parseAddition());
ParseException err;
@@ -251,12 +262,14 @@ final class Expressions {
int old = P.pos();
if( P.match("--") || P.match("++") ) {
int delta = P._lexer.peek(-1)=='+' ? 1 : -1; // Inc vs Dec
+ P._lexer.skipWhiteSpace();
int start = P.pos();
+ Lexer site=P.loc();
String name = P._lexer.matchId();
Var var = name==null ? null : P._scope.lookup(name);
if( var!=null && !(var.type() instanceof TypeMemPtr) ) {
if( var._extern==0 && var._final && !P._scope.canInit(var) )
- throw P.error("Cannot reassign final '"+var._name+"'");
+ throw Parser.error("Cannot reassign final '"+var._name+"'",site);
// Resolve fields just as a read or postfix update does. Updating
// their declaration's local slot would silently lose the Store.
P.pos(start);
@@ -286,7 +299,7 @@ final class Expressions {
// pre is +/-1 for a prefix update of a known name, zero otherwise.
private Node parsePrimary(int pre) {
if( P._lexer.isNumber(P._lexer.peek()) ) return parseLiteral();
- if( P._lexer.peek('"') ) return parsePostfix(P.allocations.newString(P._lexer.parseString()));
+ if( P._lexer.peek('"') ) return parsePostfix(P.strings.parse());
if( P.matchx("true" ) ) return P.con(1);
if( P.matchx("false") ) return P._code.ZERO;
if( P.matchx("null" ) ) return P.con(Type.NIL);
@@ -298,8 +311,10 @@ final class Expressions {
int pos = P.pos();
// Expect an identifier now
String id = P._lexer.matchId();
- if( id == null || Parser.KEYWORDS.contains(id) )
+ if( id == null || Parser.KEYWORDS.contains(id) ) {
+ P.pos(pos);
throw P.errorSyntax("an identifier or expression");
+ }
int pos2 = P.pos();
return P.references.parseName(id,pre,pos,pos2);
}
@@ -366,7 +381,11 @@ final class Expressions {
P._lexer.skipWhiteSpace();
Lexer site = P.loc();
String name;
- if( P.match(".") ) name = P.requireId();
+ if( P.match(".") ) {
+ P._lexer.skipWhiteSpace();
+ site=P.loc();
+ name = P.requireId(true);
+ }
else if( P.match("#") ) name = "#";
else if( P.match("[") ) name = "[]";
// Can get here without an explicit receiver: "f()" or
@@ -389,6 +408,6 @@ final class Expressions {
// Already parsed "'"
Node parseChar() {
- return P.require(P.con(TypeInteger.constant(P._lexer.parseChar())),"'");
+ return P.con(TypeInteger.constant(P._lexer.parseChar()));
}
}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/Functions.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/Functions.java
index 14dee33b..5175acc6 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/Functions.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/Functions.java
@@ -73,14 +73,19 @@ final class Functions {
while( true ) {
Type t = P.types.parseType(); // Arg type
if( t==null ) break;
+ P._lexer.skipWhiteSpace();
+ Lexer qualifier=P.loc();
if( P.match("^") ) {
if( !(t instanceof TypeMemPtr ptr) || ptr._obj.isAry() )
- throw P.error("'^' requires a struct parameter");
+ throw Parser.error("'^' requires a struct parameter",qualifier);
t = ptr.withPartial(true);
P.construction._borrowed = true;
}
finals.set(ids.size(),P.declarations.bindingFinal(t));
+ P._lexer.skipWhiteSpace();
+ Lexer nameLoc=P.loc();
String id = P.requireId();
+ if( ids.find(id)!=-1 ) throw Parser.error("Duplicate parameter '"+id+"'",nameLoc);
ts .push(t ); // Push type/arg pairs
ids.push(id);
P.match(",");
@@ -98,7 +103,7 @@ final class Functions {
Lexer site = P.loc();
site._position--; // Opening '('; arguments can contain nested calls.
if( fcn._type == Type.NIL )
- throw P.error("Calling a null function pointer");
+ throw Parser.error("Calling a null function pointer",site);
fcn.keep(); // Keep while parsing args
Ary<Node> args = new Ary<>( Node.class );
@@ -207,6 +212,9 @@ final class Functions {
int oldUID = P._code.UID(); // Used to approximate function size
FunNode fun = enterFunction(funName,sig,loc,finals,ids);
+ // Guards belong to this function's control flow. Do not inherit an
+ // enclosing function's predicates or leak this body's guards on exit.
+ P._scope.addGuards(fun,null,false);
Construction.Frame outerConstruction = P.construction.enter(fun,isCtor && !fun.isClz());
Node rpc = fun.parm(0);
if( fun.isClz() ) P.structs.beginClassInit(fun,loc);
@@ -260,6 +268,7 @@ final class Functions {
P._ref.addFun(P._code,fun);
// Unstack parser state
+ P._scope.removeGuards(fun);
if( _returnScope != null ) _returnScope.kill();
_returnScope = returnScope;
P.statements._continueScope = continueScope;
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/Lexer.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/Lexer.java
index ec88738b..5e4b7311 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/Lexer.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/Lexer.java
@@ -3,10 +3,12 @@ package com.seaofnodes.simple.parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.CompUnit;
import com.seaofnodes.simple.codegen.Serialize;
+import com.seaofnodes.simple.codegen.SourceError;
import com.seaofnodes.simple.parser.Parser.ParseException;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.BAOS;
import java.util.HashMap;
+import java.io.ByteArrayOutputStream;
// Lexer provides low level access to the raw file bytes, peeks and matches
// short strings, parses numbers, skips comments and whitespace, tracks
@@ -35,6 +37,11 @@ public class Lexer {
public String position() { return _line_number+":"+(_position-_line_start+1); }
public int pos() { return _position; }
+ public String diagnostic(String message) {
+ return _cu==null ? SourceError.format("<input>",_input,_position,message)
+ : SourceError.format(_cu,_position,message);
+ }
+
/**
* Record the source text for lexing
*/
@@ -143,19 +150,21 @@ public class Lexer {
while( true ) {
if( isWhiteSpace() ) inc();
// Skip // to end of line
- else if( _position+2 < _input.length &&
+ else if( _position+1 < _input.length &&
_input[_position ] == '/' &&
_input[_position+1] == '/') {
inc(); inc();
while( !isEOF() && _input[_position] != '\n' )
inc();
- } else if( _position+2 < _input.length &&
+ } else if( _position+1 < _input.length &&
_input[_position ] == '/' &&
_input[_position+1] == '*') {
- // Skip /*comment*/
- while( !isEOF() && !(_input[_position-1] == '*' && _input[_position] == '/'))
+ Lexer start=new Lexer(this);
+ inc(); inc();
+ while( !isEOF() && !(peek()=='*' && _position+1<_input.length && peek(1)=='/') )
inc();
- inc();
+ if( isEOF() ) throw Parser.error("Unclosed block comment",start);
+ inc(); inc();
} else break;
}
}
@@ -229,20 +238,27 @@ public class Lexer {
// Return a constant Type, either TypeInteger or TypeFloat
Type parseNumber() {
+ Lexer start=new Lexer(this);
int old = _position;
int len = isLongOrDouble();
boolean f32 = peek()=='f';
if( f32 ) nextChar();
int n = Math.abs(len);
- if( f32 )
- return TypeFloat.constant(Float.parseFloat(new String(_input,old,n)));
- if( len > 0 ) {
- if( len > 1 && _input[old]=='0' )
- throw Parser.error("Syntax error: integer values cannot start with '0'",this);
- long i = Long.parseLong(new String(_input,old,len));
- return TypeInteger.constant(i);
+ String literal=new String(_input,old,n);
+ try {
+ if( f32 )
+ 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);
+ long i = Long.parseLong(literal);
+ return TypeInteger.constant(i);
+ }
+ return TypeFloat.constant(Double.parseDouble(literal));
+ } catch( NumberFormatException ex ) {
+ throw Parser.error(len>0 && !f32 ? "Integer literal is outside the signed 64-bit range"
+ : "Invalid floating-point literal '"+literal+(f32 ? "f" : "")+"'",start);
}
- return TypeFloat.constant(Double.parseDouble(new String(_input,old,n)));
}
private String parseNumberString() {
int old = _position;
@@ -319,25 +335,77 @@ public class Lexer {
return 0;
}
- String parseString() {
- if( !peek('"') ) return null;
- inc();
- StringBuilder sb = new StringBuilder();
+ record StringPart(byte[] text, boolean end) {}
+
+ // Opening quote has been consumed. Leave a format's '%' unread so both
+ // the expression parser and deferred-body scanner use the same grammar.
+ StringPart stringPart(Lexer start) {
+ ByteArrayOutputStream sb = new ByteArrayOutputStream();
while( !isEOF() ) {
char c = nextChar();
- if( c=='"' ) return sb.toString();
+ if( c=='"' ) return new StringPart(sb.toByteArray(),true);
+ if( c=='%' ) {
+ if( peek()=='%' ) { inc(); sb.write('%'); continue; }
+ _position--;
+ return new StringPart(sb.toByteArray(),false);
+ }
if( c!='\\' ) {
- sb.append(c);
+ sb.write(c);
continue;
}
if( isEOF() )
- throw error("Unclosed string");
- sb.append(escapedChar("string"));
+ throw Parser.error("Unclosed string",start);
+ sb.write(escapedChar("string"));
}
- throw error("Unclosed string");
+ throw Parser.error("Unclosed string",start);
+ }
+
+ record Format(int kind, int flags, int width, int precision, Lexer site) {}
+
+ Format stringFormat() {
+ Lexer site = new Lexer(this);
+ assert peek()=='%';
+ inc();
+ if( peek()=='(' ) { inc(); return new Format(0,0,0,-1,site); }
+ int flags=0, bit;
+ while( (bit="-+ #0".indexOf(peek()))>=0 ) {
+ int mask=1<<bit;
+ if( (flags&mask)!=0 ) throw Parser.error("Repeated format flag",site);
+ flags |= mask;
+ inc();
+ }
+ int width = formatNumber(site);
+ int precision = -1;
+ if( peek()=='.' ) {
+ inc();
+ precision = formatNumber(site);
+ }
+ char kind=nextChar();
+ if( "diuoxXfFeEgGaAcsb".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') )
+ throw Parser.error("Precision is not supported for %"+kind,site);
+ if( peek()!='(' ) throw Parser.error("Expected '(' after string format %"+kind,site);
+ inc();
+ return new Format(kind,flags,width,precision,site);
+ }
+
+ private int formatNumber(Lexer site) {
+ long n=0;
+ while( peek()>='0' && peek()<='9' ) {
+ n=n*10+nextChar()-'0';
+ if( n>Integer.MAX_VALUE ) throw Parser.error("String format width or precision is too large",site);
+ }
+ return (int)n;
}
private char escapedChar(String kind) {
+ Lexer start=new Lexer(this);
+ start._position--; // The backslash introducing the escape.
char esc = nextChar();
return switch( esc ) {
case '0' -> '\u0000';
@@ -347,17 +415,24 @@ public class Lexer {
case '\\' -> '\\';
case '"' -> '"';
case '\'' -> '\'';
- default -> throw error("Unknown "+kind+" escape \\\\"+esc);
+ case '%' -> '%';
+ default -> throw Parser.error("Unknown "+kind+" escape \\"+esc,start);
};
}
char parseChar() {
- if( isEOF() ) throw error("Unclosed character");
+ Lexer start=new Lexer(this);
+ start._position--; // Opening quote was consumed by the caller.
+ if( isEOF() || peek()=='\n' || peek()=='\r' ) throw Parser.error("Unclosed character literal",start);
+ if( peek()=='\'' ) throw Parser.error("Empty character literal",start);
char c = nextChar();
if( c=='\\' ) {
- if( isEOF() ) throw error("Unclosed character");
+ if( isEOF() ) throw Parser.error("Unclosed character literal",start);
c = escapedChar("character");
}
+ if( isEOF() || peek()=='\n' || peek()=='\r' ) throw Parser.error("Unclosed character literal",start);
+ if( peek()!='\'' ) throw Parser.error("Character literal must contain exactly one byte",start);
+ inc();
return c;
}
@@ -372,16 +447,18 @@ public class Lexer {
if( isEOF() ) throw error("Unclosed "+what);
char ch = peek();
if( ch==close && depth==0 ) return;
+ Lexer site=ch=='"' ? new Lexer(this) : null;
nextChar();
if( ch==open ) depth++;
else if( ch==close ) depth--;
- else if( ch=='"' || ch=='\'' ) {
- char quote = ch;
- while( !isEOF() ) {
- ch = nextChar();
- if( ch=='\\' ) nextChar();
- else if( ch==quote ) break;
+ else if( ch=='"' ) {
+ while( !stringPart(site).end() ) {
+ stringFormat();
+ skipTo('(',')',"string interpolation");
+ nextChar();
}
+ } else if( ch=='\'' ) {
+ parseChar();
}
}
}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/Parser.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/Parser.java
index 4d140e55..0cf3c0e7 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/Parser.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/Parser.java
@@ -24,6 +24,7 @@ public class Parser {
final References references = new References(this);
final Conversions conversions = new Conversions(this);
final Construction construction = new Construction(this);
+ final Strings strings = new Strings(this);
public void checkConstructionSerializable() { construction.checkSerializable(); }
@@ -106,7 +107,7 @@ public class Parser {
put("i32" ,TypeInteger.I32);
put("i64" ,TypeInteger.BOT);
put("i8" ,TypeInteger.I8 );
- put("int" ,TypeInteger.BOT);
+ put("int" ,TypeInteger.I32);
put("u1" ,TypeInteger.U1 );
put("u16" ,TypeInteger.U16);
put("u32" ,TypeInteger.U32);
@@ -117,7 +118,8 @@ public class Parser {
public static class ParseException extends RuntimeException {
public final Lexer _loc;
- ParseException( String msg, Lexer loc ) { super(msg); _loc = 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()); }
}
public Parser(CodeGen code ) {
@@ -186,9 +188,18 @@ public class Parser {
// Require and return an identifier
String requireId() {
+ return requireId(false);
+ }
+
+ // Primitive names are useful member names (sys.text.int), but remain
+ // reserved for local variables, parameters and type declarations.
+ String requireId(boolean member) {
+ _lexer.skipWhiteSpace();
+ Lexer site=loc();
String id = _lexer.matchId();
- if (id != null && !KEYWORDS.contains(id) ) return id.intern();
- throw error("Expected an identifier, found '"+id+"'");
+ if (id != null && (!KEYWORDS.contains(id) || member &&
+ (INIT_TYPES.get(id) instanceof TypeInteger || INIT_TYPES.get(id) instanceof TypeFloat)) ) return id.intern();
+ throw error("Expected an identifier, found '"+new Lexer(site).getAnyNextToken()+"'",site);
}
String matchId() {
@@ -266,7 +277,9 @@ public class Parser {
ParseException errorSyntax(String syntax) { return _errorSyntax("expected `"+syntax+"`"); }
private ParseException _errorSyntax(String msg) {
- return error("Syntax error, "+msg+" but found `" + _lexer.getAnyNextToken()+"`");
+ _lexer.skipWhiteSpace();
+ // Reading the token for the message must not advance the error location.
+ return error("Syntax error, "+msg+" but found `" + loc().getAnyNextToken()+"`");
}
ParseException error(String msg) { return error(msg,_lexer); }
public static ParseException error(String msg, Lexer loc) { return new ParseException(msg,loc); }
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/References.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/References.java
index 483bd7a8..6335bad2 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/References.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/References.java
@@ -15,13 +15,36 @@ final class References {
References(Parser P) { this.P = P; }
+ // Compiler-generated library references use ordinary module lookup and
+ // forward resolution, without a synthetic token stream.
+ Node classReference(String name, Lexer site) {
+ CompUnit cu=ParseAll.findCompUnit(P._code,P._ref,name);
+ if( cu==null ) throw Parser.error("Cannot find standard library '"+name+"'",site);
+ String clzName=TypeStruct.addClzPrefix(cu._cname);
+ TypeStruct clz=(TypeStruct)Parser.TYPES.computeIfAbsent(clzName,n -> TypeStruct.make(n,true));
+ FRefNode ref=new FRefNode(name,site,TypeMemPtr.make((byte)2,clz,true));
+ _frefs.push(ref);
+ return ref.peephole();
+ }
+
+ // Read a member without consuming a postfix from source text.
+ Node member(Node receiver, String name, Lexer site) {
+ Field fld=receiver._type instanceof TypeMemPtr ptr ? ptr._obj.field(name) : null;
+ return P.peep(new LoadNode(site,name,fld==null ? 1 : fld._alias,
+ P.ctrl(),P.mem(),receiver,P.fldoff(receiver,name)));
+ }
+
// Resolve a primary name, retaining its use site before speculative type lookup.
Node parseName(String id, int pre, int pos, int pos2) {
+ Lexer site=P.loc();
+ site._position=pos;
+ site.skipWhiteSpace();
// Attempt a local var lookup first
Var var = P._scope.lookup(id);
if( var==null ) {
// Not a local var, try again as a type name
P.pos(pos);
+ Lexer nameLoc = P.loc();
Type t = P.types.parseType();
if( t!=null ) {
// Static <clinit> typename lookup
@@ -52,7 +75,7 @@ final class References {
t = TypeMemPtr.make((byte)2,clz,true);
}
// Define the FRef
- var = P._scope.defineFRef(id,t,true,P.loc());
+ var = P._scope.defineFRef(id,t,true,nameLoc);
P.references._frefs.push((FRefNode)P._scope.in(var));
}
@@ -73,7 +96,7 @@ final class References {
if( kx < fx && !P.structs.inExplicitConstructor() && // Out of scope
!(var._final && rvalue._type.isConstant()) ) { // Not a final constant
if( kx > 0 ) // Global scope is OK, only ever one of these (not one per function invoke)
- throw P.error("Variable '"+var._name+"' is out of function scope and must be a final constant");
+ throw Parser.error("Variable '"+var._name+"' is out of function scope and must be a final constant",site);
// var.idx==2 is self argument, 3 is args, 4 or more are now static globals
if( var._idx >= 4 ) {
@@ -98,7 +121,7 @@ final class References {
}
// Assign-update direct into Scope
if( var._final && !P._scope.canInit(var) )
- throw P.error("Cannot reassign final '"+var._name+"'");
+ throw Parser.error("Cannot reassign final '"+var._name+"'",site);
checkSharedFunction(var);
Node op = P.expressions.opAssign(ch,rvalue, var.type() );
P._scope.update(var,op);
@@ -106,14 +129,14 @@ final class References {
return pre==0 && Lexer.postfix(ch) ? rvalue : op;
}
- void checkAssignment(Var def) {
+ void checkAssignment(Var def, Lexer 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 &&
// Inside an allocation, final assign is OK, outside nope.
// The parseNew() call added the allocation scope
!P._scope.canInit(def) )
- throw P.error("Cannot reassign final '"+name+"'");
+ throw Parser.error("Cannot reassign final '"+name+"'",site);
checkSharedFunction(def);
}
@@ -201,7 +224,7 @@ final class References {
if( fld==null && ts!=null )
fld = P.types.fieldOrOpen(ts,name,false);
if( fld==null && ts!=null && !P.types.latestStruct(ts)._open )
- throw P.error("Accessing unknown field '"+name+"' from '*"+ts._name+"'");
+ throw Parser.error("Accessing unknown field '"+name+"' from '*"+ts._name+"'",site);
// We have to record "active touches" of a new class, to track when we
// need to force class initialization. Basically, we insert an CLInit,
@@ -233,7 +256,7 @@ final class References {
// Store to field
mem.keep();
if( !P.construction.own(expr) && expr._type instanceof TypeMemPtr view && view._ro && P.ctrl()._type!=Type.XCONTROL )
- throw P.error("Cannot modify final field '"+name+"'");
+ throw Parser.error("Cannot modify final field '"+name+"'",site);
Node st = new StoreNode(P.loc(), name, alias, decl, P.ctrl(), mem, expr.unkeep(), off.unkeep(), lift, P.construction.own(expr)).peephole().keep();
P.construction.store(expr,alias,st,mem);
st.unkeep();
@@ -250,17 +273,17 @@ final class References {
decl = conary.elem();
// Load field
- Node load = P.peep(new LoadNode(P.loc(),name,alias,P.ctrl(),mem,expr,off));
+ Node load = P.peep(new LoadNode(site,name,alias,P.ctrl(),mem,expr,off));
// 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 )
- throw P.error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor" );
+ 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();
if( !P.construction.own(expr) && expr._type instanceof TypeMemPtr view && view._ro && P.ctrl()._type!=Type.XCONTROL )
- throw P.error("Cannot modify final field '"+name+"'");
+ throw Parser.error("Cannot modify final field '"+name+"'",site);
Node st = new StoreNode(P.loc(), name, alias, decl, P.ctrl(), mem, expr.unkeep(), off.unkeep(), op, P.construction.own(expr)).peephole().keep();
P.construction.store(expr,alias,st,mem);
st.unkeep();
@@ -302,7 +325,8 @@ final class References {
Ary<FRefNode> frefs = new Ary<>(FRefNode.class);
for( FRefNode fref : _frefs ) {
if( fref.nIns() != 1 && fref._con == FRefNode.FREF_TYPE ) continue;
- CompUnit cu = ParseAll.findCompUnitOrThrow(P._code,P._ref,fref._name);
+ CompUnit cu = ParseAll.findCompUnit(P._code,P._ref,fref._name);
+ if( cu==null ) throw fref.err();
// Name lookup may already know this class's fields. Do not replace
// that information with an empty placeholder before Iter runs.
if( fref._con==FRefNode.FREF_TYPE )
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/Statements.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/Statements.java
index 4ff4993b..707202ab 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/Statements.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/Statements.java
@@ -24,12 +24,14 @@ final class Statements {
* @return a {@link Node} or {@code null}
*/
Node parseStatement() {
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
if( P.matchx("return") ) return parseReturn();
else if (P.matchx("if") ) return parseIf();
else if (P.matchx("while") ) return parseWhile();
else if (P.matchx("for") ) return parseFor();
- else if (P.matchx("break") ) return parseBreak();
- else if (P.matchx("continue")) return parseContinue();
+ else if (P.matchx("break") ) return parseBreak(site);
+ else if (P.matchx("continue")) return parseContinue(site);
else if (P.matchx("struct") ) return P.structs.parseStruct();
else if (P.structs.inStructDeclaration() && P.matchx("new")) return P.structs.parseConstructorDeclaration();
else if (P.matchx(";") ) return P._code.ZERO; // Empty statement
@@ -204,7 +206,7 @@ final class Statements {
if( doFor ) {
int old = P.pos(nextPos);
if( !P.peek(')') )
- P.expressions.parseAssignment();
+ P.expressions.parseAssignment().isKill();
if( P.pos() != nextEnd )
throw P.errorSyntax( "Unexpected code after expression" );
P.pos(old);
@@ -237,8 +239,8 @@ final class Statements {
P._scope = exit;
return P._code.ZERO;
}
- private Node parseBreak () {
- checkLoopActive();
+ private Node parseBreak(Lexer site) {
+ checkLoopActive(site);
// At the time of the break, and loop-exit conditions are only valid if
// they are ALSO valid at the break. It is the intersection of
// conditions here, not the union.
@@ -248,15 +250,20 @@ final class Statements {
return P._code.ZERO;
}
- private Node parseContinue() { checkLoopActive(); _continueScope = P.require(jumpTo( _continueScope ),";"); return P._code.ZERO; }
+ private Node parseContinue(Lexer site) { checkLoopActive(site); _continueScope = P.require(jumpTo( _continueScope ),";"); return P._code.ZERO; }
private ScopeNode jumpTo(ScopeNode toScope) {
ScopeNode cur = P._scope.dup();
P.ctrl(P._code.XCTRL); // Kill current scope
// Prune nested lexical scopes that have depth > than the loop head
// We use _breakScope as a proxy for the loop head scope to obtain the depth
+ // The clone shares lexical boundaries and Var records with the active
+ // parser scope. Promoting on the clone would move those boundaries
+ // without moving the active scope's bindings. Drop locals first, then
+ // copy forward references into the jump scope with their own indices.
while( cur.depth() > _breakScope.depth() )
- cur.pop();
+ cur._pop();
+ cur.balanceLoopFRefs(P._scope);
// If this is a continue then first time the target is null
// So we just use the pruned current scope as the base for the
// "continue"
@@ -264,9 +271,13 @@ final class Statements {
return cur;
// toScope is either the break scope, or a scope that was created here
assert toScope.depth() <= _breakScope.depth();
+ // A later path can discover forward references after an earlier
+ // break/continue saved its scope. Keep those bindings across the merge.
+ toScope.balanceLoopFRefs(cur);
+ cur.balanceLoopFRefs(toScope);
toScope.ctrl(toScope.mergeScopes(cur,P.loc()).peephole());
return toScope;
}
- private void checkLoopActive() { if (_breakScope == null) throw P.error("No active loop for a break or continue"); }
+ private void checkLoopActive(Lexer site) { if (_breakScope == null) throw Parser.error("No active loop for a break or continue",site); }
}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/Strings.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/Strings.java
new file mode 100644
index 00000000..5bf76585
--- /dev/null
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/Strings.java
@@ -0,0 +1,59 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+
+/** Interpolation expands to ordinary sys.text calls. The library itself uses
+ * no interpolation, so it can be compiled from source without staging. */
+final class Strings {
+ private final Parser P;
+ Strings(Parser p) { P=p; }
+
+ Node parse() {
+ Lexer start=P.loc();
+ P.require("\"");
+ Lexer.StringPart part=P._lexer.stringPart(start);
+ if( part.end() ) return P.allocations.newString(part.text());
+ Node text=P.references.classReference("sys.text",P.loc()).keep();
+ P.classInit(P.loc(),text);
+ Node builder=call(text,"begin",P.loc()).keep();
+ while( true ) {
+ if( part.text().length!=0 )
+ call(text,"string",P.loc(),builder,P.allocations.newString(part.text()),P.con('s'),P.con(0),P.con(-1)).isKill();
+ if( part.end() ) break;
+ Lexer.Format fmt=P._lexer.stringFormat();
+ Node value=P.expressions.parseAssignment().keep();
+ P.require(")");
+ Node fptr;
+ if( fmt.kind()==0 ) {
+ Node ints=P.references.member(text,"integer",fmt.site()).keep();
+ Node floats=P.references.member(text,"floating",fmt.site()).keep();
+ 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";
+ fptr=P.references.member(text,family,fmt.site());
+ }
+ call(text,fptr,fmt.site(),builder,value.unkeep(),P.con(fmt.kind() | fmt.flags()<<8),
+ P.con(fmt.width()),P.con(fmt.precision())).isKill();
+ part=P._lexer.stringPart(start);
+ }
+ // String syntax promises this result type even when sys.text is still
+ // awaiting its source parse. Check/convert through the normal IR.
+ Node result=call(text.unkeep(),"end",P.loc(),builder.unkeep());
+ return P.conversions.convertExpr(result,TypeMemPtr.make(P.types.typeAry(TypeInteger.U8,true)).makeRO());
+ }
+
+ private Node call(Node text, String name, Lexer site, Node... values) {
+ return call(text,P.references.member(text,name,site),site,values);
+ }
+
+ private Node call(Node text, Node fptr, Lexer site, Node... values) {
+ Ary<Node> args=new Ary<>(Node.class);
+ args.push(P.ctrl()); args.push(P.mem()); args.push(text);
+ for( Node value:values ) args.push(value);
+ args.push(fptr);
+ return P.functions.functionCall(args,site);
+ }
+}
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/Structs.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/Structs.java
index a5262440..3a3d894c 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/Structs.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/Structs.java
@@ -78,12 +78,14 @@ final class Structs {
Node parseConstructorDeclaration() {
TypeStruct self = P._scope.constructorSelf();
assert self != null;
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
String name = P.requireId();
String expect = initFieldName(self._name);
if( name != expect )
- throw P.error("Constructor name '" + name + "' must match type name '" + expect + "'");
+ throw Parser.error("Constructor name '" + name + "' must match type name '" + expect + "'",site);
P.require("=");
- if( _pendingCtor!=null ) throw P.error("Redefining constructor '"+name+"'");
+ if( _pendingCtor!=null ) throw Parser.error("Redefining constructor '"+name+"'",site);
_pendingCtor = new Constructor(name,P.loc());
// Parse the body after all field declarations are known, so self.x
// and x resolve the same field even when x is declared below this body.
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/parser/Types.java b/chapter30/src/main/java/com/seaofnodes/simple/parser/Types.java
index ffd3a0f4..827518fd 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/parser/Types.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/parser/Types.java
@@ -54,12 +54,14 @@ final class Types {
// Nest arrays and '?' as needed
Type t2 = t1;
while( true ) {
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
if( P.match("?") ) {
if( t2 instanceof TypeMemPtr tmp ) {
- if( tmp.nullable() ) throw P.error("Type "+t2+" already allows null");
+ if( tmp.nullable() ) throw Parser.error("Type "+t2+" already allows null",site);
t2 = tmp.makeNullable();
} else
- throw P.error("Type "+t0+" cannot be null");
+ throw Parser.error("Type "+t0+" cannot be null",site);
} else if( P.match("[~]") ) {
t2 = TypeMemPtr.make(typeAry(t2,true)).makeRO();
} else if( P.match("[]") ) {
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/type/TypeConAry.java b/chapter30/src/main/java/com/seaofnodes/simple/type/TypeConAry.java
index c9093e3b..9cb23e09 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/type/TypeConAry.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/type/TypeConAry.java
@@ -16,6 +16,7 @@ public abstract class TypeConAry<A> extends TypeScalar {
TypeConAry( boolean any, byte widen, A ary ) { super(TCONARY); _any = any; _widen = widen; _ary = ary; }
public static void gather(ArrayList<Type> ts) {
+ ts.add(TypeConAryB.make(""));
ts.add(TypeConAryB.ABC);
ts.add(TypeConAryB.ABCD);
ts.add(TypeConAryI.I123);
@@ -36,6 +37,15 @@ 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 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++ )
@@ -64,6 +74,7 @@ public abstract class TypeConAry<A> extends TypeScalar {
return _any ? Type.TOP : BOTTOM;
int len = len();
long min = Long.MAX_VALUE;
+ if( len==0 ) return emptyElem();
long max = Long.MIN_VALUE;
for( int i=0; i<len; i++ ) {
min = Math.min(min,at8(i));
@@ -71,6 +82,10 @@ 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);
+ }
public abstract long at8(int idx);
public abstract int len();
@Override public abstract int log_size();
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java b/chapter30/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
index 5877c309..84be5fb2 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
@@ -10,7 +10,7 @@ public class TypeConAryB extends TypeConAry<byte[]> {
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]; }
+ @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)+"\"]"; }
diff --git a/chapter30/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/chapter30/src/main/java/com/seaofnodes/simple/type/TypeInteger.java
index a0c2266d..6164f721 100644
--- a/chapter30/src/main/java/com/seaofnodes/simple/type/TypeInteger.java
+++ b/chapter30/src/main/java/com/seaofnodes/simple/type/TypeInteger.java
@@ -53,7 +53,15 @@ public class TypeInteger extends TypeScalar {
}
public Type same_but_slightly_wider_than( ) {
- return _widen < 3 ? make(_min,_max, (byte)(_widen+1)) : BOT;
+ if( _widen < 3 ) return make(_min,_max, (byte)(_widen+1));
+ // Start in the intersection of i32 and u32 for nonnegative ranges.
+ // Widening those straight to i32 invents negative values that prevent
+ // a u32 loop from remaining unsigned. Either 32-bit range can then
+ // grow to its own bound; unbounded arithmetic ultimately reaches BOT.
+ if( _min >= 0 && _max <= I32._max ) return make(0,I32._max,(byte)3);
+ if( _min >= I32._min && _max <= I32._max ) return I32;
+ if( _min >= 0 && _max <= U32._max ) return U32;
+ return BOT;
}
// Force max widen
@Override public Type widen() {
diff --git a/chapter30/src/main/smp/sys.smp b/chapter30/src/main/smp/sys.smp
index 7705a187..1201315b 100644
--- a/chapter30/src/main/smp/sys.smp
+++ b/chapter30/src/main/smp/sys.smp
@@ -7,5 +7,7 @@ val aryi64 = aryi64; // Variable sized array of i64
val aryu8 = aryu8; // Variable sized array of u8
val char = char; // Character utilities
val io = io; // IO library
+val text = text; // Typed text conversions
val libc = libc; // libc FFI bindings
+val rt = rt; // Run Time support
val scan = scan; // A simple string parser
diff --git a/chapter30/src/main/smp/sys/ary.smp b/chapter30/src/main/smp/sys/ary.smp
index 8312e94c..c139cd33 100644
--- a/chapter30/src/main/smp/sys/ary.smp
+++ b/chapter30/src/main/smp/sys/ary.smp
@@ -1,11 +1,25 @@
// Array utilities
+// 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 <= current ) return current;
+ i64 size = current ? current : 1;
+ while( size < needed ) {
+ size *= 2;
+ if( size > 4294967295 ) size = 4294967295;
+ }
+ u32 result = size;
+ return result;
+};
+
// Copy byte array contents. Copies the shorter of the buffers and len
-val copy = { u8[] dst, int doff, u8[~] src, int soff, int len ->
+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( int i=0; i<len; i++ )
+ for( u32 i=0; i<len; i++ )
dst[i+doff] = src[i+soff];
return dst;
};
@@ -14,10 +28,12 @@ val copy = { u8[] dst, int doff, u8[~] src, int soff, int len ->
// len. Returns <0 or >0 for first non-matching byte. Returns <0 or >0 for
// the larger array after that, or 0 if both arrays are fully equal.
val cmp = { u8[~] src1, u32 off1, u8[~] src2, u32 off2, u32 len ->
- u32 len1 = len > src1# - off1 ? src1# - off1 : len;
- u32 len2 = len > src2# - off2 ? src2# - off2 : len;
+ u32 left1 = off1 >= src1# ? 0 : src1# - off1;
+ u32 left2 = off2 >= src2# ? 0 : src2# - off2;
+ u32 len1 = len > left1 ? left1 : len;
+ u32 len2 = len > left2 ? left2 : len;
u32 max = len1 > len2 ? len2 : len1;
- for( int 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/chapter30/src/main/smp/sys/aryi64.smp b/chapter30/src/main/smp/sys/aryi64.smp
index 98b72605..b9f1fbed 100644
--- a/chapter30/src/main/smp/sys/aryi64.smp
+++ b/chapter30/src/main/smp/sys/aryi64.smp
@@ -9,36 +9,36 @@ struct aryi64 {
};
// Grow as needed
- val _grow = { int len2 ->
- if( len+len2 < buf# ) return self;
- int newlen = buf# * 2;
- while( len+len2 >= newlen ) newlen *= 2;
+ val _grow = { u32 len2 ->
+ if( len+len2 <= buf# ) return self;
+ u32 newlen = ary.capacity(buf#,len+len2);
+ if( !newlen ) libc._exit(1); // Length exceeds the array representation.
i64[] !buf2 = new i64[newlen];
- for( int i=0; i<buf#; i++ )
+ for( u32 i=0; i<len; i++ )
buf2[i] = buf[i];
buf = buf2;
self;
};
- // Add a i64 to aryu8
+ // Add an i64 to aryi64
val add = { i64 c ->
_grow(1).buf[len++] = c;
self;
};
- val get = { int index -> return buf[index]; };
+ val get = { u32 index -> return buf[index]; };
// Write the bracketed array-list form to aryu8.
val str = { ->
// TODO: clunky default constructor syntax
var str = new aryu8().add('[');
- for( int 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(']');
};
- val swap = { int i, int j ->
+ val swap = { u32 i, u32 j ->
i64 tmp = buf[i];
buf[i] = buf[j];
buf[j] = tmp;
@@ -75,8 +75,11 @@ val parseAryI64 = { u8[~] str ->
return s.x == s.buf# ? nums : null;
};
-val copy = { i64[] dst, int doff, i64[~] src, int soff, int len ->
- for( int i=0; i<len; i++ )
+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++ )
dst[i+doff] = src[i+soff];
return dst;
};
diff --git a/chapter30/src/main/smp/sys/aryu8.smp b/chapter30/src/main/smp/sys/aryu8.smp
index 21cf920b..18543f13 100644
--- a/chapter30/src/main/smp/sys/aryu8.smp
+++ b/chapter30/src/main/smp/sys/aryu8.smp
@@ -7,12 +7,12 @@ struct aryu8 {
new aryu8 = { -> buf = new u8[1]; };
// Grow as needed
- val _grow = { int len2 ->
- if( len+len2 < buf# ) return self;
- int newlen = buf# * 2;
- while( len+len2 >= newlen ) newlen *= 2;
+ val _grow = { u32 len2 ->
+ if( len+len2 <= buf# ) return self;
+ u32 newlen = ary.capacity(buf#,len+len2);
+ if( !newlen ) libc._exit(1); // Length exceeds the array representation.
u8[] !buf2 = new u8[newlen];
- ary.copy(buf2,0,buf,0,buf#);
+ ary.copy(buf2,0,buf,0,len);
buf = buf2;
self;
};
@@ -27,38 +27,28 @@ struct aryu8 {
// Add all of a plain array to the aryu8
val addAry = { u8[~] xs ->
- _grow(xs#);
- ary.copy(buf,len,xs,0,xs#);
- len += xs#;
+ u32 n=xs#;
+ _grow(n);
+ ary.copy(buf,len,xs,0,n);
+ len += n;
self;
};
// Remove x bytes from the tail.
- val undo = { int x ->
+ val undo = { u32 x ->
len = len >= x ? len-x : 0;
self;
};
// Print ASCII number into aryu8
val num = { i64 x ->
- if( !x ) { add('0'); return self; }
- if( x < 0 ) { add('-'); x = -x; }
- i64 ten = 1, log = 0;
- while( ten <= x ) { ten *= 10; log++; }
- for( int i=0; i<log; i++ ) {
- ten /= 10;
- i64 q = x/ten;
- u8 digit = q+'0';
- add(digit);
- x -= q*ten;
- }
+ text.integer(self,x,0,0,-1);
self;
};
- // Write to fd, returns the number of bytes written (len).
+ // Write all bytes to fd; return len, or -1 with errno set on failure.
val write = { i32 fd ->
- i64 ptr = buf; // Cast buffer pointer to i64 for native C call
- libc.write(fd,ptr,len);
+ io.writeOff(fd,buf,0,len);
};
};
@@ -66,7 +56,7 @@ struct aryu8 {
// Parse one integer, ignore trailing data; returns 0 with an empty string.
// If you want a more robust parsing, use the `scan` class
val atoi = { u8[~] str ->
- int x = 0;
+ u32 x = 0;
while( x < str# && str[x] <= ' ' ) x++;
if( x == str# ) return 0;
bool neg = false;
@@ -77,11 +67,12 @@ val atoi = { u8[~] str ->
return neg ? -n : n;
};
-// Zero-terminate string for libc strings. No error checking, just copies and
-// appends a zero.
+// 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];
- ary.copy(strz,0,str,0,str#);
+ u32 len=str#;
+ ary.copy(strz,0,str,0,len);
strz[str#] = '\0';
strz;
};
diff --git a/chapter30/src/main/smp/sys/io.smp b/chapter30/src/main/smp/sys/io.smp
index d6263ffe..14993429 100644
--- a/chapter30/src/main/smp/sys/io.smp
+++ b/chapter30/src/main/smp/sys/io.smp
@@ -4,43 +4,74 @@
// Write byte array to stdout
val p = { u8[~] str -> write(1,str); };
val p_noInline = { u8[~] str -> write(1,str); };
+// Write to stderr
+val err = { u8[~] str -> write(libc.STDERR,str); };
-// Return fd contents as a u8[].
+
+// Read until EOF, including after short reads. Return an empty array for an
+// empty stream, or null on error with errno preserved (partial data discarded).
val read = { i32 fd ->
- var buf = new u8[8*1024];
- i64 ptr = buf;
- int len = 0;
+ val chunk = new u8[8*1024];
+ i64 ptr = chunk;
+ val buf = new aryu8();
while( true ) {
- i32 rez = libc.read(fd,ptr,buf#);
- if( rez > buf# ) libc._exit(libc.errno());
- len += rez;
- if( rez < buf# )
- break;
- libc._exit(-2); // Need to implement for more than 8K input
+ i64 rez = libc.read(fd,ptr,chunk#);
+ if( rez < 0 ) {
+ if( libc.errno() == libc.EINTR ) continue;
+ return null;
+ }
+ if( rez == 0 ) break;
+ if( buf.len+rez > 4294967295 ) {
+ libc.set_errno(libc.EOVERFLOW);
+ return null;
+ }
+ u32 count = rez;
+ buf._grow(count);
+ ary.copy(buf.buf,buf.len,chunk,0,count);
+ buf.len += count;
}
- var out = new u8[len];
- ary.copy(out,0,buf,0,len);
+ val out = new u8[buf.len];
+ return ary.copy(out,0,buf.buf,0,buf.len);
};
-// Write all of str to fd.
+// Write all bytes, returning the count or -1 with errno set. An error can
+// follow a successfully written prefix; it does not undo that prefix.
val write = { i32 fd, u8[~] str ->
- i64 ptr = str; // Convert array pointer to a i64 for the native call
- libc.write(fd,ptr,str#);
+ writeOff(fd,str,0,str#);
};
-// Write all of str to fd.
+// Write a slice, capping len at the remaining bytes. An offset past the end
+// is invalid, including for an empty write. A valid empty slice returns zero.
+// Invalid arguments return -1;
val writeOff = { i32 fd, u8[~] str, u32 off, u32 len ->
- i64 ptr = str; // Convert array pointer to a i64 for the native call
- ptr += off; // Skip offset
- // Cap at available length
- if( len > str# - off ) len = str# - off;
- // Do the write
- libc.write(fd,ptr,len);
+ if( off > str# ) return libc.set_errno(libc.EINVAL);
+ if( len > str# - off ) len = str# - off;
+ i64 ptr = str;
+ ptr += off;
+ i64 done = 0;
+ while( done < len ) {
+ i64 rez = libc.write(fd,ptr+done,len-done);
+ if( rez < 0 ) {
+ if( libc.errno() == libc.EINTR ) continue;
+ return -1;
+ }
+ if( rez == 0 ) {
+ libc.set_errno(libc.EIO);
+ return -1;
+ }
+ done += rez;
+ }
+ return done;
};
// Return stdin as a u8[].
val stdin = { -> read(0); };
+// One byte, without Enter or echo. Restores the previous terminal mode before
+// returning. Files/pipes also work. Returns 0..255, or -1 with libc.errno() set
+// to libc.EOF or an OS error. Ctrl-C is byte 3; escape/UTF-8 sequences are multiple bytes.
+val getch = { -> libc.simple_getch(); };
+
// Create file for writing. Return fd, or -1 and errno has the error code
val create = { u8[~] str ->
i64 ptr = sys.aryu8.str0(str);
diff --git a/chapter30/src/main/smp/sys/libc.smp b/chapter30/src/main/smp/sys/libc.smp
index 17ee65cb..7122bbd8 100644
--- a/chapter30/src/main/smp/sys/libc.smp
+++ b/chapter30/src/main/smp/sys/libc.smp
@@ -2,17 +2,17 @@
// libc bindings:
// https://www.man7.org/linux/man-pages/man2/<libc call>.2.html
-// The buffers are int-versions of zero-terminated strings.
-// int buf = aryu8.str0("some_file");
+// The buffers are i64 addresses of zero-terminated strings.
+// i64 buf = aryu8.str0("some_file");
// buf mode perm-> fd
{ i64 i32 i32 -> i32 } open = "C";
// buf perm -> fd
{ i64 i32 -> i32 } creat = "C";
// fd buf len -> len
-{ i32 i64 u32 -> i32 } write = "C";
+{ i32 i64 i64 -> i64 } write = "C";
// fd buf len -> len
-{ i32 i64 u32 -> i32 } read = "C";
+{ i32 i64 i64 -> i64 } read = "C";
// fd -> err
{ i32 -> i32 } close = "C";
// path -> err
@@ -27,12 +27,29 @@ i32 O_TRUNC = 1024; // 04000 on x86_64-pc-cygwin
i32 STDIN = 0;
i32 STDOUT = 1;
i32 STDERR = 2;
+i32 EOF = -1; // simple_getch sets errno to EOF on end-of-input.
// status
{ i32 -> i32 } _exit = "C";
+
// errno is not an ordinary C data symbol; read it through the native runtime.
{ i32 } simple_errno = "C";
val errno = { -> simple_errno(); };
+{ i32 -> i32 } simple_set_errno = "C";
+// Set errno and return -1, matching the native I/O error convention.
+val set_errno = { i32 error -> simple_set_errno(error); };
+
+i32 EINTR = "C";
+i32 EIO = "C";
+i32 EINVAL = "C";
+i32 EOVERFLOW = "C";
+
+// Read one byte from stdin, without <enter> or echo. Restores the previous
+// terminal mode before returning. Files and pipes also work. Returns 0..255,
+// or -1 with errno set to EOF or an OS error. Ctrl-C is byte 3; escape/UTF-8
+// sequences are multiple bytes.
+{ i32 } simple_getch = "C";
+
// addr len prot flags fd off -> void*
{ i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C";
// mmap flags
diff --git a/chapter30/src/main/smp/sys/rt.smp b/chapter30/src/main/smp/sys/rt.smp
new file mode 100644
index 00000000..d62b4def
--- /dev/null
+++ b/chapter30/src/main/smp/sys/rt.smp
@@ -0,0 +1,3 @@
+// Run Time support: "rt" has the same meaning as Java's rt.jar or C's crt.o.
+// Native entry helpers are compiled and linked independently of ordinary sys
+// imports, so programs without library imports can use the same startup code.
diff --git a/chapter30/src/main/smp/sys/rt/simple_args.smp b/chapter30/src/main/smp/sys/rt/simple_args.smp
new file mode 100644
index 00000000..3f630e54
--- /dev/null
+++ b/chapter30/src/main/smp/sys/rt/simple_args.smp
@@ -0,0 +1,22 @@
+// Native startup support, compiled independently and linked with the CRT.
+// Raw native addresses stay i64; only the arrays allocated here are Simple objects.
+{ 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;
+ // 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++ ) {
+ i64 src = simple_argv(argv,i);
+ i64 len = strlen(src);
+ if( len < 0 || len > 4294967295 ) return null;
+ val bytes = new u8[len];
+ i64 dst = bytes;
+ memcpy(dst,src,len);
+ args[i] = bytes;
+ }
+ return args;
+};
diff --git a/chapter30/src/main/smp/sys/text.smp b/chapter30/src/main/smp/sys/text.smp
new file mode 100644
index 00000000..a414ff45
--- /dev/null
+++ b/chapter30/src/main/smp/sys/text.smp
@@ -0,0 +1,98 @@
+// Formatting primitives used directly by interpolation and named converters.
+// Keep this module free of interpolation: sys can build itself from source.
+// spec's low byte is the format kind; bits 8..12 are flags: - + space # 0.
+
+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;
+};
+
+val _pad = { !aryu8 b, i64 n, u8 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;
+ if( !(spec&256) ) _pad(b,pad,' ');
+ 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' ) {
+ if( !(spec&256) ) _pad(b,width-1,' ');
+ 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;
+ // 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; }
+ 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; }
+ }
+ digits[count++]=alphabet[r];
+ n=q;
+ }
+ 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";
+ }
+ 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( !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]);
+ if( left ) _pad(b,pad,' ');
+ b;
+};
+
+// Only conversion of binary floating-point values is delegated to libc.
+// The fixed signature accepts a validated format and returns a byte string.
+{ f64 i64 i64 i64 -> u8[~] } simple_text_float = "C";
+val floating = { !aryu8 b, f64 value, i64 spec, i64 width, i64 precision ->
+ b.addAry(simple_text_float(value,spec,width,precision));
+};
+
+// Explicitly named, typed conversions share the same formatting routines.
+val int = { i64 x -> end(integer (begin(),x, 0 ,0,-1)); };
+val flt = { f64 x -> end(floating(begin(),x, 0 ,0,-1)); };
+val f32 = { f32 x -> end(floating(begin(),x,'g',0,-1)); };
+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)); };
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/BoundaryTest.java b/chapter30/src/test/java/com/seaofnodes/simple/BoundaryTest.java
index 10b7517e..0b05dc52 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/BoundaryTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/BoundaryTest.java
@@ -20,7 +20,7 @@ public class BoundaryTest {
.driver(CodeGen.Phase.TypeCheck);
assertEquals("7",Eval2.eval(factory,0));
}
- String src="struct Counter { int x; int y; int z; new Counter={int n -> while(x<n) { int old=x; y=x+1; x=y+1; z=old; }}; }; val c=new Counter(arg); return c.x*100+c.z;";
+ String src="struct Counter { int x; int y; int z; new Counter={int n -> while(x<n) { int old=x; y=x+1; x=y+1; z=old; }}; }; val c=new Counter(((arg<<32)>>32)); return c.x*100+c.z;";
CodeGen code=new CodeGen(src).driver(CodeGen.Phase.LocalSched);
assertEquals("2018",Eval2.eval(code,19));
assertEquals("0",Eval2.eval(code,0));
@@ -53,11 +53,11 @@ public class BoundaryTest {
@Test public void testRecursiveCallback() {
String src="""
val run={int n ->
- val power={int base,int exponent -> if(exponent<=0) return 1; return base*power(base,exponent-1);};
- val sumPowers={int n,{int int -> int} f -> if(n<=0) return 0; return f(2,n)+sumPowers(n-1,f);};
+ val power={int base,int exponent -> if(exponent<=0) return 1; return base*power(base,--exponent);};
+ val sumPowers={int n,{int int -> i64} f -> if(n<=0) return 0; return f(2,n)+sumPowers(--n,f);};
return sumPowers(n,power);
};
- return run(arg);
+ return run(((arg<<32)>>32));
""";
for( long seed : new long[]{2,13,123} ) {
CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.Opto);
@@ -74,11 +74,11 @@ public class BoundaryTest {
"retain(power); return power(2,n);",
"if(n) { retain(power); while(true) {} } return power(2,n);"
} ) {
- String src="val run={int n -> val power={int base,int exponent -> if(exponent<=0) return 1; return base*power(base,exponent-1);}; { {int int -> int} -> int } retain=\"C\";"+tail+"}; return run(arg);";
+ String src="val run={int n -> val power={int base,int exponent -> if(exponent<=0) return 1; return base*power(base,--exponent);}; { {int int -> i64} -> int } retain=\"C\";"+tail+"}; return run(((arg<<32)>>32));";
CodeGen code=new CodeGen(src).driver(CodeGen.Phase.Opto);
ParmNode base=code._start.walk(n -> n instanceof ParmNode p && p._label.equals("base") ? p : null);
assertNotNull(base);
- assertSame(TypeInteger.BOT,base._type);
+ assertSame(TypeInteger.I32,base._type);
code.driver(CodeGen.Phase.TypeCheck);
}
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/BrainFuckTest.java b/chapter30/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
index c2713e4f..166f5c5e 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
@@ -63,7 +63,7 @@ val brain_fuck = { ->
return output;
};
""";
- TestC.runC(src,"brain_fuck", brain_fuck, 40);
+ TestC.runC(src,"brain_fuck", brain_fuck,165);
EvalRisc5 R5 = TestRisc5.build( src, "brain_fuck", 0, 25, false);
int trap = R5.step(100000);
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter04Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter04Test.java
index 1132375e..bfde3b2a 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter04Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter04Test.java
@@ -127,14 +127,14 @@ public class Chapter04Test {
public void testBug1() {
CodeGen code = new CodeGen("int a=arg+1; int !b=a; b=1; return a+2; ");
code.parse();
- assertEquals("return (arg+3);", code.print());
+ assertEquals("return ((((arg+1)<<32)>>32)+2);", code.print());
}
@Test
public void testBug2() {
CodeGen code = new CodeGen("int !a=arg+1; a=a; return a; ");
code.parse();
- assertEquals("return (arg+1);", code.print());
+ assertEquals("return (((arg+1)<<32)>>32);", code.print());
}
@Test
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter05Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter05Test.java
index afdb1071..43057c00 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter05Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter05Test.java
@@ -21,7 +21,7 @@ else {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,(arg+2),(arg-3));", code.print());
+ assertEquals("return ((Phi(Region,(arg+2),(arg-3))<<32)>>32);", code.print());
}
@Test
@@ -65,7 +65,7 @@ else
b=a+1;
return a+b;""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return ((arg<<1)+Phi(Region,2,3));", code.print());
+ assertEquals("return ((((Phi(Region,arg,(((arg+1)<<32)>>32))+1)<<32)>>32)+Sar);", code.print());
}
@Test
@@ -80,7 +80,7 @@ else
a=b+1;
return a+b;""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return ((Phi(Region,(arg<<1),arg)+arg)+Phi(Region,4,5));", code.print());
+ assertEquals("return ((((Phi(Region,arg,(((arg+2)<<32)>>32))+1)<<32)>>32)+(((Phi(Region,(((arg+1)<<32)>>32),2)+Phi(Region,Sar,arg))<<32)>>32));", code.print());
}
@Test
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter07Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter07Test.java
index 7094c0d5..20786571 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter07Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter07Test.java
@@ -35,7 +35,7 @@ if(arg){}else{
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,1,Phi(Loop,1,(Phi_a+1)));", code.print());
+ assertEquals("return Phi(Region,1,Phi(Loop,1,(((Phi_a+1)<<32)>>32)));", code.print());
}
@Test
@@ -55,7 +55,7 @@ while(i < arg) {
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,Phi(Loop,Phi_sum,(Phi_sum+Phi(Loop,0,(Phi_j+1)))));", code.print());
+ assertEquals("return Phi(Loop,0,Phi(Loop,Phi_sum,(((Phi_sum+Phi(Loop,0,(((Phi_j+1)<<32)>>32)))<<32)>>32)));", code.print());
}
@Test
@@ -90,7 +90,7 @@ while(a < 10) {
return b;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,2,(Phi(Region,Phi_b,4)+1));", code.print());
+ assertEquals("return Phi(Loop,2,(((Phi(Region,Phi_b,4)+1)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -107,7 +107,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -123,7 +123,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -165,7 +165,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -181,7 +181,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -198,7 +198,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -215,7 +215,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter08Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter08Test.java
index 376e472b..249beb11 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter08Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter08Test.java
@@ -46,7 +46,7 @@ while(arg < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,Phi(Region,Phi_a,(Phi_a+1),Phi_a));", code.print());
+ assertEquals("return Phi(Loop,1,Phi(Region,Phi_a,(((Phi_a+1)<<32)>>32),Phi_a));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -185,7 +185,7 @@ while(arg < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,Phi(Loop,1,(Phi_a+1)),Add);", code.print());
+ assertEquals("return Phi(Region,Phi(Loop,1,(((Phi_a+1)<<32)>>32)),Sar);", code.print());
assertTrue(code.ctrl() instanceof RegionNode);
}
@@ -201,7 +201,7 @@ while(1) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return (Phi(Loop,1,Add)+1);", code.print());
+ assertEquals("return (((Phi(Loop,1,Sar)+1)<<32)>>32);", code.print());
assertTrue(code.ctrl() instanceof CProjNode prj && prj._idx==1 );
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter09Test.java
index 7025b98a..6be01487 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter09Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter09Test.java
@@ -41,7 +41,7 @@ else {
return x;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,(arg<<1),(Shl+1));", code.print());
+ assertEquals("return Phi(Region,(arg<<1),(((((Shl<<32)>>32)+1)<<32)>>32));", code.print());
Assert.assertEquals( "2", Eval2.eval(code, 1));
Assert.assertEquals("23", Eval2.eval(code, 11));
}
@@ -182,7 +182,7 @@ while(arg) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(-(Phi_a+3)));", code.print());
+ assertEquals("return Phi(Loop,0,(((-(Phi_a+3))<<32)>>32));", code.print());
}
@Test
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter10Test.java
index eadaf9fe..559def2f 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter10Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter10Test.java
@@ -74,7 +74,8 @@ public class Chapter10Test {
CodeGen code = new CodeGen("""
val f = { int n -> (n+1)&7; };
int x = 0;
- if (!!(arg && (x=f(arg)))) return x;
+ int n=arg;
+ if (!!(n && (x=f(n)))) return x;
return -1;
""").driver(CodeGen.Phase.TypeCheck);
assertEquals("-1",Eval2.eval(code,0));
@@ -181,7 +182,7 @@ struct s0 { int v0; };
arg=0+new s0.0;
""");
try { code.parse(); fail(); }
- catch( Exception e ) { assertEquals("Expected an identifier, found 'null'",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Expected an identifier, found '0'",e.getMessage()); }
}
@Test
@@ -196,7 +197,7 @@ while (arg) {
return bar.a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("Stop[ return Phi(Loop,0,(Phi_a+2)); return Test.Bar; ]", code.print());
+ assertEquals("Stop[ return ((Phi(Loop,0,(Sar+2))<<32)>>32); return Test.Bar; ]", code.print());
}
@Test
@@ -318,7 +319,7 @@ while( i.x < i.len ) {
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("Stop[ return Phi(Loop,0,(Phi(Loop,0,(Phi_x+1))+Phi_sum)); return Test.Iter; ]", code.print());
+ assertEquals("Stop[ return Phi(Loop,0,(((Phi_sum+((Phi(Loop,0,(((Sar+1)<<32)>>32))<<32)>>32))<<32)>>32)); return Test.Iter; ]", code.print());
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter12Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter12Test.java
index c6817308..8b7c5eef 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter12Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter12Test.java
@@ -127,7 +127,7 @@ N n = new N(null);
return n.next;
""");
try { code.driver(CodeGen.Phase.TypeCheck); fail(); }
- catch( Exception e ) { assertEquals("Argument #0 isa null, but must be a *Test.M {i64 !m; }",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Argument #0 isa null, but must be a *Test.M {i32 !m; }",e.getMessage()); }
}
@Test
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter13Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter13Test.java
index 214bc69e..70cfe25e 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter13Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter13Test.java
@@ -35,7 +35,7 @@ while( prime <= arg ) {
return primeCount;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,0,Phi(Loop,1,Phi(Region,Phi_primeCount,Phi_primeCount,(Phi_primeCount+1))));", code.print());
+ assertEquals("return Phi(Region,0,Phi(Loop,1,Phi(Region,Phi_primeCount,Phi_primeCount,(((Phi_primeCount+1)<<32)>>32))));", code.print());
assertEquals("0", Eval2.eval(code, 1)); // No primes 1 or below
assertEquals("1", Eval2.eval(code, 2)); // 2
assertEquals("2", Eval2.eval(code, 3)); // 2, 3
@@ -116,7 +116,7 @@ if (v1) {
return v0.f;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,.f,2);", code.print());
+ assertEquals("return ((Phi(Region,((.f<<32)>>32),2)<<32)>>32);", code.print());
}
@@ -134,7 +134,7 @@ while (arg) v.f = 2;
return i;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return arg;", code.print());
+ assertEquals("return ((arg<<32)>>32);", code.print());
}
@Test
@@ -151,7 +151,7 @@ while(1) {
return v.f;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,2);", code.print());
+ assertEquals("return ((Phi(Loop,0,2)<<32)>>32);", code.print());
for( int arg : new int[]{-1,0,1,3,100} )
assertEquals("0",Eval2.eval(code,arg));
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java
index e058a822..9a3bb87a 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java
@@ -12,8 +12,10 @@ public class Chapter14FloatTest {
{"int rounded=arg/2.5; return rounded;", "6", "-6"},
{"int rounded=0; rounded=arg/2.5; return rounded;", "6", "-6"},
{"i8 rounded=arg*16.0; return rounded;", "16", "-16"},
- {"int rounded=1e100; return rounded;", "9223372036854775807", "9223372036854775807"},
- {"int rounded=-1e100; return rounded;", "-9223372036854775808", "-9223372036854775808"},
+ {"int rounded=1e100; return rounded;", "-1", "-1"},
+ {"int rounded=-1e100; return rounded;", "0", "0"},
+ {"i64 rounded=1e100; return rounded;", "9223372036854775807", "9223372036854775807"},
+ {"i64 rounded=-1e100; return rounded;", "-9223372036854775808", "-9223372036854775808"},
{"int rounded=1e300*1e300-1e300*1e300; return rounded;", "0", "0"},
{"f32 rounded=arg+16777216.0; return rounded;", "1.6777232E7", "1.6777199E7"}
};
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter15Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter15Test.java
index c2071cb6..97c3af46 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter15Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter15Test.java
@@ -157,7 +157,7 @@ return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
assertEquals("return []*Test._A?;", code.print());
- assertEquals("*Test._A {i64 !i; }?[ null,null]", Eval2.eval(code, 0));
+ assertEquals("*Test._A {i32 !i; }?[ null,null]", Eval2.eval(code, 0));
}
@Test
@@ -312,7 +312,7 @@ flt is = new int[2];
return is;
""");
try { code.driver(CodeGen.Phase.TypeCheck); fail(); }
- catch( Exception e ) { assertEquals("Type *[]i64 is not of declared type flt",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Type *[]i32 is not of declared type flt",e.getMessage()); }
}
@Test
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter16Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter16Test.java
index 2942cfee..09bd9793 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter16Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter16Test.java
@@ -77,7 +77,7 @@ return z.x;
struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int ii -> next=n; i=ii; }; };
!_LLI? !head = null;
while( arg ) {
- head = new _LLI(head,arg);
+ int n=arg; head = new _LLI(head,n);
arg = arg-1;
}
if( !head ) return 0;
@@ -107,7 +107,7 @@ struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int a ->
}; };
!_LLI? !head = null;
while( arg ) {
- head = new _LLI(head,arg);
+ int n=arg; head = new _LLI(head,n);
arg = arg-1;
}
if( !head ) return 0;
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
index 98197b77..12e54193 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
@@ -409,7 +409,7 @@ var b = arg ? new _Bar : null;
return b ? b.x++ + b.x++ : -1;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,((.x<<1)+1),-1);", code.print());
+ assertEquals("return Phi(Region,((((.x+1)<<32)>>32)+ld_x),-1);", code.print());
assertEquals("-1", Eval2.eval(code, 0));
assertEquals("1", Eval2.eval(code, 1));
@@ -423,7 +423,7 @@ var b = arg ? new _Bar;
return b ? b.x++ + b.x++ : -1;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,((.x<<1)+1),-1);", code.print());
+ assertEquals("return Phi(Region,((((.x+1)<<32)>>32)+ld_x),-1);", code.print());
assertEquals("-1", Eval2.eval(code, 0));
assertEquals("1", Eval2.eval(code, 1));
}
@@ -475,7 +475,7 @@ for( int i=0; i<arg; i++ )
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+Phi(Loop,0,(((Phi_i+1)<<32)>>32)))<<32)>>32));", code.print());
assertEquals("3", Eval2.eval(code, 3));
assertEquals("45", Eval2.eval(code, 10));
}
@@ -488,7 +488,7 @@ for( ; i<arg; i++ )
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+Phi(Loop,0,(((Phi_i+1)<<32)>>32)))<<32)>>32));", code.print());
assertEquals("3", Eval2.eval(code, 3));
assertEquals("45", Eval2.eval(code, 10));
}
@@ -503,7 +503,7 @@ for( int i=0; ; i++ ) {
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+Phi(Loop,0,(((Phi_i+1)<<32)>>32)))<<32)>>32));", code.print());
assertEquals("3", Eval2.eval(code, 3));
assertEquals("45", Eval2.eval(code, 10));
}
@@ -516,7 +516,7 @@ for( int i=0; i<arg; )
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+Phi(Loop,0,(((Phi_i+1)<<32)>>32)))<<32)>>32));", code.print());
assertEquals("3", Eval2.eval(code, 3));
assertEquals("45", Eval2.eval(code, 10));
}
@@ -578,8 +578,9 @@ return new _A(new !B?[0]).array_of_b;
CodeGen code = new CodeGen("""
struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int ii -> next=n; i=ii; }; };
!_LLI? !head = null;
-while( arg-- )
- head = new _LLI(head,arg);
+while( arg-- ) {
+ int n=arg; head = new _LLI(head,n);
+}
int sum=0;
var ptr = head; // A read-only ptr, to be assigned from read-only next fields
for( ; ptr; ptr = ptr.next )
@@ -587,7 +588,7 @@ for( ; ptr; ptr = ptr.next )
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(Phi_sum+.i));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+.i)<<32)>>32));", code.print());
assertEquals("45", Eval2.eval(code, 10));
}
@@ -618,8 +619,8 @@ for( int j=0; j<nprimes; j++ )
return rez;
""");
code.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule);
- assertEquals("return []i64;", code.print());
- assertEquals("i64[ 2,3,5,7,11,13,17,19]",Eval2.eval(code, 20));
+ assertEquals("return []i32;", code.print());
+ assertEquals("i32[ 2,3,5,7,11,13,17,19]",Eval2.eval(code, 20));
}
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter18Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter18Test.java
index ecb2b934..b1caae96 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter18Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter18Test.java
@@ -49,9 +49,9 @@ public class Chapter18Test {
}
@Test public void testFunctionLocalConstantChains() {
- String src = "val f = { int x -> x ? f(x-1)*305420988+x/305420988 : 1; }; "+
- "val g = { int x -> x ? g(x-1)/305420988+x*305420988 : 1; }; "+
- "return f(arg)+g(arg);";
+ String src = "val f = { int x -> int prev=x-1; x ? f(prev)*305420988+x/305420988 : 1; }; "+
+ "val g = { int x -> int prev=x-1; x ? g(prev)/305420988+x*305420988 : 1; }; "+
+ "int n=arg; return f(n)+g(n);";
CodeGen code = new CodeGen(src);
code.driver(CodeGen.Phase.Schedule,"riscv","SystemV");
var owners = new java.util.IdentityHashMap<CFGNode,Boolean>();
@@ -173,8 +173,8 @@ return x2;
};
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("Stop[ return { sq}; return (Parm_x(sq,i64)*x); ]", code.print());
- assertEquals("{ ptr i64 -> i64 #[ 3]}", Eval2.eval(code, 3));
+ assertEquals("Stop[ return { sq}; return (Parm_x(sq,i32)*x); ]", code.print());
+ assertEquals("{ ptr i32 -> i64 #[ 3]}", Eval2.eval(code, 3));
}
@Test
@@ -184,10 +184,10 @@ return x2;
var _sq = { int x ->
x*x;
};
-return _sq(arg)+_sq(3);
+int n=arg; return _sq(n)+_sq(3);
""");
code.driver(Phase.LocalSched);
- assertEquals("return ((arg*arg)+9);", code.print());
+ assertEquals("return ((((arg<<32)>>32)*Sar)+9);", code.print());
assertEquals("13", Eval2.eval(code, 2));
}
@@ -200,8 +200,8 @@ int cnt=1; // Global scope; exactly one of these
return { -> cnt++; }; // Escape global-scope variable is OK
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("Stop[ return { ptr -> i64 #[ 3]}; return .cnt; ]", code.print());
- assertEquals("{ ptr -> i64 #[ 3]}", Eval2.eval(code, 0));
+ assertEquals("Stop[ return { ptr -> i32 #[ 3]}; return .cnt; ]", code.print());
+ assertEquals("{ ptr -> i32 #[ 3]}", Eval2.eval(code, 0));
}
// Function scope test
@@ -244,7 +244,7 @@ var fcn = arg ? { int x -> x*x; } : { int x -> x+x; };
return fcn(3);
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("Stop[ return #2; return (Parm_x($fun3,i64,3)*x); return (Parm_x($fun4,i64,3)<<1); ]", code.print());
+ assertEquals("Stop[ return #2; return (Parm_x($fun3,i32,3)*x); return (Parm_x($fun4,i32,3)<<1); ]", code.print());
assertEquals("6", Eval2.eval(code, 0));
assertEquals("9", Eval2.eval(code, 1));
}
@@ -252,10 +252,10 @@ return fcn(3);
// Recursive factorial test
@Test
public void testFcn5() {
- CodeGen code = new CodeGen("val _fact = { int x -> x <= 1 ? 1 : x*_fact(x-1); }; return _fact(arg);");
+ CodeGen code = new CodeGen("val _fact = { int x -> int prev=x-1; x <= 1 ? 1 : x*_fact(prev); }; int n=arg; return _fact(n);");
code.driver(CodeGen.Phase.TypeCheck);
String ir = code.print();
- assertTrue(ir.equals("Stop[ return #2; return Phi(Region,1,(Parm_x(_fact,arg,(x-1))*#2)); ]") ||
+ assertTrue(ir,ir.equals("Stop[ return #2; return Phi(Region,1,(Parm_x(_fact,((arg<<32)>>32),(((x-1)<<32)>>32))*#2)); ]") ||
ir.equals("Stop[ return Phi(Region,1,(arg*#2)); return Phi(Region,1,(Parm_x(_fact,(arg-1),(x-1))*#2)); ]") ||
ir.equals("Stop[ return Phi(Region,1,(arg*Phi(Region,1,((arg-1)*#2)))); return Phi(Region,1,(Parm_x(_fact,(Sub-1),(x-1))*#2)); ]"));
assertEquals( "1", Eval2.eval(code, 0));
@@ -270,7 +270,7 @@ return fcn(3);
// recursive result, but the floating base case requires a floating mode.
@Test
public void testFcn5Float() {
- CodeGen code = new CodeGen("val _fact = { int x -> x <= 1 ? 1.0 : x*_fact(x-1); }; return _fact(arg);");
+ CodeGen code = new CodeGen("val _fact = { int x -> int prev=x-1; x <= 1 ? 1.0 : x*_fact(prev); }; int n=arg; return _fact(n);");
code.driver(CodeGen.Phase.TypeCheck);
assertEquals("1.0", Eval2.eval(code, 0));
assertEquals("1.0", Eval2.eval(code, 1));
@@ -281,7 +281,7 @@ return fcn(3);
@Test
public void testFcn5Minus() {
- CodeGen code = new CodeGen("val _neg = { int x -> x ? -_neg(x-1) : 1; }; return _neg(arg);");
+ CodeGen code = new CodeGen("val _neg = { int x -> int prev=x-1; x ? -_neg(prev) : 1; }; int n=arg; return _neg(n);");
code.driver(CodeGen.Phase.TypeCheck);
assertEquals( "1", Eval2.eval(code, 0));
assertEquals("-1", Eval2.eval(code, 1));
@@ -324,14 +324,14 @@ return 2;
{int -> int}? !_i2i = null;
var _id = {{int->int} f-> return f;};
for(;;) {
- if (_i2i) return _i2i(arg);
+ if (_i2i) { int n=arg; return _i2i(n); }
var _x = {int i-> return i;};
arg = _x(3);
_i2i = _id(_x);
}
""");
code.driver(Phase.LocalSched);
- assertEquals("return Phi(Loop,arg,3);", code.print());
+ assertEquals("return ((Phi(Loop,arg,3)<<32)>>32);", code.print());
assertEquals("3", Eval2.eval(code, 0));
}
@@ -407,7 +407,7 @@ for(;;) {
return 0;
""");
try { code.driver(CodeGen.Phase.TypeCheck); fail(); }
- catch( Exception e ) { assertEquals("Might be null calling { ptr i64 -> i64 #[ 3]}?",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Might be null calling { ptr i32 -> i32 #[ 3]}?",e.getMessage()); }
}
@@ -419,7 +419,7 @@ val f = { int i, int j -> return i+j; };
return f();
""");
try { code.driver(CodeGen.Phase.TypeCheck); fail(); }
- catch( Exception e ) { assertEquals("Expecting 3 arguments, but found 1",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Expecting 2 arguments, but found 0",e.getMessage()); }
}
@@ -457,9 +457,9 @@ return g();
public void testFcnMutRec() {
CodeGen code = new CodeGen(
"""
-val is_even = { int x -> x ? is_odd (x-1) : true ; };
-val is_odd = { int x -> x ? is_even(x-1) : false; };
-return is_even(arg);
+val is_even = { int x -> int prev=x-1; x ? is_odd (prev) : true ; };
+val is_odd = { int x -> int prev=x-1; x ? is_even(prev) : false; };
+int n=arg; return is_even(n);
""");
code.driver(CodeGen.Phase.TypeCheck);
assertEquals("1", Eval2.eval(code, 0));
@@ -496,11 +496,11 @@ for(;;) {
{{int->int}->{int->int}}! _f2f = {{int->int} f->return f;};
val _o = _i2i_noInline;
if (arg) _i2i_noInline = null;
-if (_i2i_noInline) return _i2i_noInline(arg);
+if (_i2i_noInline) { int n=arg; return _i2i_noInline(n); }
return _f2f(_o)(1);
""");
code.driver(Phase.LocalSched);
- assertEquals("Stop[ return Phi(Region,#2,#2); return Parm_i(_i2i_noInline,i64); ]", code.print());
+ assertEquals("Stop[ return Phi(Region,#2,#2); return Parm_i(_i2i_noInline,i32); ]", code.print());
assertEquals("1", Eval2.eval(code, 2));
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter19Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter19Test.java
index 50ca8922..50fecdfd 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter19Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter19Test.java
@@ -32,14 +32,14 @@ public class Chapter19Test {
case "u16" -> arg & 0xffffL;
case "i16" -> (long)(short)arg;
case "u32" -> arg & 0xffffffffL;
- case "i32" -> (long)(int)arg;
+ case "i32", "int" -> (long)(int)arg;
case "f32" -> (double)(float)arg;
case "flt" -> (double)arg;
default -> arg;
};
String msg = type+" array="+array+" arg="+arg;
String printed = array
- ? (type.equals("u8") ? ""+(char)(long)(Long)expected : (type.equals("int") ? "i64" : type)+"[ "+expected+"]")
+ ? (type.equals("u8") ? ""+(char)(long)(Long)expected : (type.equals("int") ? "i32" : type)+"[ "+expected+"]")
: "Test.S{x="+expected+"}";
assertEquals(msg,printed,Eval2.eval(code,arg));
}
@@ -118,8 +118,8 @@ val hashCode = { !String self ->
hashCode(new String("Hello, World!"));
""";
CodeGen code = new CodeGen(src).driver(Phase.LocalSched);
- assertEquals("Stop[ return Phi(Region,123456789,(!=0)Phi(Loop,0,(((Phi_hash<<5)-Phi_hash)+.[]))); return Test.String; return Phi(Region,0,1,0,1); return Phi(Region,(!=0)._hashCode,Phi(Region,123456789,(!=0)Phi(Loop,0,(((Phi_hash<<5)-Phi_hash)+.[])))); ]", code.print());
- assertEquals("4029215624828139541", Eval2.eval(code, 2));
+ assertEquals("Stop[ return Phi(Region,123456789,(!=0)Phi(Loop,0,(((((Phi_hash<<5)-Phi_hash)+.[])<<32)>>32))); return Test.String; return Phi(Region,0,1,0,1); return Phi(Region,(!=0)._hashCode,Phi(Region,123456789,(!=0)Phi(Loop,0,(((((Phi_hash<<5)-Phi_hash)+.[])<<32)>>32)))); ]", code.print());
+ assertEquals("1498789909", Eval2.eval(code, 2));
}
@Test
@@ -251,7 +251,7 @@ hashCode(new String("Hello"));
int arg1 = arg + 1;
return arg1 / arg;""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (div,(inc,arg),arg);", code.print());
+ assertEquals("return (div,(sari,(shli,(inc,arg))),arg);", code.print());
}
@Test
@@ -262,7 +262,7 @@ int arg1 = arg + 1;
return arg1 * arg;
""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (mul,(inc,arg),arg);", code.print());
+ assertEquals("return (mul,(sari,(shli,(inc,arg))),arg);", code.print());
}
@Test
@@ -272,7 +272,7 @@ int a = arg;
return a + 2.0;
"""
).driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (addf,(cvtf,arg),2.0f);", code.print());
+ assertEquals("return (addf,(cvtf,(sari,(shli,arg))),2.0f);", code.print());
}
@Test
@@ -287,7 +287,7 @@ else {
}
return a;""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return Phi(Region,(addi,arg),(addi,arg));", code.print());
+ assertEquals("return (sari,(shli,Phi(Region,(addi,arg),(addi,arg))));", code.print());
}
@Test
@@ -302,7 +302,7 @@ else
a=b+1;
return a+b;""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (add,(add,Phi(Region,(shli,arg),arg),arg),Phi(Region,4,5));", code.print());
+ assertEquals("return (add,(sari,(shli,(inc,Phi(Region,arg,(sari,(shli,(addi,arg))))))),(sari,(shli,(add,Phi(Region,(sari,(shli,(inc,arg))),2),Phi(Region,sari,arg)))));", code.print());
}
@Test
@@ -314,7 +314,7 @@ for( int i=0; i<arg; i++ )
sum += i;
return sum;""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return Phi(Loop,0,(add,Phi_sum,Phi(Loop,0,(inc,Phi_i))));", code.print());
+ assertEquals("return Phi(Loop,0,(sari,(shli,(add,Phi_sum,Phi(Loop,0,(sari,(shli,(inc,Phi_i))))))));", code.print());
}
@Test
@@ -331,21 +331,21 @@ return new _S;""");
public void testLea1() {
CodeGen code = new CodeGen("int x = arg/3; return arg+x+7;");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (lea,arg,(div,arg,3));", code.print());
+ assertEquals("return (lea,arg,(sari,(shli,(div,arg,3))));", code.print());
}
@Test
public void testLea2() {
CodeGen code = new CodeGen("int x = arg/3; return arg+x*4+7;");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (lea,arg,(div,arg,3));", code.print());
+ assertEquals("return (lea,arg,(sari,(shli,(div,arg,3))));", code.print());
}
@Test
public void testLea3() {
CodeGen code = new CodeGen("int x = arg/3; return x*4+arg;");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (lea,arg,(div,arg,3));", code.print());
+ assertEquals("return (lea,arg,(sari,(shli,(div,arg,3))));", code.print());
}
@Test
@@ -359,7 +359,7 @@ return new _S;""");
public void testAlloc3() {
CodeGen code = new CodeGen("int[] !xs = new int[3]; xs[arg]=1; return xs[arg&1]+3;");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return .[];", code.print());
+ assertEquals("return (addi,.[]);", code.print());
}
@Test
@@ -376,7 +376,7 @@ for( int i=0; i<ary#-1; i++ )
return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6
""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return .[];", code.print());
+ assertEquals("return (add,.[],(muli,.[]));", code.print());
}
@Test
@@ -479,7 +479,7 @@ val _fcn = arg ? { int x -> x*x; } : { int x -> x+x; };
return _fcn(2)*10 + _fcn(3);
""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("Stop[ return (add,#2,(muli,#2)); return (mul,Parm_x($fun3,[2-3]),x); return (shli,Parm_x($fun4,[2-3])); ]", code.print());
+ assertEquals("Stop[ return (add,#2,(muli,#2)); return (mul,(sext32,Parm_x($fun3,[2-3])),sext32); return (shli,(sext32,Parm_x($fun4,[2-3]))); ]", code.print());
}
@Test
@@ -487,9 +487,9 @@ return _fcn(2)*10 + _fcn(3);
CodeGen code = new CodeGen(
"""
val _sq = { int x -> x*x; };
-return _sq(arg) + _sq(3);
+int n=arg; return _sq(n) + _sq(3);
""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (addi,(mul,arg,arg));", code.print());
+ assertEquals("return (addi,(mul,(sari,(shli,arg)),sari));", code.print());
}
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter20Test.java
index f5eb2b36..273d76ba 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter20Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter20Test.java
@@ -10,6 +10,7 @@ import static org.junit.Assert.*;
public class Chapter20Test {
@Test public void testAllocatorTopPhi() { com.seaofnodes.simple.codegen.RegAllocTestSupport.topPhi(); }
+ @Test public void testIndependentFunctions() { com.seaofnodes.simple.codegen.RegAllocTestSupport.independentFunctions(); }
@Test public void testNeverReturnAllocation() {
String[] sources = {
@@ -141,7 +142,8 @@ val _sqrt = { int x ->
guess = next;
}
};
-return _sqrt(arg) + _sqrt(arg+2);
+int x=arg, y=arg+2;
+return _sqrt(x) + _sqrt(y);
""";
testCPU(src,"x86_64_v2", "win64" ,38,null);
testCPU(src,"riscv" , "SystemV",19,null);
@@ -216,9 +218,9 @@ for( int i=0; i<ary#-1; i++ )
ary[i+1] += ary[i];
return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6
""";
- testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);");
- testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));");
- testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"x86_64_v2", "SystemV",-1,"return (add,.[],(muli,.[]));");
+ testCPU(src,"riscv" , "SystemV",9,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"arm" , "SystemV",13,"return (add,.[],(mul,.[],1000));");
}
@@ -259,7 +261,7 @@ val hashCode = { !String self ->
""";
// Return the public hashCode wrapper; keep its helper private.
- testCPU(src,"x86_64_v2", "SystemV",12,null);
+ testCPU(src,"x86_64_v2", "SystemV",28,null);
testCPU(src,"riscv" , "SystemV",8,null);
testCPU(src,"arm" , "SystemV",6,null);
}
@@ -267,7 +269,7 @@ val hashCode = { !String self ->
@Test
public void testCast() {
String src = "struct Bar { int x; }; var b = arg ? new Bar; return b ? b.x++ + b.x++ : -1;";
- testCPU(src,"x86_64_v2", "SystemV",3,null);
+ testCPU(src,"x86_64_v2", "SystemV",5,null);
testCPU(src,"riscv" , "SystemV",6,null);
testCPU(src,"arm" , "SystemV",6,null);
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter21Test.java
index e15c2581..c6d558eb 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -17,7 +17,7 @@ import static org.junit.Assert.*;
public class Chapter21Test {
@Test public void testFloatToInteger() throws IOException {
- String src = "val float_to_int={ flt x -> int i=x; return i; };"+
+ String src = "val float_to_int={ flt x -> i64 i=x; return i; };"+
"val float_to_u8={ flt x -> u8 i=x; return i; };";
TestC.runC(src,"floatToInteger","",-1);
for( double x : new double[]{0.0,-0.0,6.92,-6.92,255.75,-257.75,
@@ -138,9 +138,10 @@ public class Chapter21Test {
String src = """
val sum = { int n ->
if( n<=0 ) return 1;
- return sum(n-1)+n;
+ int prev=n-1;
+ return sum(prev)+n;
};
- val run = { int n -> return sum(n)+sum(n+1); };
+ val run = { int n -> int next=n+1; return sum(n)+sum(next); };
""";
assertTrue("Ordinary calls must survive optimization",checkArmCalls(src,"run",4,27)>0);
}
@@ -157,7 +158,7 @@ public class Chapter21Test {
buf=buf2;
};
val add = { int e ->
- if( len>=buf# ) grow(buf#*2);
+ if( len>=buf# ) { int size=buf#*2; grow(size); }
buf[len++]=e;
return self;
};
@@ -323,9 +324,9 @@ for( int i=0; i<ary#-1; i++ )
ary[i+1] += ary[i];
return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6
""";
- testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);");
- testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));");
- testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"x86_64_v2", "SystemV",-1,"return (add,.[],(muli,.[]));");
+ testCPU(src,"riscv" , "SystemV",9,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"arm" , "SystemV",13,"return (add,.[],(mul,.[],1000));");
}
@Test
@@ -384,7 +385,7 @@ val hashCode = { !String self ->
};
""";
// Return the public hashCode wrapper; keep its helper private.
- testCPU(src,"x86_64_v2", "SystemV",12,null);
+ testCPU(src,"x86_64_v2", "SystemV",28,null);
testCPU(src,"riscv" , "SystemV",8,null);
testCPU(src,"arm" , "SystemV",6,null);
}
@@ -423,7 +424,7 @@ val hashCode = { !String self ->
: (self._hashCode = _hashCodeString(self));
};
""";
- TestC.runC(src, "stringHash", "",12);
+ TestC.runC(src, "stringHash", "",28);
}
@Test public void testLoop2() {
@@ -439,9 +440,9 @@ while(true) {
}
return i;
""";
- testCPU(src,"x86_64_v2", "win64" ,0,"return (!=0)(inc,Phi(Loop,0,Phi(Region,(!=0),(!=0),0)));");
- testCPU(src,"riscv" , "SystemV",0,"return (!=0)( Phi(Loop,0,Phi(Region,(!=0),(!=0),0)) + #1 );");
- testCPU(src,"arm" , "SystemV",0,"return (!=0)(inc,Phi(Loop,0,Phi(Region,(!=0),(!=0),0)));");
+ testCPU(src,"x86_64_v2", "win64" ,0,"return (!=0)(sari,(shli,(inc,Phi(Loop,0,Phi(Region,(!=0),(!=0),0)))));");
+ testCPU(src,"riscv" , "SystemV",0,"return (!=0)( ( ( Phi(Loop,0,Phi(Region,(!=0),(!=0),0)) + #1 ) << #32 ) >> #1056 );");
+ testCPU(src,"arm" , "SystemV",0,"return (!=0)(asri,(lsli,(inc,Phi(Loop,0,Phi(Region,(!=0),(!=0),0)))));");
}
@Test public void testNewtonExport() throws IOException {
@@ -530,7 +531,7 @@ 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, 186);
+ TestC.runC(src, "sieve", sprimes,344);
// Evaluate on RISC5 emulator; expect return of an array of primes in
// the simulated heap.
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter22Test.java
index e9a668a7..9bebee23 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter22Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter22Test.java
@@ -18,7 +18,7 @@ import static org.junit.Assert.*;
public class Chapter22Test {
@Test public void testNarrowCReturnsEmulated() {
- String[] types = {"i8","u8","i16","u16","i32","u32","int"};
+ String[] types = {"i8","u8","i16","u16","i32","u32","i64","int"};
long[] values = {0xa5a5a5a500000000L,0xa5a5a5a50000007fL,0xa5a5a5a500000080L,
0xa5a5a5a500007fffL,0xa5a5a5a500008000L,0xa5a5a5a57fffffffL,
0xa5a5a5a580000000L,0xa5a5a5a5ffffffffL};
@@ -35,7 +35,7 @@ public class Chapter22Test {
long expected = switch(i) {
case 0 -> (byte)value; case 1 -> value&255;
case 2 -> (short)value; case 3 -> value&65535;
- case 4 -> (int)value; case 5 -> value&0xffffffffL;
+ case 4, 7 -> (int)value; case 5 -> value&0xffffffffL;
default -> value;
};
byte[] image = new byte[1<<16];
@@ -70,7 +70,7 @@ public class Chapter22Test {
{i8} c_i8="C"; {u8} c_u8="C";
{i16} c_i16="C"; {u16} c_u16="C";
{i32} c_i32="C"; {u32} c_u32="C";
- {int} c_i64="C";
+ {i64} c_i64="C";
val test_i8={->return c_i8();};
val test_u8={->return c_u8();};
val test_i16={->return c_i16();};
@@ -344,7 +344,7 @@ val _sum = { i32[~] is -> // final array
return _sum(is);
""";
CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
- assertEquals("return Phi(Loop,0,(Phi_sum+.[]));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+.[])<<32)>>32));", code.print());
assertEquals("14", Eval2.eval(code, 0));
}
@@ -354,7 +354,8 @@ return _sum(is);
String src =
"""
// Echo stdin to stdout.
-return sys.io.p( sys.io.stdin() );
+val input=sys.io.stdin();
+return input ? sys.io.p(input) : -1;
""";
TestC.runSF(src,"echo", "",0);
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter23Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter23Test.java
index df02d3b4..89e41efa 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter23Test.java
+++ b/chapter30/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",23,null);
+ testCPU(src,"x86_64_v2", "win64",31,null);
};
@Test
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter24Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter24Test.java
index 2bc2ce7f..f4c4f12f 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter24Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter24Test.java
@@ -20,9 +20,9 @@ public class Chapter24Test {
@Test public void testRecursiveWidening() {
String[] srcs = {
// Parm(x) must widen even though the recursive result is constant.
- "val f = { int x, int limit -> x >= limit ? 0 : f(x+1,limit); }; return f(0,arg);",
+ "val f = { int x, int limit -> x >= limit ? 0 : f(++x,limit); }; int limit=arg; return f(0,limit);",
// The growing result has its own cycle, which never crosses Parm(x).
- "val f = { int x -> x <= 0 ? 0 : f(x-1)+1; }; return f(3);"
+ "val f = { int x -> x <= 0 ? 0 : f(--x)+1; }; return f(3);"
};
for( int i=0; i<srcs.length; i++ )
for( long seed : new long[]{0,42,126} ) {
@@ -32,6 +32,7 @@ public class Chapter24Test {
}
@Test public void testColdLoopSelfConflict() { com.seaofnodes.simple.codegen.RegAllocTestSupport.coldLoopSelfConflict(); }
+ @Test public void testTiedLoopBackedges() { com.seaofnodes.simple.codegen.RegAllocTestSupport.tiedLoopBackedges(); }
@Test public void testPrintingDeadFunction() throws Exception {
var code = new CodeGen("return 0;").parse();
@@ -106,7 +107,7 @@ return (sq_noInline(2) <= score < sq_noInline(3))
? 0 // Expected
: 1; // Error
""";
- TestC.runSF( src, "stacked_r_3", "", 2);
+ TestC.runSF( src, "stacked_r_3", "",0);
// Evaluate on RISC5 emulator
EvalRisc5 R5 = TestRisc5.build( src, "stacked_r_3", 0, 2, false);
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 5f615282..67e7f982 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -142,8 +142,9 @@ public class Chapter25Test {
CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}),
"Client","""
val _other={int n -> n-1;};
- val inc={int n -> n+1;};
- return Lib.apply_noInline(inc,arg);
+ val inc={int n -> n+=1; return n;};
+ int n=arg;
+ return Lib.apply_noInline(inc,n);
""",987654321,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
var clz=(com.seaofnodes.simple.type.TypeStruct)Parser.TYPES.get("class:Lib");
int hidden=((com.seaofnodes.simple.type.TypeFunPtr)clz.field("_hidden_noInline")._t).fidx();
@@ -169,7 +170,7 @@ public class Chapter25Test {
String[] sources={
"val _spin={ -> while(1) {} return 1; }; _spin(); return 2;",
// Two callers require cloning, including the non-exiting arm.
- "val _spin={int x -> if(x) while(1) {} return 7; }; return _spin(arg)+_spin(arg);"
+ "val _spin={int x -> if(x) while(1) {} return 7; }; int n=arg; return _spin(n)+_spin(n);"
};
for( int i=0; i<sources.length; i++ ) {
for( long seed : new long[]{1,123,987654321} ) {
@@ -191,6 +192,9 @@ public class Chapter25Test {
i8 small="C";
f64 fraction="C";
{int} bump="C";
+ val extern_matches={i32 x -> counter==x;};
+ val extern_less={i32 x -> x<counter;};
+ val extern_seven={ -> counter==7;};
val test_global={ ->
val before=counter;
counter=before;
@@ -471,9 +475,9 @@ public class Chapter25Test {
// Elf files are sane
- // Sys depends on io, libc, char, collections, and array utilities.
+ // Sys depends on io, libc, text, char, collections, and array utilities.
assertArrayEquals(new String[]{"sys/adt", "sys/ary",
- "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/scan"},sys_elf._deps);
+ "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
assertArrayEquals(new String[]{"sys"},sys_elf._units);
assertSame("class:sys" ,sys_elf._clz._name);
}
@@ -496,7 +500,7 @@ public class Chapter25Test {
@Test
public void testHelloWorldDriver() throws Exception {
String out = "build/objs/helloWorldDriver/";
- Simple.main(new String[]{"-L",SYS_BLDDIR,"-o",out,"--norun","src/test/smp/examples/A_helloWorld.smp"});
+ Simple.main(new String[]{"-L",SYS_BLDDIR,"-o",out,"--norun","docs/examples/A_helloWorld.smp"});
assertTrue(Files.isRegularFile(Path.of(out,"A_helloWorld.o")));
}
@@ -507,7 +511,7 @@ public class Chapter25Test {
try( PrintStream capture = new PrintStream(output,true,StandardCharsets.UTF_8) ) {
System.setOut(capture);
Simple.main(new String[]{"-L",SYS_BLDDIR,"-o","build/objs/helloWorldDriverLibFile/",
- "src/test/smp/examples/A_helloWorld.smp"});
+ "docs/examples/A_helloWorld.smp"});
} finally {
System.setOut(saved);
}
@@ -554,7 +558,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2);
@Test
public void testBubbles() throws IOException {
- String src = Files.readString( Path.of("src/test/smp/examples/BubbleSort.smp"));
+ String src = Files.readString( Path.of("docs/examples/BubbleSort.smp"));
String exe = TestC.compile(src,"BubbleSort",new Ary<>(new String[]{SYS_BLDDIR}),
TestC.CALL_CONVENTION,null,null,-1);
assertEquals("[-17, 2, 3, 999]\n",
@@ -575,7 +579,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2);
@Test
public void testCapitalize() throws IOException {
- String src = Files.readString(Path.of("src/test/smp/examples/Capitalize.smp"));
+ String src = Files.readString(Path.of("docs/examples/Capitalize.smp"));
String exe = TestC.compile(src,"Capitalize",new Ary<>(new String[]{SYS_BLDDIR}),
TestC.CALL_CONVENTION,null,null,-1);
assertEquals("Hello world\n",TestC.exec(exe,"hello world"));
@@ -588,7 +592,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2);
@Test
public void testDijkstra() throws IOException {
- String src = Files.readString(Path.of("src/test/smp/examples/Dijkstra.smp"));
+ String src = Files.readString(Path.of("docs/examples/Dijkstra.smp"));
String matrix = "[0, 2, 0, 6, 0, 2, 0, 3, 8, 5, 0, 3, 0, 0, 7, 6, 8, 0, 0, 9, 0, 5, 7, 9, 0]";
String exe = TestC.compile(src,"Dijkstra",new Ary<>(new String[]{SYS_BLDDIR}),
TestC.CALL_CONVENTION,null,null,-1);
@@ -603,7 +607,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2);
@Test
public void testFileIO() throws IOException {
- String src = Files.readString(Path.of("src/test/smp/examples/FileIO.smp"));
+ String src = Files.readString(Path.of("docs/examples/FileIO.smp"));
String exe = TestC.compile(src,"FileIO",new Ary<>(new String[]{SYS_BLDDIR}),
TestC.CALL_CONVENTION,null,null,-1);
assertEquals("File I/O succeeded\n",TestC.exec(exe));
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/chapter30/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java
index dae64fb0..8f1d1ee8 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java
@@ -188,7 +188,7 @@ public class ClassInitDependenciesTest {
"B","int value=A.value;"),"A -> B -> A");
cycle("A",Map.of("A","int value=get(); val get={ -> B.value; };",
"B","int value=A.value;"),"A -> B -> A");
- cycle("A",Map.of("A","int value=0; if(arg) value=B.value;",
+ cycle("A",Map.of("A","int value=0; if(((arg<<32)>>32)) value=B.value;",
"B","int value=A.value;"),"A -> B -> A");
cycle("A",Map.of("A","val f=arg ? { -> B.value; } : { -> 0; }; int value=f();",
"B","int value=A.value;"),"A -> B -> A");
@@ -237,7 +237,7 @@ public class ClassInitDependenciesTest {
}
@Test public void testRecursionAndNativeCalls() throws Exception {
- CodeGen code=compile("A",Map.of("A","val f={int n -> n ? f(n-1) : B.value; }; int value=f(arg);",
+ CodeGen code=compile("A",Map.of("A","val f={int n -> n ? f(--n) : B.value; }; int value=f(((arg<<32)>>32));",
"B","int value=arg|1;"),123);
assertTrue(touches(code,new ClassInitDependencies(code),"Root/A","Root/B"));
code=compile("A",Map.of("A","{int} native=\"C\"; return native();"),123);
@@ -386,8 +386,8 @@ public class ClassInitDependenciesTest {
String message=ex.getMessage();
assertCycle(message,"Root.A -> Root.B -> Root.A");
// Inlining must keep Helper's source identity, not substitute A.
- assertTrue(message,message.contains("Helper.smp:2:4"));
- assertTrue(message,message.contains(" B.value;\n ---^"));
+ assertTrue(message,message.contains("Helper.smp:2:5"));
+ assertTrue(message,message.contains(" B.value;\n ____^"));
if( name.endsWith("noInline") ) {
assertTrue(message,message.contains("A.smp:1:31"));
assertTrue(message,message.contains("int value=Helper.read_noInline();"));
@@ -401,7 +401,7 @@ public class ClassInitDependenciesTest {
CodeGen code=new CodeGen(src).driver(CodeGen.Phase.Parse);
CompUnit cu=code._compunits.get("Test");
int pos=src.substring(0,src.indexOf('1')).getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
- assertEquals("Test.smp:2:9: error: example\n return 1;\n ---------------^",SourceError.format(cu,pos,"example"));
+ assertEquals("Test.smp:2:16: error: example\n return 1;\n _______________^",SourceError.format(cu,pos,"example"));
assertEquals("Test.smp:3:1: error: example\n \n ^",SourceError.format(cu,src.getBytes(java.nio.charset.StandardCharsets.UTF_8).length,"example"));
}
@@ -414,8 +414,8 @@ public class ClassInitDependenciesTest {
@Test public void testInitializationMemory() throws Exception {
String[] sources={
"int value=B.value+B.value;",
- "int value; if(arg) value=B.value; else value=B.value+1; value=value+B.value;",
- "int value; if(arg) value=B.value; value=value+B.value;"
+ "int value; if(((arg<<32)>>32)) value=B.value; else value=B.value+1; value=value+B.value;",
+ "int value; if(((arg<<32)>>32)) value=B.value; value=value+B.value;"
};
int[] counts={1,2,2};
for( long seed : new long[]{1,123,987654321} )
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/chapter30/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
index 285d4293..b9d65b1c 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -23,7 +23,7 @@ public class CompoundAssignmentTest {
@Test public void testLocals() {
for (String op : OPS)
for (long arg : new long[]{-8, 0, 5, 64}) {
- long expected = apply(op, arg, 3);
+ long expected = (int)apply(op, arg, 3);
check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
arg, expected*11);
}
@@ -31,7 +31,7 @@ public class CompoundAssignmentTest {
@Test public void testRhsOnce() {
for (String op : OPS) {
- long expected = apply(op, 5, 1);
+ long expected = (int)apply(op, 5, 1);
check("int x=arg; int count=0; int r=(x " + op +
" (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
}
@@ -42,7 +42,7 @@ public class CompoundAssignmentTest {
check("u8 x=255; x ^= 256; return x;", 0, 255);
check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
check("int x=-8; x >>= 65; return x;", 0, -4);
- check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ check("int x=-8; x >>>= 65; return x;", 0, -4);
}
@Test public void testExistingOperators() {
@@ -73,7 +73,7 @@ public class CompoundAssignmentTest {
@Test public void testFields() {
for (String op : OPS) {
- long expected = apply(op, -8, 3);
+ long expected = (int)apply(op, -8, 3);
check("struct S { int x; }; !S !s=new S; s.x=arg; int r=(s.x " +
op + " 3); return s.x*10+r;", -8, expected*11);
}
@@ -81,7 +81,7 @@ public class CompoundAssignmentTest {
@Test public void testArrayAddressAndRhsOnce() {
for (String op : OPS) {
- long expected = apply(op, 5, 1);
+ long expected = (int)apply(op, 5, 1);
check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " +
"int r=(a[i++] " + op + " (count=count+1)); " +
"return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11);
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java b/chapter30/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
index e544fcad..1eb64610 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
@@ -30,7 +30,7 @@ public class ConstructionGroupTest {
@Test public void testCyclesAndIndependentAllocations() {
String src = parent("val detached = new Child(null); payload = n+read(detached); child = new Child(self);")+
- "val p = new Parent(arg); val local = new Child(p); val par = local.parent; return (par ? par.payload : 0)+p.child.value;";
+ "val p = new Parent(((arg<<32)>>32)); val local = new Child(p); val par = local.parent; return (par ? par.payload : 0)+p.child.value;";
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
assertEquals("17",Eval2.eval(code,7));
@@ -44,7 +44,7 @@ public class ConstructionGroupTest {
// Child's own slots are initialized, even though its parent remains
// partial. Reading that slot does not complete the parent reference.
String src = parent("child = new Child(self); payload = child.parent==null;")+
- "return new Parent(arg).payload;";
+ "return new Parent(((arg<<32)>>32)).payload;";
assertEquals("0",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),0));
}
@@ -73,7 +73,7 @@ public class ConstructionGroupTest {
}
@Test public void testAliasesAndLoopBackedges() {
- String src = parent("val alias = self; alias.payload = n; child = new Child(alias);")+"return new Parent(arg).child.value;";
+ String src = parent("val alias = self; alias.payload = n; child = new Child(alias);")+"return new Parent(((arg<<32)>>32)).child.value;";
assertEquals("5",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),3));
rejects(parent("payload = 1; child = new Child(self); Child? !c = null; while(n--) { if(c) read(c); c = child; }")+
"return 0;","ordinary function");
@@ -116,7 +116,7 @@ public class ConstructionGroupTest {
head = p;
};
};
- val root = new Root(arg);
+ val root = new Root(((arg<<32)>>32));
return root.value + root.head.value;
""";
CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java b/chapter30/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
index 71a281b5..5a20388b 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
@@ -28,7 +28,7 @@ public class EscapeFlowTest {
// Start memory rather than a frozen declaration constant.
@Test public void testUnknownCallerCallback() {
CodeGen code = new CodeGen("""
- val _hidden_noInline = { int n -> return n+2; };
+ val _hidden_noInline = { int n -> int result=n+2; return result; };
val apply_noInline = { {int->int} f,int n -> return f(n); };
return apply_noInline;
""").parse();
@@ -42,16 +42,16 @@ public class EscapeFlowTest {
assertTrue(callback.in(1) instanceof ProjNode);
TypeFunPtr type = (TypeFunPtr)callback._type;
assertTrue(XInt.isHigh(type.fidxs()));
- assertSame(TypeInteger.BOT,apply.ret().expr()._type);
+ assertSame(TypeInteger.I32,apply.ret().expr()._type);
// Only <clinit> and apply escape; forwarding #self must not expose _hidden.
assertEquals("Stop[ return { apply_noInline}; return #2; ]",code.print());
}
@Test public void testNativeCallbackRoundTrip() {
CodeGen code = new CodeGen("""
- val _callback_noInline = { int n -> return n+1; };
+ val _callback_noInline = { int n -> int result=n+1; return result; };
{ {int->int} -> {int->int} } bounce = "C";
- return bounce(_callback_noInline)(arg);
+ return bounce(_callback_noInline)(((arg<<32)>>32));
""").driver(CodeGen.Phase.TypeCheck);
FunNode callback = named(code,"_callback_noInline");
assertTrue(callback.unknownCallers());
@@ -87,7 +87,7 @@ public class EscapeFlowTest {
@Test public void testPublicAllocator() {
CodeGen code = new CodeGen("""
struct Box { int n; new Box = { int x -> n=x; }; };
- return new Box(arg).n;
+ return new Box(((arg<<32)>>32)).n;
""").parse();
FunNode allocator = named(code,"Test.Box.<new>");
int[] helpers = XInt.EMPTY;
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/chapter30/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java
index 66fdacc3..bdb71fc6 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java
@@ -7,7 +7,7 @@ import static org.junit.Assert.*;
public class FinalConstructionTest {
private static final String LOOP = """
- val step={int x -> x*2+1;};
+ val step={i64 x -> x*2+1;};
struct C {
val x=1;
new C={int n ->
@@ -16,7 +16,7 @@ public class FinalConstructionTest {
};
val get={->x;};
};
- return new C(arg).get();
+ return new C(((arg<<32)>>32)).get();
""";
@Test public void testFinalConstructorFields() throws java.io.IOException {
@@ -27,10 +27,10 @@ public class FinalConstructionTest {
// Writes are ordinary Stores and may remain inside the loop.
String[] sources={
"struct C { val x=1; new C={->x=1; x=2;}; }; return new C().x;",
- "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i<n;i++) self.x*=2;}; }; return new C(arg).x;",
- "struct C { val x=1; new C={int n -> for(int i=0;i<n;i++) ++x;}; }; return new C(arg).x;",
+ "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i<n;i++) self.x*=2;}; }; return new C(((arg<<32)>>32)).x;",
+ "struct C { val x=1; new C={int n -> for(int i=0;i<n;i++) ++x;}; }; return new C(((arg<<32)>>32)).x;",
"struct C { val x=1.5; new C={->x=2.5;}; }; return new C().x;",
- "struct C { val x=\"a\"; new C={int n -> for(int i=0;i<n;i++) x=\"b\";}; }; return new C(arg).x[0];",
+ "struct C { val x=\"a\"; new C={int n -> for(int i=0;i<n;i++) x=\"b\";}; }; return new C(((arg<<32)>>32)).x[0];",
"struct C { val x=new int[1]; new C={->x=new int[2]; x=new int[3];}; }; return new C().x#;"
};
String[] results={"2","8","4","2.5","98","3"};
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java b/chapter30/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java
index d8f7ef34..155ebac3 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java
@@ -68,8 +68,8 @@ public class IncrementalBuildTest {
@Test public void testSourceChangeAndCycle() throws Exception {
Path root=fixture("bundle-");
Path src=root.resolve("src"), obj=root.resolve("obj");
- write(src,"App/A","val value={int n -> n ? B.value(n-1) : 1; };",1000);
- write(src,"App/B","val value={int n -> n ? A.value(n-1) : 5; };",1000);
+ write(src,"App/A","val value={int n -> n ? B.value(--n) : 1; };",1000);
+ write(src,"App/B","val value={int n -> n ? A.value(--n) : 5; };",1000);
write(src,"App/Main","return A.value(1);",1000);
compile(src,obj,"App/A");
assertEquals("One build timestamp for the cycle",Files.getLastModifiedTime(obj.resolve("App/A.o")),
@@ -82,7 +82,7 @@ public class IncrementalBuildTest {
assertNull(code._compunits.get("App/A")._src);
assertNull(code._compunits.get("App/B")._src);
- write(src,"App/B","val value={int n -> n ? A.value(n-1) : 7; };",4000);
+ write(src,"App/B","val value={int n -> n ? A.value(--n) : 7; };",4000);
code=client(src,obj);
assertEquals("7",Eval2.eval(code,0));
assertNotNull(code._compunits.get("App/A")._src);
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Int32Test.java b/chapter30/src/test/java/com/seaofnodes/simple/Int32Test.java
new file mode 100644
index 00000000..bea7c116
--- /dev/null
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Int32Test.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.node.cpus.arm.arm;
+import com.seaofnodes.simple.node.cpus.riscv.riscv;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeStruct;
+import java.io.IOException;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class Int32Test {
+ @Test public void testAliasAndLayout() {
+ new CodeGen("struct Pair { int x; int y; }; return new Pair;")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertSame(TypeInteger.I32,Parser.INIT_TYPES.get("int"));
+ var pair=(TypeStruct)Parser.TYPES.get("Test.Pair");
+ assertSame(TypeInteger.I32,pair.field("x")._t);
+ assertEquals(8,pair.size());
+ assertEquals(4,Math.abs(pair.offset(1)-pair.offset(0)));
+ }
+
+ @Test public void testNarrowAssignmentsAndUpdates() {
+ for(String source:new String[]{
+ "int x=arg; return x;",
+ "int x=0; x=arg; return x;",
+ "struct S { int x; }; val s=new S; s.x=arg; return s.x;",
+ "val a=new int[2]; a[1]=arg; return a[1];"
+ }) {
+ var code=new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ for(long x:new long[]{0,2147483647L,2147483648L,-2147483649L,Long.MAX_VALUE,Long.MIN_VALUE})
+ assertEquals(source,Integer.toString((int)x),Eval2.eval(code,x));
+ }
+ var increment=new CodeGen("int x=arg; x++; return x;").driver(CodeGen.Phase.TypeCheck);
+ assertEquals("-2147483648",Eval2.eval(increment,2147483647));
+ var compound=new CodeGen("int x=arg; x+=2; return x;").driver(CodeGen.Phase.TypeCheck);
+ assertEquals("-2147483647",Eval2.eval(compound,2147483647));
+ }
+
+ @Test public void testWideExpressionsAndInference() {
+ for(String source:new String[]{
+ "int x=2147483647; return x+1;",
+ "var x=2147483647; x++; return x;",
+ "val x=2147483648; return x;",
+ "i64 x=2147483648; return x;"
+ }) assertEquals(source,"2147483648",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.TypeCheck),0));
+ }
+
+ @Test public void testLoopBoundSurvivesWidening() {
+ for(String type:new String[]{"int","u32"}) {
+ String source="val f_noInline={"+type+" x -> x;}; "+type+" x=0; while(x<arg) x++; return f_noInline(x);";
+ for(long seed:new long[]{1,123,456}) {
+ var code=new CodeGen(source,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("7",Eval2.eval(code,7));
+ }
+ }
+ var high=new CodeGen("val f_noInline={u32 x -> x;}; u32 x=2147483646; while(x<arg) x++; return f_noInline(x);")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals("2147483650",Eval2.eval(high,2147483650L));
+ var wrap=new CodeGen("val f_noInline={u32 x -> x;}; u32 x=4294967294; while(x) x++; return f_noInline(x);")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(wrap,0));
+ }
+
+ @Test public void testNativeLayoutAndParameters() throws IOException {
+ TestC.runC("""
+ val int_echo={int x -> x;};
+ val int_wrap={int x -> x++; return x;};
+ val wide_echo={i64 x -> x;};
+ val narrow_sum={i8 a,u8 b,i16 c,u16 d,int e,u32 f,int g,u32 h -> a+b+c+d+e+f+g+h;};
+ struct Pair { int x; int y; };
+ val int_pair={ -> val p=new Pair; p.x=-1; p.y=2147483647; return p; };
+ val int_array={ -> val a=new int[3]; a[0]=-1; a[1]=2147483647; a[2]=-2147483648; return a; };
+ ""","int32","ok\n",-1);
+ }
+
+ @Test public void testNarrowParameterBackends() throws IOException {
+ long input=0xa5a5a5a580008080L;
+ String[] types={"i8","u8","i16","u16","int","u32"};
+ long[] expected={(byte)input,input&255,(short)input,input&65535,(int)input,input&0xffffffffL};
+ for(int i=0; i<types.length; i++) {
+ String source="val probe={"+types[i]+" x -> x;};";
+ var a=TestArm64.build("probe",source,0,-1,false);
+ a.regs[arm.X0]=input;
+ assertEquals(0,a.step(1000));
+ assertEquals(types[i],expected[i],a.regs[arm.X0]);
+ var r=TestRisc5.build(source,"probe",0,-1,false);
+ r.regs[riscv.A0]=input;
+ assertEquals(0,r.step(1000));
+ assertEquals(types[i],expected[i],r.regs[riscv.A0]);
+ }
+ }
+}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/MergeSortTest.java b/chapter30/src/test/java/com/seaofnodes/simple/MergeSortTest.java
index f5d3d146..b177de23 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/MergeSortTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/MergeSortTest.java
@@ -18,8 +18,9 @@ public class MergeSortTest {
"""
val merge_sort = { int[] a ->
int[] !b = new int[a#];
- _copy_array (a, 0, a#, b);
- _split_merge(a, 0, a#, b);
+ int len=a#;
+ _copy_array (a, 0, len, b);
+ _split_merge(a, 0, len, b);
};
val _split_merge = { int[] !b, int begin, int end, int[] a ->
@@ -57,16 +58,16 @@ val _copy_array = { int[] !a, int begin, int end, int[] b ->
for( int prime : primes )
sb.p(prime).p(", ");
String sprimes = sb.p("]").toString();
- TestC.runC(src, "sort", sprimes, 34);
+ TestC.runC(src, "sort", sprimes,70);
EvalRisc5 R5 = TestRisc5.build( src, "merge_sort", 0, 36, false);
// Allocate an array of primes, filled in reverse order
int ps = R5._heap;
- R5._heap += (primes.length+1)*8; // 25 primes, and length
+ R5._heap += (primes.length+1)*4; // 25 primes, and length
R5.st4(ps,primes.length);
for( int i=0; i<primes.length; i++ )
- R5.st8(ps+(primes.length-1-i+1)*8,primes[i]);
+ R5.st4(ps+(primes.length-1-i+1)*4,primes[i]);
// Pass array base when calling `merge_sort`
R5.regs[riscv.A0] = ps; // Array base to be sorted
@@ -75,7 +76,7 @@ val _copy_array = { int[] !a, int begin, int end, int[] b ->
assertEquals(primes.length,R5.ld4s(ps));
for( int i=0; i<primes.length; i++ )
- assertEquals(primes[i], R5.ld8(ps+(i+1)*8));
+ assertEquals(primes[i], R5.ld4s(ps+(i+1)*4));
// TODO: replace with ARM Eval
Chapter21Test.testCPU(src,"arm", "SystemV",36,null);
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/MinusNodeTest.java b/chapter30/src/test/java/com/seaofnodes/simple/MinusNodeTest.java
index f7fbde03..1b120e6e 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/MinusNodeTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/MinusNodeTest.java
@@ -57,7 +57,7 @@ public class MinusNodeTest {
}
@Test public void testRecursiveFloat() {
- CodeGen code = new CodeGen("val _neg = { int x -> x ? -_neg(x-1) : 1.0; }; return _neg(arg);")
+ CodeGen code = new CodeGen("val _neg = { int x -> x ? -_neg(--x) : 1.0; }; return _neg(((arg<<32)>>32));")
.driver(CodeGen.Phase.TypeCheck);
for( int i=0; i<4; i++ )
assertEquals((i&1)==0 ? "1.0" : "-1.0",Eval2.eval(code,i));
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java b/chapter30/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
new file mode 100644
index 00000000..2b7eea78
--- /dev/null
+++ b/chapter30/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
@@ -0,0 +1,191 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class ParserDiagnosticTest {
+ private static Parser.ParseException rejected(String source) {
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.Parse);
+ throw new AssertionError("Accepted invalid source");
+ } catch( Parser.ParseException e ) {
+ return e;
+ }
+ }
+
+ private static void diagnostic(String source, int line, int column, String message) {
+ var error=rejected(source);
+ assertEquals(message,error.getMessage());
+ assertEquals("Test.smp:"+line+":"+column+": error: "+message
+ +"\n "+source.split("\n",-1)[line-1]+"\n "+"_".repeat(column-1)+"^",error.diagnostic());
+ }
+
+ @Test public void testNumericLiteralErrors() {
+ 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'");
+ }
+
+ @Test public void testUnclosedCommentsAndStrings() {
+ diagnostic("/* never closed",1,1,"Unclosed block comment");
+ diagnostic("return 0; /*",1,11,"Unclosed block comment");
+ diagnostic("return \"abc\n",1,8,"Unclosed string");
+ diagnostic("return \"abc\\",1,8,"Unclosed string");
+ // A comment at byte zero used to index -1; a final // was not recognized.
+ new CodeGen("/* ok */ return 0; //").driver(CodeGen.Phase.Parse);
+ }
+
+ @Test public void testCharacterLiteralsAndEscapes() {
+ diagnostic("return '';",1,8,"Empty character literal");
+ diagnostic("return 'ab';",1,8,"Character literal must contain exactly one byte");
+ diagnostic("return 'a ';",1,8,"Character literal must contain exactly one byte");
+ diagnostic("return '",1,8,"Unclosed character literal");
+ diagnostic("return 'a\n",1,8,"Unclosed character literal");
+ diagnostic("return '\\q';",1,9,"Unknown character escape \\q");
+ diagnostic("return \"\\q\";",1,9,"Unknown string escape \\q");
+ new CodeGen("return ' ' + '\\'' + '\\\\';").driver(CodeGen.Phase.Parse);
+ }
+
+ @Test public void testDeferredLiteralErrors() {
+ diagnostic("struct S {\n new S={ ->\n val x='ab';\n };\n};",3,9,
+ "Character literal must contain exactly one byte");
+ diagnostic("struct S {\n new S={ ->\n val x=\"abc",3,9,"Unclosed string");
+ diagnostic("for(int i=0; i<1; i+='ab') {}",1,22,
+ "Character literal must contain exactly one byte");
+ diagnostic("struct S {\n new S={ ->\n val x=\"outer %(\"bad\\q\")\";\n };\n};",3,22,
+ "Unknown string escape \\q");
+ }
+
+ @Test public void testIdentifierAndKeywordLocations() {
+ diagnostic("int = 1;",1,5,"Expected an identifier, found '='");
+ diagnostic("int true=1;",1,5,"Expected an identifier, found 'true'");
+ diagnostic("int",1,4,"Expected an identifier, found 'EOF'");
+ diagnostic("return if;",1,8,"Syntax error, expected `an identifier or expression` but found `if`");
+ diagnostic("\n break;",2,3,"No active loop for a break or continue");
+ diagnostic("\n continue;",2,3,"No active loop for a break or continue");
+ }
+
+ @Test public void testFinalAssignmentLocations() {
+ diagnostic("val x=1;\n x=2;",2,3,"Cannot reassign final 'x'");
+ diagnostic("val x=1;\n x++;",2,3,"Cannot reassign final 'x'");
+ diagnostic("val x=1;\n ++x;",2,5,"Cannot reassign final 'x'");
+ }
+
+ @Test public void testParameterAndConstructorLocations() {
+ diagnostic("val f={int x, int x -> x;};",1,19,"Duplicate parameter 'x'");
+ diagnostic("val f={int ^x -> x;};",1,12,"'^' requires a struct parameter");
+ diagnostic("struct S { new Wrong={ -> }; };",1,16,"Constructor name 'Wrong' must match type name 'S'");
+ diagnostic("int? x;",1,4,"Type i32 cannot be null");
+ diagnostic("return new int;",1,12,"Cannot allocate a i32");
+ }
+
+ @Test public void testMemberLocation() {
+ diagnostic("struct S { int x; };\nval s=new S;\nreturn s.missing;",3,10,
+ "Accessing unknown field 'missing' from '*Test.S'");
+ diagnostic("struct S { int x; };\nval s=new S;\nreturn s.;",3,10,"Expected an identifier, found ';'");
+ }
+
+ @Test public void testInitializerAndOperatorLocations() {
+ diagnostic("val x = null;",1,9,"Cannot infer a type from null; declare a nullable type explicitly");
+ diagnostic("u8[] bytes=\"C\";",1,6,"C data binding requires an integer or float type");
+ diagnostic("return 1 < 2 > 3;",1,14,"Mixing relational directions in a chained relational test");
+ var shift=rejected("return 1 << 2.0;");
+ assertTrue(shift.getMessage(),shift.getMessage().startsWith("Cannot '<<'"));
+ assertEquals("Test.smp:1:10: error: "+shift.getMessage()
+ +"\n return 1 << 2.0;\n _________^",shift.diagnostic());
+ }
+
+ @Test public void testCLILexicalErrorsWithoutStackTrace() throws Exception {
+ checkCLI("return 9223372036854775808;",
+ ":1:8: error: Integer literal is outside the signed 64-bit range"
+ +"\n return 9223372036854775808;\n _______^\n");
+ checkCLI("/* missing close",
+ ":1:1: error: Unclosed block comment\n /* missing close\n ^\n");
+ }
+
+ @Test public void testMissingInitializer() {
+ var error=rejected("// declaration\n\nval _validMove() {\n");
+ assertEquals("Syntax error, expected `=expression` but found `(`",error.getMessage());
+ assertEquals("Test.smp:3:15: error: "+error.getMessage()
+ +"\n val _validMove() {\n ______________^",error.diagnostic());
+ }
+
+ @Test public void testFoundIdentifierPointsAtItsStart() {
+ var error=rejected("int x=1\nreturn x;");
+ assertEquals("Test.smp:2:1: error: Syntax error, expected `;` but found `return`"
+ +"\n return x;\n ^",error.diagnostic());
+ }
+
+ @Test public void testEOF() {
+ var error=rejected("return 1");
+ assertEquals("Test.smp:1:9: error: Syntax error, expected `;` but found `EOF`"
+ +"\n return 1\n ________^",error.diagnostic());
+ }
+
+ @Test public void testCRLFTabsAndUTF8() {
+ var error=rejected("// π\r\n\tint x=1\r\n\treturn x;");
+ assertEquals("Test.smp:3:9: error: Syntax error, expected `;` but found `return`"
+ +"\n return x;\n ________^",error.diagnostic());
+ var unicode=rejected("val x=\"π\" return 1;");
+ assertEquals("Test.smp:1:11: error: Syntax error, expected `;` but found `return`"
+ +"\n val x=\"π\" return 1;\n __________^",unicode.diagnostic());
+ }
+
+ @Test public void testDeferredForUpdateLocation() {
+ // The update is parsed after the body, so lexer line counters can be stale.
+ var error=rejected("for(int i=0; i<3; i+=) {\n int x=i;\n}\nreturn 0;");
+ assertEquals("Test.smp:1:22: error: Syntax error, expected `an identifier or expression` but found `)`"
+ +"\n for(int i=0; i<3; i+=) {\n _____________________^",error.diagnostic());
+ }
+
+ @Test public void testCLIReportsSourceWithoutStackTrace() throws Exception {
+ checkCLI("// declaration\nval _validMove() {\n",
+ ":2:15: error: Syntax error, expected `=expression` but found `(`"
+ +"\n val _validMove() {\n ______________^\n");
+ }
+
+ @Test public void testUndefinedNameKeepsItsUseSite() {
+ var error=rejected("val make={ -> val board=new u8[9]; board; };\n"
+ +"val read={ ->\n return board[0];\n};\nreturn read();");
+ assertEquals("Test.smp:3:12: error: Undefined name 'board'"
+ +"\n return board[0];\n ___________^",error.diagnostic());
+ }
+
+ @Test public void testUndefinedAssignmentPointsAtName() {
+ var error=rejected("\n board = 1;");
+ assertEquals("Test.smp:2:5: error: Undefined name 'board'"
+ +"\n board = 1;\n ____^",error.diagnostic());
+ }
+
+ @Test public void testCLIUndefinedNameWithoutStackTrace() throws Exception {
+ checkCLI("// unresolved\nreturn board[0];\n",
+ ":2:8: error: Undefined name 'board'"
+ +"\n return board[0];\n _______^\n");
+ }
+
+ private static void checkCLI(String text, String expected) throws Exception {
+ Path dir=Files.createDirectories(Path.of("build/diagnostics/source with spaces"));
+ Path source=dir.resolve("Bad.smp").toAbsolutePath();
+ Files.writeString(source,text);
+ String java=Path.of(System.getProperty("java.home"),"bin","java").toString();
+ Process child=new ProcessBuilder(java,"-ea","-cp",System.getProperty("java.class.path"),
+ Simple.class.getName(),"--norun","-o","build/diagnostics/objects/",source.toString()).start();
+ try {
+ assertTrue("Compiler did not exit",child.waitFor(20,TimeUnit.SECONDS));
+ assertEquals(1,child.exitValue());
+ assertEquals("",new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8));
+ String diagnostic=new String(child.getErrorStream().readAllBytes(),StandardCharsets.UTF_8)
+ .replace("\r\n","\n");
+ assertEquals(source+expected,diagnostic);
+ } finally {
+ child.destroyForcibly();
+ }
+ }
+}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/PartialReadTest.java b/chapter30/src/test/java/com/seaofnodes/simple/PartialReadTest.java
index a6230b69..427096e5 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/PartialReadTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/PartialReadTest.java
@@ -11,7 +11,7 @@ public class PartialReadTest {
private static String program(String body, String helper) {
return "struct _C { int~ x; int~ y; new _C = { int n -> " + body +
" }; }; val _read_noInline = { _C ^p -> " + helper +
- " }; return new _C(arg).y;";
+ " }; return new _C(((arg<<32)>>32)).y;";
}
private static void rejects(String source, String message) {
try {
@@ -108,7 +108,7 @@ public class PartialReadTest {
_Child child;
new _Parent = { int n -> x=n; child=new _Child(self); };
};
- val p=new _Parent(arg); return p.child.seen+p.child.parent.x;
+ val p=new _Parent(((arg<<32)>>32)); return p.child.seen+p.child.parent.x;
""";
assertEquals("22",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),11));
rejects(src.replace("x=n; child=new _Child(self);","child=new _Child(self); x=n;"),"might not be initialized");
@@ -127,7 +127,7 @@ public class PartialReadTest {
new _C = { int n -> data=new int[1]; data[0]=n; y=_read_noInline(self); };
};
val _read_noInline={ _C ^p -> return p.data[0]; };
- return new _C(arg).y;
+ return new _C(((arg<<32)>>32)).y;
""";
assertEquals("23",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),23));
// A set reference slot is not proof that its referent is initialized.
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/RuntimeArgsTest.java b/chapter30/src/test/java/com/seaofnodes/simple/RuntimeArgsTest.java
new file mode 100644
index 00000000..9086e00c
--- /dev/null
+++ b/chapter30/src/test/java/com/seaofnodes/simple/RuntimeArgsTest.java
@@ -0,0 +1,29 @@
+package com.seaofnodes.simple;
+
+import java.io.IOException;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class RuntimeArgsTest extends TestC {
+ @Test public void testNativeArguments() throws IOException {
+ runArgs("""
+ val main={ u8[~][] args ->
+ if(args#!=4 || args[0]#==0) return 1;
+ if(args[1]#!=0) return 2;
+ val words=args[2];
+ if(words#!=9 || words[3]!=' ' || words[8]!='s') return 3;
+ val longArg=args[3];
+ if(longArg#!=4096) return 4;
+ for(int i=0; i<longArg#; i++) if(longArg[i]!='x') return 5;
+ return 0;
+ };
+ ""","runtimeArgs",null,CALL_CONVENTION,"",-1,"","two words","x".repeat(4096));
+ }
+
+ @Test public void testArgumentCopiesAndEmptyVector() throws IOException {
+ String exe="build/objs/runtimeArgsCopy"+(OS.startsWith("Windows") ? ".exe" : "");
+ linkExe(runtimeObject("simple_args"),OS.startsWith("Windows") ? "ms_abi" : "sysv_abi",
+ C_DRIVERS_DIR+"runtime_args.c",null,exe);
+ assertEquals("ok\n",exec(TEST_TIMEOUT_SECONDS,exe));
+ }
+}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java b/chapter30/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java
index 88c11101..447025f6 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java
@@ -23,7 +23,7 @@ public class SelectiveLoadingTest {
new CodeGen(src.toString(),obj.toString(),null,"Lib",null,123,true,TypeInteger.BOT)
.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
CodeGen code = new CodeGen(null,null,new Ary<>(new String[]{obj.toString()}),
- "Use","return Lib.sum_noInline(arg);",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ "Use","return Lib.sum_noInline(((arg<<32)>>32));",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
assertNull(code._compunits.get("Lib/Left")._src);
assertNull(code._compunits.get("Lib/Right")._src);
assertEquals("2000000000000",Eval2.eval(code,0));
@@ -33,11 +33,11 @@ public class SelectiveLoadingTest {
@Test public void testUnreachableImportIsNotARoot() throws Exception {
Path root = library("dead-import-");
Ary<String> paths = new Ary<>(new String[]{root.toString()});
- object(root,paths,"Unused","val run_noInline = { int n -> n+1; };");
+ object(root,paths,"Unused","val run_noInline = { int n -> int result=n+1; return result; };");
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(null,root.toString(),paths,"Main","""
val _false_noInline = { -> false; };
- if( _false_noInline() ) return Unused.run_noInline(arg);
+ if( _false_noInline() ) return Unused.run_noInline(((arg<<32)>>32));
return 7;
""",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
CompUnit unused = code._compunits.get("Unused");
@@ -61,8 +61,8 @@ public class SelectiveLoadingTest {
object(root,paths,"L"+i,"val relay_noInline = { {int->int} f -> L"+(i-1)+".relay_noInline(f); };");
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(null,root.toString(),paths,"Main","""
- val _increment_noInline = { int n -> n+1; };
- val _decrement_noInline = { int n -> n-1; };
+ val _increment_noInline = { int n -> int result=n+1; return result; };
+ val _decrement_noInline = { int n -> int result=n-1; return result; };
val f = L5.relay_noInline(arg ? _increment_noInline : _decrement_noInline);
return f(41);
""",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
@@ -78,9 +78,9 @@ public class SelectiveLoadingTest {
object(root,paths,"Relay","val relay = { {int->int} f -> f; };");
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(null,root.toString(),paths,"Main","""
- val _increment_noInline = { int n -> n+1; };
+ val _increment_noInline = { int n -> int result=n+1; return result; };
val f = Relay.relay(_increment_noInline);
- return f(arg);
+ return f(((arg<<32)>>32));
""",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
assertNull(function(code,"relay").summary());
assertEquals("42",Eval2.eval(code,41));
@@ -91,11 +91,11 @@ public class SelectiveLoadingTest {
@Test public void testImportedFunctionField() throws Exception {
Path root = library("function-field-");
Ary<String> paths = new Ary<>(new String[]{root.toString()});
- object(root,paths,"Targets","var chosen = { int n -> n+1; };");
+ object(root,paths,"Targets","var chosen = { int n -> int result=n+1; return result; };");
object(root,paths,"Apply","val run = { int n -> val f = Targets.chosen; return f(n); };");
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(null,root.toString(),paths,"Main",
- "return Apply.run(arg);",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ "return Apply.run(((arg<<32)>>32));",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
assertNull(function(code,"run").summary());
assertEquals("42",Eval2.eval(code,41));
assertEquals("1",Eval2.eval(code,0));
@@ -112,15 +112,15 @@ public class SelectiveLoadingTest {
for( String target : new String[]{"apply","apply_noInline","forward_noInline"} )
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(null,root.toString(),paths,"Main",
- "val _increment_noInline = { int n -> n+1; }; "+
+ "val _increment_noInline = { int n -> int result=n+1; return result; }; "+
"val forward_noInline = { {int->int} f,int n -> Apply.apply_noInline(f,n); }; return "+
- (target.equals("forward_noInline") ? target : "Apply."+target)+"(_increment_noInline,arg);",
+ (target.equals("forward_noInline") ? target : "Apply."+target)+"(_increment_noInline,((arg<<32)>>32));",
seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
assertEquals("42",Eval2.eval(code,41));
assertEquals("1",Eval2.eval(code,0));
}
CodeGen nativeCode = new CodeGen(null,root.toString(),paths,"NativeCallback","""
- val _increment_noInline = { int n -> n+1; };
+ val _increment_noInline = { int n -> int result=n+1; return result; };
val run = { int n -> Apply.apply_noInline(_increment_noInline,n); };
""",123,true,TypeInteger.BOT).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
Path driver = root.resolve("driver.c");
@@ -212,7 +212,7 @@ public class SelectiveLoadingTest {
val p={int n -> Work.write_noInline(n);};
val p_noInline={int n -> Work.write_noInline(n);};
""");
- write(src,"Lib/Work","val write_noInline={int n -> n+1;};");
+ write(src,"Lib/Work","val write_noInline={ int n -> int result=n+1; return result;};");
write(src,"Lib/Unused","val f={int n -> n*2;};");
new CodeGen(src.toString(),lib.toString(),null,"Lib",null,123,true,TypeInteger.BOT)
.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
@@ -221,7 +221,7 @@ public class SelectiveLoadingTest {
Ary<String> paths = new Ary<>(new String[]{lib.toString()});
CodeGen code = new CodeGen(null,app.toString(),paths,"Main",
- "if(arg==0) return -1; return Lib.api.p_noInline(arg);",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ "if(arg==0) return -1; return Lib.api.p_noInline(((arg<<32)>>32));",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
assertNotNull(function(code,"Lib.Api.p_noInline").summary());
assertNotNull(function(code,"Lib.Work.write_noInline").summary());
assertNotNull(function(code,"Lib.Unused.f").summary());
@@ -233,7 +233,7 @@ public class SelectiveLoadingTest {
assertEquals("6",Eval2.eval(code,5));
code=new CodeGen(null,app.toString(),paths,"Main",
- "val run={int n -> Lib.api.p(n);}; return run(arg);",123,true,TypeInteger.BOT)
+ "val run={int n -> Lib.api.p(n);}; return run(((arg<<32)>>32));",123,true,TypeInteger.BOT)
.driver(CodeGen.Phase.TypeCheck);
assertNull("p requested Api's implementation",function(code,"Lib.Api.p").summary());
assertNotNull("The remaining call needs a signature, not a body",function(code,"Lib.Work.write_noInline").summary());
@@ -255,7 +255,7 @@ public class SelectiveLoadingTest {
// fresh program.
try {
Eval2.eval(new CodeGen(null,app.toString(),paths,"Missing",
- "if(arg==0) return -1; return Lib.api.p_noInline(arg);",123,true,TypeInteger.BOT),0);
+ "if(arg==0) return -1; return Lib.api.p_noInline(((arg<<32)>>32));",123,true,TypeInteger.BOT),0);
fail("Missing transitive code must fail before evaluation");
} catch( RuntimeException ex ) { assertTrue(ex.getMessage(),ex.getMessage().contains("Lib/Work")); }
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java b/chapter30/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
index cfc0b7e0..1853aa48 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
@@ -18,9 +18,9 @@ public class SeparateCompilationTest {
val run={int n ->
val power={int base,int exponent ->
if(exponent<=0) return 1;
- return base*power(base,exponent-1);
+ return base*power(base,--exponent);
};
- { {int int -> int} -> int } retain="C";
+ { {int int -> i64} -> int } retain="C";
retain(power);
return power(2,n);
};
@@ -28,14 +28,14 @@ public class SeparateCompilationTest {
new CodeGen(null,lib.toString(),null,"Callbacks",src,123,true,TypeInteger.BOT)
.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
CodeGen code=new CodeGen(null,null,new Ary<>(new String[]{lib.toString()}),
- "Client","return Callbacks.run(arg);",123,true,TypeInteger.BOT)
+ "Client","return Callbacks.run(((arg<<32)>>32));",123,true,TypeInteger.BOT)
.driver(CodeGen.Phase.Opto);
// This assertion inspects a body, not just its saved summary.
ParseAll.loadCompUnit(code,code._compunits.get("Callbacks"));
assertNull(code._compunits.get("Callbacks")._src);
ParmNode base=code._start.walk(n -> n instanceof ParmNode p && p._label.equals("base") ? p : null);
assertNotNull(base);
- assertSame("Native code may call the saved callback with any base",TypeInteger.BOT,base._type);
+ assertSame("Native code may call the saved callback with any base",TypeInteger.I32,base._type);
code.driver(CodeGen.Phase.TypeCheck);
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/Simple.java b/chapter30/src/test/java/com/seaofnodes/simple/Simple.java
index 1f1e8ab0..78b1e76d 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/Simple.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/Simple.java
@@ -3,6 +3,7 @@ package com.seaofnodes.simple;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.CompUnit;
import com.seaofnodes.simple.codegen.CodeGen.Phase;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.print.*;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.util.Ary;
@@ -34,6 +35,8 @@ Options:
-L path - add an external Simple object search path
--eval - (slowly) evaluate the compiled code in emulator
--run - run the compiled code natively; this is the default
+ --link - build a native executable without running it
+ --norun - emit objects without linking or running
--main - export the top-level initializer as C `main`
--cpu <cpu-name> - use specific CPU (x86_64_v2, riscv, arm)
--abi <abi-name> - use specific ABI variant (SystemV)
@@ -90,10 +93,20 @@ Options:
}
public static void main(String[] args) throws Exception {
+ try {
+ run(args);
+ } catch( Parser.ParseException e ) {
+ System.err.println(e.diagnostic());
+ System.exit(1);
+ }
+ }
+
+ private static void run(String[] args) throws Exception {
String root = null;
String input_filename = null;
boolean do_eval = false;
boolean do_run = true;
+ boolean do_link = false;
boolean do_codegen = false;
boolean emit_main = false;
boolean print_time = false;
@@ -134,9 +147,10 @@ Options:
case "--dump": dump |= 1<<Phase.LastPhase .ordinal() ; break;
case "--dump-after-all": dump |= (2<<Phase.LastPhase .ordinal())-1; break;
case "-S": print_asm = true; break;
- case "--eval": do_eval = true ; do_run = false; break;
+ case "--eval": do_eval = true ; do_run = false; do_link = false; break;
case "--run": do_run = true ; break;
- case "--norun": do_run = false; break;
+ case "--link": do_link = true ; do_run = false; break;
+ case "--norun": do_run = false; do_link = false; break;
case "--main": emit_main = true; break;
case "--dump-size": print_size = true; break;
case "--dump-time": print_time = true; break;
@@ -215,7 +229,7 @@ Options:
}
String outBase = outPath.resolve(srcName).toString();
- if( do_run || print_asm || print_size ) {
+ if( do_run || do_link || print_asm || print_size ) {
if (cpu == null) cpu = TestC.CPU_PORT;
if (abi == null) abi = TestC.CALL_CONVENTION;
do_codegen = true;
@@ -234,7 +248,7 @@ Options:
Ary<String> externPaths = libPaths.isEmpty() ? null : new Ary<>(libPaths.toArray(new String[0]));
CodeGen code = new CodeGen(modPath.toString(), outPath.toString(), externPaths, srcName, src, 456, true, TypeInteger.BOT );
code.driver(do_eval && !do_codegen ? Phase.TypeCheck : Phase.LastPhase,
- cpu,abi, false, emit_main || do_run || do_eval, dump);
+ cpu,abi, false, emit_main || do_run || do_link || do_eval, dump);
if( do_codegen && print_asm )
System.out.println(code.asm());
@@ -253,12 +267,12 @@ Options:
if( print_time )
System.out.printf( "EXECUTION TIME: %.3f sec%n", (System.currentTimeMillis() - t) / 1e3);
}
- if( do_run ) {
+ if( do_run || do_link ) {
if ( !TestC.CPU_PORT.equals( cpu ) || !TestC.CALL_CONVENTION.equals( abi ) )
- throw bad("cannot run code on not native target");
+ throw bad("cannot link or run code on a non-native target");
String exe = TestC.OS.startsWith("Windows") ? outBase+".exe" : outBase;
- String result = TestC.gcc(code, null, null, "", exe);
- System.out.print(result);
+ TestC.linkExe(TestC.linkObjs(code),null,null,exe);
+ if( do_run ) TestC.execInteractive(exe);
}
}
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/StreamIOTest.java b/chapter30/src/test/java/com/seaofnodes/simple/StreamIOTest.java
new file mode 100644
index 00000000..f3491314
--- /dev/null
+++ b/chapter30/src/test/java/com/seaofnodes/simple/StreamIOTest.java
@@ -0,0 +1,138 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class StreamIOTest extends TestC {
+ @Test public void testReadUntilEOF() throws Exception {
+ String src="""
+ val main={ ->
+ val bytes=sys.io.stdin();
+ if(!bytes) return 1;
+ return sys.io.write(1,bytes)==bytes# ? 0 : 2;
+ };
+ """;
+ String exe=compile(src,"streamEcho",new Ary<>(new String[]{RELEASE_SYS_DIR}),
+ CALL_CONVENTION,null,null,-1);
+ for(int size:new int[]{0,8192,8193,65537}) {
+ String input="a\0bc\n".repeat(size/5)+"z".repeat(size%5);
+ Path in=Files.createTempFile(Path.of("build/objs"),"stream-in-",".txt");
+ Path out=Files.createTempFile(Path.of("build/objs"),"stream-out-",".txt");
+ Process child=null;
+ try {
+ Files.writeString(in,input);
+ ProcessBuilder builder=new ProcessBuilder(exe).redirectOutput(out.toFile());
+ if(size!=8193) builder.redirectInput(in.toFile());
+ child=builder.start();
+ if(size==8193) { // Also exercise a real pipe, ending only when its writer closes.
+ child.getOutputStream().write(input.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ child.getOutputStream().close();
+ }
+ assertTrue(child.waitFor(TEST_TIMEOUT_SECONDS,TimeUnit.SECONDS));
+ assertEquals(0,child.exitValue());
+ assertEquals(input,Files.readString(out));
+ } finally {
+ if(child!=null) child.destroyForcibly();
+ Files.deleteIfExists(in);
+ Files.deleteIfExists(out);
+ }
+ }
+ }
+
+ @Test public void testUnsignedArrayGrowth() throws Exception {
+ runSYS("""
+ val main={ ->
+ val b=new sys.aryu8();
+ val first=b.buf;
+ b.add('a');
+ if(b.buf!=first || b.len!=1) return 1;
+ b.addAry("bcdefghij");
+ if(b.len!=10 || b.buf[0]!='a' || b.buf[9]!='j') return 2;
+ b.undo(4294967295);
+ if(b.len!=0) return 3;
+ b.len=2147483648;
+ b.undo(1);
+ if(b.len!=2147483647) return 4;
+ b.clear();
+ val nums=new sys.aryi64(new i64[0]);
+ nums.add(10).add(20);
+ if(nums.len!=2 || nums.get(0)!=10 || nums.get(1)!=20) return 5;
+ nums.len=4294967295;
+ if(nums.len!=4294967295) return 6;
+ nums.len=2;
+ if(sys.ary.capacity(0,1)!=1) return 7;
+ if(sys.ary.capacity(8,8)!=8 || sys.ary.capacity(8,9)!=16) return 8;
+ if(sys.ary.capacity(2147483648,2147483649)!=4294967295) return 9;
+ if(sys.ary.capacity(4294967295,4294967296)!=0) return 10;
+ if(sys.ary.cmp("abc",4294967295,"",0,4294967295)!=0) return 11;
+ nums.str().add('\\n').write(1);
+ return 0;
+ };
+ ""","unsignedArrayGrowth","[10, 20]\n",-1);
+ }
+
+ @Test public void testShortInterruptedAndFailedIO() throws Exception {
+ String src="""
+ {i32 i64 -> i32} test_io_mode="C";
+ {i64} test_io_count="C";
+ {i32} test_io_status="C";
+ val main={ ->
+ test_io_mode(1,0);
+ val bytes=sys.io.read(700);
+ if(!bytes) return 1;
+ if(bytes#!=24593) return 2;
+ for(u32 i=0; i<bytes#; i++) if(bytes[i]!=i-(i/251)*251) return 3;
+ test_io_mode(1,0);
+ if(sys.io.write(701,bytes)!=bytes# || test_io_count()!=bytes#) return 4;
+ if(test_io_status()) return 5;
+ test_io_mode(1,3);
+ if(sys.io.writeOff(701,bytes,3,5)!=5 || test_io_count()!=5) return 6;
+ if(test_io_status()) return 7;
+ test_io_mode(1,24590);
+ if(sys.io.writeOff(701,bytes,24590,4294967295)!=3) return 8;
+ if(test_io_status()) return 9;
+ test_io_mode(1,0);
+ if(sys.io.writeOff(701,bytes,24593,0)!=0 || test_io_count()!=0) return 10;
+ if(sys.io.writeOff(701,bytes,4294967295,1)!=-1) return 11;
+ if(sys.libc.errno()!=sys.libc.EINVAL || test_io_count()!=0) return 12;
+ test_io_mode(2,0);
+ if(sys.io.read(700)!=null || sys.libc.errno()!=sys.libc.EIO) return 13;
+ test_io_mode(3,0);
+ if(sys.io.write(701,bytes)!=-1 || sys.libc.errno()!=sys.libc.EIO) return 14;
+ if(test_io_count()!=7) return 15;
+ test_io_mode(4,0);
+ if(sys.io.write(701,bytes)!=-1 || sys.libc.errno()!=sys.libc.EIO) return 16;
+ test_io_mode(5,0);
+ if(sys.io.write(701,bytes)!=-1 || test_io_status()) return 17;
+ if(sys.io.read(-1)!=null) return 18;
+ val b=new sys.aryu8().addAry(bytes);
+ test_io_mode(1,0);
+ if(b.write(701)!=b.len || test_io_count()!=b.len || test_io_status()) return 19;
+ sys.io.p("ok\\n");
+ return 0;
+ };
+ """;
+ CodeGen code=new CheckedCodeGen(null,"build/objs",new Ary<>(new String[]{RELEASE_SYS_DIR}),
+ "streamFaults",src,126,true,TypeInteger.BOT);
+ code.driver(CPU_PORT,CALL_CONVENTION,false,true);
+ String exe="build/objs/streamFaults"+(OS.startsWith("Windows") ? ".exe" : "");
+ Ary<String> gcc=new Ary<>(String.class);
+ gcc.add("gcc");
+ gcc.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie");
+ gcc.add("-Wl,--wrap=read,--wrap=write");
+ gcc.add(C_DRIVERS_DIR+"stream_io.c");
+ gcc.addAll(linkObjs(code));
+ gcc.add(runtimeObject("simple_crt"));
+ gcc.add(runtimeObject("simple_args"));
+ gcc.addAll(new String[]{"-lm","-o",exe});
+ exec(TEST_TIMEOUT_SECONDS,gcc.asAry());
+ assertEquals("ok\n",exec(TEST_TIMEOUT_SECONDS,exe));
+ }
+}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/StringFormatTest.java b/chapter30/src/test/java/com/seaofnodes/simple/StringFormatTest.java
new file mode 100644
index 00000000..53a10092
--- /dev/null
+++ b/chapter30/src/test/java/com/seaofnodes/simple/StringFormatTest.java
@@ -0,0 +1,158 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.node.CallNode;
+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 StringFormatTest extends TestC {
+ private static void prints(String body, String expected) throws Exception {
+ runSYS("val main={ -> "+body+" return 0; };", "stringFormat",expected,-1);
+ }
+
+ @Test public void testDefaultsAndNestedStrings() throws Exception {
+ prints("sys.io.p(\"CPU plays %(2+3). %(\"nested %(7)\") %(true) %(1.5)\\n\");",
+ "CPU plays 5. nested 7 1 1.5\n");
+ }
+
+ @Test public void testFormats() throws Exception {
+ prints("sys.io.p(\"%+06d(17)|%#08x(42)|%-5.3s(\"abcdef\")|%.2f(1.25)|%c(65)|%b(false)\\n\");",
+ "+00017|0x00002a|abc |1.25|A|false\n");
+ }
+
+ @Test public void testIntegerLimits() throws Exception {
+ prints("i64 lo=-9223372036854775807-1; sys.io.p(\"%(lo)|%(9223372036854775807)|%u(-1)|%X(-1)\");",
+ "-9223372036854775808|9223372036854775807|18446744073709551615|FFFFFFFFFFFFFFFF");
+ }
+
+ @Test public void testSignedAndUnsignedRadices() throws Exception {
+ long[] values={Long.MIN_VALUE,Long.MIN_VALUE+1,Long.MAX_VALUE,-1,-17,0,1,7,8,9,15,16,17,1L<<32};
+ StringBuilder source=new StringBuilder("val show_noInline={i64 x -> sys.io.p(\"%(x)|%u(x)|%o(x)|%x(x)\\n\"); }; val main={ -> ");
+ StringBuilder expected=new StringBuilder();
+ for(long x:values) {
+ source.append("show_noInline(").append(x==Long.MIN_VALUE ? "(-9223372036854775807-1)" : Long.toString(x)).append(");");
+ expected.append(x).append('|').append(Long.toUnsignedString(x)).append('|')
+ .append(Long.toUnsignedString(x,8)).append('|').append(Long.toUnsignedString(x,16)).append('\n');
+ }
+ source.append("return 0; };");
+ runSYS(source.toString(),"stringFormatRadices",expected.toString(),-1);
+ }
+
+ @Test public void testIntegerPaddingRules() throws Exception {
+ prints("sys.io.p(\"%#.0o(0)|%#.3o(1)|%#x(0)|%-08d(17)|%08.4d(-17)|%+d(0)|% d(0)\");",
+ "0|001|0|17 | -0017|+0| 0");
+ }
+
+ @Test public void testBuilderNumberUsesFormatter() throws Exception {
+ prints("val b=new sys.aryu8(); b.num(-9223372036854775807-1).add('|').num(9223372036854775807); sys.io.p(sys.text.end(b));",
+ "-9223372036854775808|9223372036854775807");
+ }
+
+ @Test public void testFormattingInEvaluatorFromSource() {
+ // No installed sys objects and no native execution: the library builds
+ // from source, and all non-float formatting executes as ordinary IR.
+ String source="""
+ val s="%#08x(arg)|%b(true)|%-4s("ok")|%c(65)";
+ val expected="0x00002a|true|ok |A";
+ if(s#!=expected#) return 1;
+ for(int i=0; i<s#; i++) if(s[i]!=expected[i]) return 2;
+ return 0;
+ """;
+ CodeGen code=new CodeGen("src/main/smp",null,null,"FreshText",source,123,true,TypeInteger.BOT)
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,42,10000));
+ }
+
+ @Test public void testEvaluationOrderAndLength() throws Exception {
+ prints("int x=0; val s=\"%(x++)%(x++)%(x++)\"; sys.io.p(\"%(s):%(s#):%(x)|%%|\\%\");",
+ "012:3:3|%|%");
+ prints("val a=new u8[1]; a[0]='A'; sys.io.p(\"%(a)%(a[0]='B')%(a)\");", "A66B");
+ }
+
+ @Test public void testConverters() throws Exception {
+ prints("sys.io.p(sys.text.int(17)); sys.io.p(sys.text.char(65)); sys.io.p(sys.text.bool(true)); sys.io.p(sys.text.flt(1.5));",
+ "17Atrue1.5");
+ prints("struct S { int int=7; }; sys.io.p(\"%((new S).int)\");", "7");
+ }
+
+ @Test public void testForwardReturnTypes() throws Exception {
+ runSYS("val main={ -> sys.io.p(\"%(later())\"); return 0; }; val later={ -> \"ok\"; };",
+ "stringFormatForward","ok",-1);
+ }
+
+ @Test public void testFloatAndAlternateFormats() throws Exception {
+ prints("f32 x=1.25f; sys.io.p(\"%(x)|%f(x)|%.2e(x)|%.2G(x)|%#o(9)|% d(7)|%.0d(0)|%a(1.5)\");",
+ "1.25|1.250000|1.25e+00|1.2|011| 7||0x1.8p+0");
+ runSYS("val format_noInline={ f32 x -> \"%(x)|%f(x)\"; }; val main={ -> sys.io.p(format_noInline(1.25f)); return 0; };",
+ "stringFormatFloat","1.25|1.250000",-1);
+ }
+
+ @Test public void testBufferGrowth() throws Exception {
+ prints("sys.io.p(\"%0100d(17)|%.100f(1.0)\");", "0".repeat(98)+"17|1."+"0".repeat(100));
+ }
+
+ @Test public void testLiteralRemainsConstant() {
+ CodeGen code=new CodeGen("return \"100%% \\%\";").driver(CodeGen.Phase.TypeCheck);
+ assertNull(code._stop.walk(n -> n instanceof CallNode ? n : null));
+ }
+
+ @Test public void testBytesAndEmptyStrings() throws Exception {
+ prints("val empty=\"%(\"\")\"; val bytes=\"a\\0b\"; sys.io.p(\"%(empty#):%s(bytes):%.2s(bytes):%c(0)\");",
+ "0:a\0b:a\0:\0");
+ prints("val s=\"π\"; sys.io.p(\"%(s):%(s#):%(s[0]):π\");", "π:2:207:π");
+ }
+
+ @Test public void testDeferredParsing() throws Exception {
+ prints("""
+ struct S { u8[~] s; new S={ -> s="%("}) %(3)")"; }; };
+ val s=new S;
+ sys.io.p(s.s);
+ for(int i=0; i<2; sys.io.p("%(i++) %("(")")) { }
+ """, "}) 30 (1 (");
+ }
+
+ @Test public void testImportedFormatter() throws Exception {
+ Path lib=Files.createTempDirectory(Path.of("build/objs"),"text-library-");
+ new CodeGen(null,lib.toString(),new Ary<>(new String[]{RELEASE_SYS_DIR}),"TextLibrary",
+ "val format_noInline={int x -> \"value=%04d(x)\"; };",123,true,TypeInteger.BOT)
+ .driver(CPU_PORT,CALL_CONVENTION,false,false);
+ run("val main={ -> sys.io.p(TextLibrary.format_noInline(7)); return 0; };",
+ "stringFormatImported",new Ary<>(new String[]{lib.toString(),RELEASE_SYS_DIR}),
+ CALL_CONVENTION,null,null,"value=0007",-1);
+ }
+
+ @Test public void testWrongTypesAndReservedNames() {
+ rejects("return \"%d(1.5)\";", "must be a");
+ rejects("return \"%s(17)\";", "must be a");
+ rejects("return \"%(null)\";", "Cannot format");
+ rejects("val f={int int -> int;}; return 0;", "Expected an identifier");
+ rejects("val f={ -> int int=1; return int; }; return 0;", "Expected an identifier");
+ }
+
+ private static void rejects(String source, String expected) {
+ try {
+ new CodeGen(null,null,new Ary<>(new String[]{RELEASE_SYS_DIR}),"Test",source,123,true,TypeInteger.BOT)
+ .driver(CodeGen.Phase.TypeCheck);
+ fail("Accepted invalid source: "+source);
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains(expected));
+ }
+ }
+
+ @Test public void testMalformedFormats() {
+ for(String format:new String[]{"%name", "%1$d(1)", "%*d(1)", "%n(1)", "%lld(1)", "%"})
+ rejects("return \""+format+"\";", "Expected a string format");
+ rejects("return \"%X.cliff\";", "Expected '('");
+ rejects("return \"%+s(\"x\")\";", "Invalid flags");
+ rejects("return \"%.2c(65)\";", "Precision is not supported");
+ }
+
+ @Test public void testUnsupportedDefault() {
+ rejects("struct S { int x; }; return \"%(new S)\";", "Cannot format");
+ }
+}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/TerminalInputTest.java b/chapter30/src/test/java/com/seaofnodes/simple/TerminalInputTest.java
new file mode 100644
index 00000000..bd3831b4
--- /dev/null
+++ b/chapter30/src/test/java/com/seaofnodes/simple/TerminalInputTest.java
@@ -0,0 +1,184 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.util.Ary;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class TerminalInputTest {
+ @Test public void testLinkOnlyAndEnterEscape() throws Exception {
+ String java=Path.of(System.getProperty("java.home"),"bin","java").toString();
+ String output="build/objs/linkOnlyGame/";
+ // Linking must return without displaying the game or waiting for input.
+ assertEquals("",TestC.exec(20,java,"-ea","-cp",System.getProperty("java.class.path"),
+ Simple.class.getName(),"--link","-L",TestC.RELEASE_SYS_DIR,"-o",output,
+ "docs/examples/TicTacToe.smp"));
+ Path exe=Path.of(output,"TicTacToe"+(TestC.OS.startsWith("Windows") ? ".exe" : ""));
+ assertTrue(Files.isRegularFile(exe));
+ Process child=new ProcessBuilder(exe.toString()).redirectErrorStream(true).start();
+ try {
+ child.getOutputStream().write(new byte[]{'\r','\n',27});
+ child.getOutputStream().flush(); // Keep stdin open: Escape must cause the exit.
+ assertTrue("Escape did not quit",child.waitFor(10,TimeUnit.SECONDS));
+ String text=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(text,0,child.exitValue());
+ assertTrue(text,text.endsWith("Your move (1-9): \nGoodbye!\n"));
+ assertFalse(text,text.contains("Choose a square"));
+ } finally {
+ child.destroyForcibly();
+ }
+ }
+
+ @Test public void testRedirectedBytesAndEOF() throws IOException {
+ TestC.run("""
+ val main={ u8[~][] args ->
+ if(sys.io.getch()!='5') return 1;
+ if(sys.io.getch()!='q') return 2;
+ if(sys.io.getch()!=-1) return 3;
+ if(sys.libc.errno()!=sys.libc.EOF) return 4;
+ sys.io.p("ok\n"); return 0;
+ };
+ ""","getchBytes",new Ary<>(new String[]{TestC.RELEASE_SYS_DIR}),
+ TestC.CALL_CONVENTION,null,null,"5q","ok\n",-1);
+ }
+
+ @Test public void testReadError() throws IOException {
+ TestC.runSYS("""
+ val main={ u8[~][] args ->
+ sys.io.close(sys.libc.STDIN);
+ if(sys.io.getch()!=-1) return 1;
+ if(sys.libc.errno()<=0) return 2;
+ return 0;
+ };
+ ""","getchError","",-1);
+ }
+
+ @Test public void testGuardsStayInsideFunction() {
+ String src="""
+ val first={ int n -> while(true) { if(n) return 0; return 1; } };
+ val second={ -> int sum=0; for(int i=0; i<3; i++) sum+=i; return sum; };
+ return first(((arg<<32)>>32))+second();
+ """;
+ for(long seed:new long[]{1,123,456}) {
+ CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("4",Eval2.eval(code,0));
+ assertEquals("3",Eval2.eval(code,1));
+ }
+ }
+
+ @Test public void testForwardReferenceAfterContinue() {
+ String src="""
+ val run={ int n ->
+ while(n) {
+ if(n==1) { n=0; continue; }
+ if(later(n)) return 7;
+ n=0;
+ }
+ return 3;
+ };
+ val later={ int n -> n==2; };
+ return run(((arg<<32)>>32));
+ """;
+ for(long seed:new long[]{1,123,456}) {
+ CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("3",Eval2.eval(code,1));
+ assertEquals("7",Eval2.eval(code,2));
+ assertEquals("3",Eval2.eval(code,3));
+ // The same merge is used by break, including the implicit loop exit.
+ String breaking=src.replace("return 7;","break;").replace("return 3;","return n+3;");
+ CodeGen code2=new CodeGen(breaking,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("3",Eval2.eval(code2,1));
+ assertEquals("5",Eval2.eval(code2,2));
+ assertEquals("3",Eval2.eval(code2,3));
+ }
+ }
+
+ @Test public void testForwardReferenceWithLoopLocal() {
+ String src="""
+ val run={ int n ->
+ while(n) {
+ int key=n;
+ n--;
+ if(key==1) continue;
+ if(later(key)) continue;
+ return key;
+ }
+ return 0;
+ };
+ val later={ int key -> key==2; };
+ return run(((arg<<32)>>32));
+ """;
+ String nested=src.replace("if(later(key)) continue;",
+ "if(later(key)) { int local=key+1; continue; }");
+ String breaking=nested.replace("int local=key+1; continue;","int local=key+1; break;")
+ .replace("return 0;","return n+10;");
+ String twoRefs=src.replace("return key;","if(other(key)) continue; return key;")
+ .replace("return run(((arg<<32)>>32));","val other={ int key -> key==3; }; return run(((arg<<32)>>32));");
+ for(long seed:new long[]{1,123,456}) {
+ for(String variant:new String[]{src,nested}) {
+ CodeGen code=new CodeGen(variant,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,1));
+ assertEquals("0",Eval2.eval(code,2));
+ assertEquals("3",Eval2.eval(code,3));
+ }
+ CodeGen code=new CodeGen(breaking,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("10",Eval2.eval(code,1));
+ assertEquals("11",Eval2.eval(code,2));
+ assertEquals("3",Eval2.eval(code,3));
+ CodeGen code2=new CodeGen(twoRefs,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code2,3));
+ assertEquals("4",Eval2.eval(code2,4));
+ }
+ }
+
+ @Test public void testInteractiveCLIShowsPromptBeforeInput() throws Exception {
+ // Pipe input rather than a terminal: this catches CLI buffering and
+ // requires exactly one byte, with no Enter.
+ String java=Path.of(System.getProperty("java.home"),"bin","java").toString();
+ Process child=new ProcessBuilder(java,"-ea","-cp",System.getProperty("java.class.path"),
+ Simple.class.getName(),"-L",TestC.RELEASE_SYS_DIR,"-o","build/objs/interactiveGame/",
+ "docs/examples/TicTacToe.smp").redirectErrorStream(true).start();
+ try(var reader=Executors.newSingleThreadExecutor()) {
+ try {
+ var prompt=reader.submit(() -> readPrompt(child));
+ String opening=prompt.get(20,TimeUnit.SECONDS);
+ assertTrue(opening,opening.contains("You go first."));
+ assertTrue(opening,opening.contains(" | | \n"));
+ assertTrue(child.isAlive());
+ child.getOutputStream().write('5');
+ child.getOutputStream().flush();
+ String turn=reader.submit(() -> readPrompt(child)).get(20,TimeUnit.SECONDS);
+ assertEquals("5\n | | \n---+---+---\n | X | \n---+---+---\n | | \n"
+ +"CPU plays 1.\n O | | \n---+---+---\n | X | \n---+---+---\n | | \n"
+ +"Your move (1-9): ",turn);
+ assertTrue(child.isAlive());
+ child.getOutputStream().write(27);
+ child.getOutputStream().flush();
+ assertTrue("Escape did not quit",child.waitFor(10,TimeUnit.SECONDS));
+ assertEquals("\nGoodbye!\n",new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8));
+ assertEquals(0,child.exitValue());
+ } finally {
+ child.destroyForcibly();
+ }
+ }
+ }
+
+ private static String readPrompt(Process child) throws IOException {
+ var bytes=new ByteArrayOutputStream();
+ while(bytes.size()<8192) {
+ int ch=child.getInputStream().read();
+ if(ch<0) throw new IOException("Exited before prompt: "+bytes.toString(StandardCharsets.UTF_8));
+ bytes.write(ch);
+ String text=bytes.toString(StandardCharsets.UTF_8);
+ if(text.endsWith("Your move (1-9): ")) return text;
+ }
+ throw new IOException("No prompt: "+bytes.toString(StandardCharsets.UTF_8));
+ }
+}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/TestC.java b/chapter30/src/test/java/com/seaofnodes/simple/TestC.java
index 54adf3af..dd7999c9 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/TestC.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/TestC.java
@@ -10,10 +10,14 @@ import com.seaofnodes.simple.util.Utils;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ExecutionException;
import static org.junit.Assert.*;
// Call a C driver program, which calls a Simple-generated .o, and self-checks.
@@ -32,10 +36,10 @@ public abstract class TestC {
public static final String CPU_ABI = CPU_PORT+"_"+CALL_CONVENTION;
public static final String RELEASE_SYS_DIR = "build/release/"+CPU_ABI;
- public static String runtimeObject() {
+ public static String runtimeObject(String name) {
String home = System.getenv("SIMPLE_HOME");
String root = home==null || home.isEmpty() ? "build/release" : home;
- return Path.of(root,CPU_ABI,"simple_crt.o").toString();
+ return Path.of(root,CPU_ABI,name+".o").toString();
}
public static final String C_DRIVERS_DIR = "src/test/java/com/seaofnodes/simple/progs/";
@@ -176,7 +180,7 @@ public abstract class TestC {
linkExe(objects,c_conv,cfile,exe);
}
- private static void linkExe(Ary<String> objects, String c_conv, String cfile, String exe) throws IOException {
+ static void linkExe(Ary<String> objects, String c_conv, String cfile, String exe) throws IOException {
// Compile the C program. Compiling code and constants in the low
// 2Gig. Pointers are 64b BUT since always in the low 2G all the
// high bits are zero - and Simple code can be emitted treating
@@ -187,9 +191,12 @@ public abstract class TestC {
if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main`
params.addAll(objects);
// Standalone Simple programs export `simple_main`; the small C
- // runtime owns the native main symbol and converts argc/argv into a
- // normal Simple array before making the ABI-qualified call.
- if( cfile==null ) params.add(runtimeObject());
+ // runtime owns native main and calls the Simple argument converter
+ // before making the ABI-qualified entry call.
+ if( cfile==null ) {
+ params.add(runtimeObject("simple_crt"));
+ params.add(runtimeObject("simple_args"));
+ }
params.addAll(new String[] {
"-lm", // Picks up 'sqrt' for newtonFloat tests to compare
"-g", // debug symbols
@@ -215,6 +222,40 @@ public abstract class TestC {
return execStdin(null,seconds,args);
}
+ // The CLI must display prompts while the child is waiting for input.
+ // Keep the capture-to-files path below for bounded, noninteractive tests.
+ public static void execInteractive(String... args) throws IOException {
+ Process child = new ProcessBuilder(args).redirectInput(ProcessBuilder.Redirect.INHERIT).start();
+ try( var relays = Executors.newFixedThreadPool(2) ) {
+ var out = relays.submit(() -> { relay(child.getInputStream(),System.out); return null; });
+ var err = relays.submit(() -> { relay(child.getErrorStream(),System.err); return null; });
+ try {
+ int status = child.waitFor();
+ out.get();
+ err.get();
+ if( status!=0 ) throw new IOException(String.join(" ",args)+"\nexec exit code: "+status);
+ } catch( InterruptedException e ) {
+ child.destroyForcibly();
+ Thread.currentThread().interrupt();
+ throw new IOException("interrupted",e);
+ } catch( ExecutionException e ) {
+ throw new IOException("Cannot relay program output",e.getCause());
+ } finally {
+ if( child.isAlive() ) child.destroyForcibly();
+ }
+ }
+ }
+
+ private static void relay(InputStream in, PrintStream out) throws IOException {
+ try( in ) {
+ byte[] buffer = new byte[4096];
+ for( int n; (n=in.read(buffer))!=-1; ) {
+ out.write(buffer,0,n);
+ out.flush();
+ }
+ }
+ }
+
private static String execStdin( String stdin, int seconds, String... args ) throws IOException {
ExecResult result = execute(stdin,seconds,args);
if( result.exit!=0 )
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/TicTacToeTest.java b/chapter30/src/test/java/com/seaofnodes/simple/TicTacToeTest.java
new file mode 100644
index 00000000..1d267935
--- /dev/null
+++ b/chapter30/src/test/java/com/seaofnodes/simple/TicTacToeTest.java
@@ -0,0 +1,92 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.util.Ary;
+import java.nio.charset.StandardCharsets;
+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.*;
+
+public class TicTacToeTest {
+ private static String exe;
+
+ @BeforeClass public static void compileGame() throws Exception {
+ exe=TestC.compile(Files.readString(Path.of("docs/examples/TicTacToe.smp")),
+ "ticTacToeGames",new Ary<>(new String[]{TestC.RELEASE_SYS_DIR}),
+ TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String play(String keys) throws Exception {
+ Process child=new ProcessBuilder(exe).redirectErrorStream(true).start();
+ try {
+ child.getOutputStream().write(keys.getBytes(StandardCharsets.US_ASCII));
+ child.getOutputStream().close();
+ assertTrue("Game did not finish",child.waitFor(10,TimeUnit.SECONDS));
+ String text=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(text,0,child.exitValue());
+ return text;
+ } finally {
+ child.destroyForcibly();
+ }
+ }
+
+ private static String board(String squares) {
+ StringBuilder out=new StringBuilder();
+ for(int row=0;row<3;row++) {
+ if(row>0) out.append("---+---+---\n");
+ for(int col=0;col<3;col++) {
+ if(col>0) out.append('|');
+ out.append(' ').append(squares.charAt(row*3+col)).append(' ');
+ }
+ out.append('\n');
+ }
+ return out.toString();
+ }
+
+ @Test public void testPlayerWin() throws Exception {
+ String text=play("1879");
+ assertTrue(text,text.endsWith(board("X OOO XXX")+"You win!\n"));
+ assertEquals(3,text.split("CPU plays ",-1).length-1);
+ }
+
+ @Test public void testCPUWinsEveryLine() throws Exception {
+ // Three rows, three columns, and both diagonals in complete games.
+ String[][] games={
+ {"4697","OOOXOXX X"}, {"1278","XXOOOOXX "}, {"5234","OXXXX OOO"},
+ {"2893","OXXOO OXX"}, {"134","XOXXO O "}, {"5472","OXOXXOX O"},
+ {"234","OXXXO O"}, {"124","XXOXO O "}
+ };
+ for(String[] game:games) {
+ String text=play(game[0]);
+ assertTrue(game[0]+"\n"+text,text.endsWith(board(game[1])+"CPU wins!\n"));
+ }
+ }
+
+ @Test public void testDraw() throws Exception {
+ String text=play("12768");
+ assertTrue(text,text.endsWith(board("XXOOOXXXO")+"It's a draw!\n"));
+ assertEquals(4,text.split("CPU plays ",-1).length-1);
+ }
+
+ @Test public void testBusySquaresAndInvalidInputDoNotTakeATurn() throws Exception {
+ // X takes 5 and O takes 1; neither occupied square can be overwritten.
+ String text=play("x\r\n551q");
+ assertEquals(text,1,text.split("Choose a square",-1).length-1);
+ assertEquals(text,2,text.split("That square is occupied",-1).length-1);
+ assertEquals(text,1,text.split("CPU plays ",-1).length-1);
+ assertTrue(text,text.contains("Your move (1-9): \nChoose a square from 1 to 9: 5\n"
+ +board(" X ")));
+ assertTrue(text,text.contains(board("O X ")));
+ assertTrue(text,text.endsWith("Your move (1-9): "
+ +"\nThat square is occupied. Your move (1-9): "
+ +"\nThat square is occupied. Your move (1-9): \nGoodbye!\n"));
+ }
+
+ @Test public void testCPUBlocksAndEOFQuits() throws Exception {
+ String text=play("12");
+ assertTrue(text,text.contains("CPU plays 3.\n"+board("XXO O ")));
+ assertTrue(text,text.endsWith("Goodbye!\n"));
+ }
+}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java b/chapter30/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java
index adbb10a5..edb2519c 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java
+++ b/chapter30/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java
@@ -91,12 +91,52 @@ public class RegAllocTestSupport {
public static void nullUseMask() {
CodeGen code = graph();
Op def = new Op(A,null,null,false,code._start);
+ new Op(null,null,A,false,code._start); // The dependency must not keep A live across this kill.
new Op(null,null,null,false,code._start,def); // Scheduling dependency only.
RegAlloc alloc = new RegAlloc(code);
assertTrue(BuildLRG.run(0,alloc));
assertTrue(IFG.build(0,alloc));
}
+ public static void independentFunctions() {
+ for( boolean reverse : new boolean[]{false,true} ) {
+ CodeGen code = graph();
+ code._cfg.clear();
+ code._callerSave = RegMask.FULL; // These graphs only test local constraints.
+ FunNode masks = new FunNode(null,com.seaofnodes.simple.type.TypeFunPtr.BOT,
+ "masks",null,null,code._start);
+ Op a = new Op(A,null,null,false,masks);
+ Op b = new Op(null,B,null,false,masks,a); // Fails BuildLRG.
+ ReturnNode ret1 = new ReturnNode(masks,null,null,null,masks);
+ masks.setRet(ret1);
+
+ FunNode kills = new FunNode(null,com.seaofnodes.simple.type.TypeFunPtr.BOT,
+ "kills",null,null,code._start);
+ Op c = new Op(A,null,null,false,kills);
+ new Op(null,null,A,false,kills);
+ Op d = new Op(null,A,null,false,kills,c); // Fails IFG, independently of masks.
+ ReturnNode ret2 = new ReturnNode(kills,null,null,null,kills);
+ kills.setRet(ret2);
+ code._cfg.add(reverse ? kills : masks);
+ code._cfg.add(reverse ? ret2 : ret1);
+ code._cfg.add(reverse ? masks : kills);
+ code._cfg.add(reverse ? ret1 : ret2);
+ RegAlloc alloc = new RegAlloc(code);
+ alloc.regAlloc();
+ assertEquals(0,alloc.regnum(a));
+ assertEquals(1,alloc.regnum(b.in(1)));
+ assertEquals(0,alloc.regnum(c));
+ assertEquals(0,alloc.regnum(d.in(1)));
+ int splits = 0;
+ for( Node n : kills.outs() )
+ if( n instanceof SplitNode split ) {
+ assertEquals("Another function's mask failure must not delay this split",0,split._round);
+ splits++;
+ }
+ assertTrue("Preserving A across the kill requires transfers",splits>0);
+ }
+ }
+
public static void cloneRegisterClass() {
for( RegMask required : new RegMask[]{B,new RegMask(6L)} ) {
CodeGen code = graph();
@@ -144,7 +184,7 @@ public class RegAllocTestSupport {
assertEquals(op,1,found);
code.driver(CodeGen.Phase.RegAlloc,"x86_64_v2","SystemV");
checkRegisters(code);
- assertEquals(op+" reversed="+reversed,8,code._regAlloc._spillScaled);
+ assertEquals(op+" reversed="+reversed,24,code._regAlloc._spillScaled);
code.encode();
}
}
@@ -307,6 +347,34 @@ public class RegAllocTestSupport {
}
}
+ public static void tiedLoopBackedges() {
+ CodeGen code = graph();
+ RegAlloc alloc = new RegAlloc(code);
+ Op init = new Op(A,null,null,false,code._start);
+ LoopNode loop = new LoopNode(null,null,code._start);
+ PhiNode first = new PhiNode("first",loop,init,null);
+ PhiNode second = new PhiNode("second",loop,init,null);
+ first._type = second._type = TypeInteger.BOT;
+ Op back = new Op(A,A,null,false,loop,first);
+ Op narrowed = new Op(A,A,null,false,loop,back);
+ first.setDef(2,back);
+ second.setDef(2,narrowed);
+ new Op(null,A,null,false,loop,first,second);
+ loop.setDef(2,loop);
+ // Model the union created by two-address operations on the backedge.
+ LRG lrg = alloc.newLRG(first,null);
+ lrg._mask = A;
+ for( Node n : new Node[]{second,init,back,narrowed} ) alloc.union(lrg,n);
+ lrg.selfConflict(first);
+ lrg.selfConflict(second);
+ alloc.splitSelfConflict((byte)0,lrg);
+ assertSame(back,first.in(2));
+ assertSame(narrowed,second.in(2));
+ alloc.splitSelfConflict((byte)1,lrg);
+ assertTrue(first.in(2) instanceof SplitNode);
+ assertTrue(second.in(2) instanceof SplitNode);
+ }
+
public static void copyForward() throws Exception {
// Forwarding, fixed operand, two-address tie, intervening clobber,
// another user, and a scheduling-only edge.
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/progs/extern_data.c b/chapter30/src/test/java/com/seaofnodes/simple/progs/extern_data.c
index 9325f65e..e8156d0f 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/progs/extern_data.c
+++ b/chapter30/src/test/java/com/seaofnodes/simple/progs/extern_data.c
@@ -7,8 +7,13 @@ double fraction=1.25;
int64_t bump(void) { counter+=5; return counter; }
__attribute__((CALL_CONV)) extern int64_t test_global(void);
+__attribute__((CALL_CONV)) extern int64_t extern_matches(int32_t);
+__attribute__((CALL_CONV)) extern int64_t extern_less(int32_t);
+__attribute__((CALL_CONV)) extern int64_t extern_seven(void);
int main(void) {
+ if (!extern_matches(7) || extern_matches(8) || !extern_seven() ||
+ !extern_less(6) || extern_less(7) || extern_less(8)) return 2;
int64_t result=test_global();
if(result!=71515 || counter!=15 || small!=-2 || fraction!=1.75) {
printf("result=%lld counter=%d small=%d fraction=%g\n",
@@ -17,6 +22,8 @@ int main(void) {
}
// A second call must observe C's new values, not a Simple initialization copy.
counter=-2;
+ if (!extern_matches(-2) || extern_matches(7) || extern_seven() ||
+ !extern_less(-3) || extern_less(-2)) return 3;
result=test_global();
return result!=-19394 || counter!=6 || small!=-1 || fraction!=2.25;
}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/progs/int32.c b/chapter30/src/test/java/com/seaofnodes/simple/progs/int32.c
new file mode 100644
index 00000000..9694c47b
--- /dev/null
+++ b/chapter30/src/test/java/com/seaofnodes/simple/progs/int32.c
@@ -0,0 +1,26 @@
+#include <stdint.h>
+#include <stdio.h>
+
+typedef struct { int32_t x, y; } Pair;
+typedef struct { uint32_t len; int32_t values[]; } Ints;
+
+__attribute__((CALL_CONV)) extern int64_t int_echo(int32_t);
+__attribute__((CALL_CONV)) extern int64_t int_wrap(int32_t);
+__attribute__((CALL_CONV)) extern int64_t wide_echo(int64_t);
+__attribute__((CALL_CONV)) extern int64_t narrow_sum(int8_t,uint8_t,int16_t,uint16_t,int32_t,uint32_t,int32_t,uint32_t);
+__attribute__((CALL_CONV)) extern Pair *int_pair(void);
+__attribute__((CALL_CONV)) extern Ints *int_array(void);
+
+int main(void) {
+ if (int_echo(-1) != -1 || int_echo(INT32_MIN) != INT32_MIN) return 1;
+ if (int_wrap(INT32_MAX) != INT32_MIN) return 2;
+ if (wide_echo(INT64_MAX) != INT64_MAX || wide_echo(INT64_MIN) != INT64_MIN) return 3;
+ int64_t expected = -128LL + 255 - 32768 + 65535 + INT32_MIN + UINT32_MAX - 7 + UINT32_MAX;
+ if (narrow_sum(-128,255,-32768,65535,INT32_MIN,UINT32_MAX,-7,UINT32_MAX) != expected) return 4;
+ Pair *p = int_pair();
+ if (p->x != -1 || p->y != INT32_MAX) return 5;
+ Ints *a = int_array();
+ if (a->len != 3 || a->values[0] != -1 || a->values[1] != INT32_MAX || a->values[2] != INT32_MIN) return 6;
+ puts("ok");
+ return 0;
+}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/progs/runtime_args.c b/chapter30/src/test/java/com/seaofnodes/simple/progs/runtime_args.c
new file mode 100644
index 00000000..cd676006
--- /dev/null
+++ b/chapter30/src/test/java/com/seaofnodes/simple/progs/runtime_args.c
@@ -0,0 +1,35 @@
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+typedef struct { uint32_t len; unsigned char bytes[]; } Bytes;
+typedef struct { uint32_t len, pad; Bytes *args[]; } Args;
+
+__attribute__((CALL_CONV))
+extern Args *simple_args(int64_t argc, char **argv);
+
+const char *simple_argv(char **argv, int64_t index) { return argv[index]; }
+
+int main(void) {
+ Args *empty = simple_args(0, NULL);
+ if (!empty || empty->len != 0) return 1;
+ if (simple_args(-1, NULL) != NULL) return 2;
+ if (simple_args(4294967296LL, NULL) != NULL) return 6;
+
+ char name[] = "program", blank[] = "", utf8[] = "\xc3\xa9\xcf\x80";
+ char *argv[] = { name, blank, utf8, name };
+ Args *args = simple_args(4, argv);
+ if (!args || args->len != 4) return 3;
+ for (int i = 0; i < 4; i++) {
+ if (!args->args[i] || args->args[i]->len != strlen(argv[i]) ||
+ memcmp(args->args[i]->bytes, argv[i], args->args[i]->len)) return 4;
+ }
+ // Copies survive changes to the native buffers, and repeated argv pointers
+ // still produce independent Simple arrays.
+ name[0] = 'X';
+ utf8[0] = 0;
+ args->args[0]->bytes[0] = 'Y';
+ if (args->args[3]->bytes[0] != 'p' || args->args[2]->bytes[0] != 0xc3) return 5;
+ puts("ok");
+ return 0;
+}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/progs/sort.c b/chapter30/src/test/java/com/seaofnodes/simple/progs/sort.c
index ad76cc7f..08acf2da 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/progs/sort.c
+++ b/chapter30/src/test/java/com/seaofnodes/simple/progs/sort.c
@@ -5,14 +5,14 @@
typedef struct AryInt {
unsigned int len;
- long long data[];
+ int data[];
} AryInt;
__attribute__ ((CALL_CONV))
extern AryInt* merge_sort(AryInt*);
-long long primes[] = { 25, 97, 89, 83, 79, 73, 71, 67, 61, 59, 53, 47, 43, 41, 37, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2 };
+int primes[] = { 25, 97, 89, 83, 79, 73, 71, 67, 61, 59, 53, 47, 43, 41, 37, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2 };
int main() {
AryInt *as = (AryInt*)primes;
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/progs/stream_io.c b/chapter30/src/test/java/com/seaofnodes/simple/progs/stream_io.c
new file mode 100644
index 00000000..5cd1096a
--- /dev/null
+++ b/chapter30/src/test/java/com/seaofnodes/simple/progs/stream_io.c
@@ -0,0 +1,49 @@
+#include <errno.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <unistd.h>
+
+// Deterministic short I/O and failures; ordinary descriptors still use libc.
+extern ssize_t __real_read(int, void *, size_t);
+extern ssize_t __real_write(int, const void *, size_t);
+static int mode, calls, bad;
+static int64_t count, offset;
+
+int32_t test_io_mode(int32_t next, int64_t start) {
+ mode = next;
+ calls = bad = 0;
+ count = 0;
+ offset = start;
+ return 0;
+}
+int64_t test_io_count(void) { return count; }
+int32_t test_io_status(void) {
+ return bad || (mode == 5 && (errno != EAGAIN || calls != 1));
+}
+
+ssize_t __wrap_read(int fd, void *buffer, size_t size) {
+ if (fd != 700) return __real_read(fd, buffer, size);
+ ++calls;
+ if (mode == 1 && calls == 1) { errno = EINTR; return -1; }
+ if (mode == 2 && calls > 1) { errno = EIO; return -1; }
+ if (count == 24593) return 0;
+ size_t n = size < 137 ? size : 137;
+ if (n > (size_t)(24593-count)) n = (size_t)(24593-count);
+ for (size_t i=0; i<n; ++i) ((unsigned char *)buffer)[i] = (count+i)%251;
+ count += n;
+ return (ssize_t)n;
+}
+
+ssize_t __wrap_write(int fd, const void *buffer, size_t size) {
+ if (fd != 701) return __real_write(fd, buffer, size);
+ ++calls;
+ if (mode == 1 && calls == 1) { errno = EINTR; return -1; }
+ if (mode == 3 && calls > 1) { errno = EIO; return -1; }
+ if (mode == 4) return 0;
+ if (mode == 5) { errno = EAGAIN; return -1; }
+ size_t n = size < 7 ? size : 7;
+ for (size_t i=0; i<n; ++i)
+ if (((const unsigned char *)buffer)[i] != (offset+count+i)%251) bad = 1;
+ count += n;
+ return (ssize_t)n;
+}
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/progs/stringHash.c b/chapter30/src/test/java/com/seaofnodes/simple/progs/stringHash.c
index 2c6d0fa9..6578c852 100644
--- a/chapter30/src/test/java/com/seaofnodes/simple/progs/stringHash.c
+++ b/chapter30/src/test/java/com/seaofnodes/simple/progs/stringHash.c
@@ -7,7 +7,7 @@ typedef struct ArrayU8 {
} ArrayU8;
typedef struct String {
ArrayU8 * cs;
- long long _hashCode;
+ int _hashCode;
} String;
diff --git a/chapter30/src/test/smp/separate/client/Main.smp b/chapter30/src/test/smp/separate/client/Main.smp
index 077f6575..e70079db 100644
--- a/chapter30/src/test/smp/separate/client/Main.smp
+++ b/chapter30/src/test/smp/separate/client/Main.smp
@@ -3,4 +3,4 @@ val run={int n ->
val c=new Counters.Counter(n);
return c.get()+100*Counters.next_noInline();
};
-return run(arg);
+int n=arg; return run(n);
diff --git a/docs/roadmap.ja.md b/docs/roadmap.ja.md
index 81b8a69f..5612e0f1 100644
--- a/docs/roadmap.ja.md
+++ b/docs/roadmap.ja.md
@@ -5,35 +5,45 @@
未完了の作業と将来の言語設計案の要約です。現在の言語仕様や実装済みの機能を
示すものではありません。詳細と最新の判断は[英語版](roadmap.md)を参照してください。
-現在の作業場所は[第30章: 作業中](../chapter30/README.ja.md)です。
-第29章の完成したコンパイラを引き継ぎます。次のテーマは未定です。
-ソース上のプライバシー制約は、最初の整理が終わるまで保留します。
-自動メモリ管理と、2-D グラフィックス・キーボード・音声を使う小さなゲームも将来案です。
+現在の作業場所は[第30章: テキスト整形と診断](../chapter30/README.md)です。
+ASCII 三目並べを題材に、文字列補間、型別の文字列変換、1 キー入力、
+ソース位置付きの診断を実装しました。以下は残っている作業です。
+
+## 目次
+
+- [正確性とコンパイル](#正確性とコンパイル) — 診断、増分解析・ビルド、ロード、最適化。
+- [言語とランタイム](#言語とランタイム) — アクセス制御、構築契約、初期化、メモリ管理。
+- [データとライブラリの設計案](#データとライブラリの設計案) — 配列、文字列、構造体の値、グラフィカルなゲーム。
## 正確性とコンパイル
+- 診断に関連する宣言や呼び出し位置を追加し、最適化・インポート後も位置を保持して連鎖的なエラーを抑える。
- Eval2 の追加ロード時のスケジューリング用領域を整理し、`GlobalEscapeNode` が共有する呼び出し・関数要約の走査を増分化する。
-- 依存関係全体を調べ、変更を伝播してから保存 IR を再利用する、正しい増分ビルドを実装する。
+- 第28b章の推移的なタイムスタンプ検査を拡張し、入力の内容・コンパイラの版・オプションを記録するビルドドライバを実装する。
- 型の読み込みからネイティブリンクまでのライブラリ変更を検出する。SHA-256 を含むシンボルの照合は将来案であり、Eval2 は整合したビルドを前提とする。
- コンパイル単位ごとの選択的ロードを、オブジェクト内の関数単位へ細分化する。
- 最適化後の関数サイズと安定した順位を使い、インライン化の選択を決定的にする。
- CFG の実行頻度推定、ループの回転、定数インスタンスフィールドの格納省略を検討する。
+- [整数命令の幅](roadmap.md#integer-instruction-widths): 明示的な縮小を命令選択でまとめ、範囲の証明がある場合に32ビット命令を使う。使用側が必要とするビットの後向き解析は別の将来案とし、先に現在のレジスタ割り当ての再試行失敗を調べる。
+- [関数ごとの呼び出し規約](roadmap.md#per-function-calling-conventions): 破壊レジスタの要約から始め、既知の直接呼び出しに専用の引数・戻り値配置を検討する。間接呼び出し、再帰、公開ABIのメタデータ、関数間の共同割り当ては段階的な将来案とする。
## 言語とランタイム
- アンダースコアによる公開インターフェースの選別に加え、ソースの private アクセス規則を決めて強制する。
-- 未完成の self をヘルパーへ渡せる条件と、コンストラクタの完了検査を行う時点を検討する。
+- 第29章の前向き型フローによる部分初期化済みフィールドの読み出しを基礎に、推論されたフィールド要件・入れ子の保証・分離コンパイル用の構築契約を将来検討する。
- ELF シンボルの可視性と、未知の呼び出し元・コード出力の所有権を区別する。
- クラス初期化子の整数引数を引数配列に置き換える。
- 初期化全体を静的データで表せるクラスについて、実行時の初期化呼び出しと状態を省略する。
- コンパクトなコード/データ領域、ネイティブポインタ型、GC に依存しない寿命管理を設計する。
+- C のバイトポインタは8バイトとし、アドレス範囲とメモリ所有権を分けて追跡する。C による Simple バッファの一時借用は、参照を保持しないという文書化された契約で許可する。C レイアウトの構造体は将来の拡張とする。
+- 所有権、共有、循環参照、ネイティブ側での参照保持を含めた自動メモリ管理を検討する。
## データとライブラリの設計案
- 名前付きフィールドの末尾に配列を配置する構造体と、異なる幅の長さフィールド。
- 要素を生成する関数を使う配列初期化。
-- 可変・不変・C 互換・Unicode・短い値文字列の各用途に応じた文字列ライブラリ。
+- 実装済みの `sys.aryu8` と `sys.text` を基礎に、所有権・Unicode・C 互換・短い値文字列の設計を拡張する。
- 構造体のインライン格納と値渡し。
-- `int` の既定幅を現在の `i64` から `i32` に変えるかどうか。
+- ASCII 三目並べの次に、2-D グラフィックス・キーボード・音声を使う小さなゲームでライブラリを試す。
これらの言語変更には、それぞれ設計判断と導入する章の境界が必要です。
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 81608f30..ac0ac239 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -3,42 +3,52 @@
English | [日本語](roadmap.ja.md)
Open compiler work and language proposals, reviewed against the current
-snapshots on 2026-11-07. These are future changes, not promises about the
+snapshots on 2026-11-08. These are future changes, not promises about the
language implemented by the tutorial. Language changes still need their own
design decisions and chapter boundaries.
-## Current chapter: Chapter 30
+## Contents
+
+- [Compilation and optimization](#compilation-and-optimization) — diagnostics, analysis costs, builds, loading, and code quality.
+ - [General cleanup](#general-cleanup) - Some general quality-of-life suggestions
+ - [Phase ordering and incremental analysis](#phase-ordering-and-incremental-analysis) — scheduling costs, shared escape scans, and late identity precision.
+ - [Reliable incremental builds](#reliable-incremental-builds) — input identities, compatibility, and relinking.
+ - [Function-level loading](#function-level-loading-of-library-ir) — load individual library bodies.
+ - [Deterministic inlining](#deterministic-inlining) — stable choices using optimized body sizes.
+ - [CFG frequency estimates](#cfg-frequency-estimates) — guide scheduling and layout.
+ - [Loop rotation](#loop-rotation) — bottom-tested loops with correct entry and exit behavior.
+ - [Integer instruction widths](#integer-instruction-widths) — local narrowing patterns, 32-bit instructions, and later demanded-bit analysis.
+ - [Per-function calling conventions](#per-function-calling-conventions) — precise clobbers, custom interfaces, and later joint allocation.
+ - [Constant instance-field storage](#constant-instance-field-storage) — omit slots whose values are constant across instances.
+- [Language boundaries](#language-boundaries) — native argument representation, source access rules, and symbol visibility.
+ - [C byte pointers](#c-byte-pointers) — distinct native pointers with byte loads/stores and C-owned memory.
+ - [Source privacy](#source-privacy) — enforce private access through imports and optimization.
+ - [ELF visibility](#elf-visibility-and-unknown-callers) — separate linker exports from unknown-caller reachability.
+- [Initialization and runtime](#initialization-and-runtime) — entry arguments, static initialization, and pointer storage.
+ - [Entry arguments](#entry-arguments) — pass argument arrays to class initializers.
+ - [Pre-initialize classes](#pre-initialize-classes-from-elf-data) — replace provably static initialization with ELF data.
+ - [Compact arenas](#compact-code-and-data-arenas) — four-byte references and explicit native pointer boundaries.
+- [Longer-term proposals](#longer-term-language-and-library-proposals) — deferred language and library designs.
+ - [Partial-initialization contracts](#inferred-partial-initialization-contracts--deferred) — inferred requirements, nested guarantees, and imported contracts.
+ - [Automatic memory management](#automatic-memory-management) — ownership, sharing, cycles, and native retention.
+ - [A graphical game](#a-small-graphical-game) — exercise graphics, keyboard, and audio APIs.
+ - [Trailing arrays](#structs-with-trailing-arrays) — inline elements after named fields.
+ - [Array initialization](#array-initialization) — generate each element during allocation.
+ - [String libraries](#string-libraries) — ownership, Unicode, and additional representations beyond byte buffers.
+ - [Struct values](#struct-values) — inline storage, copying, and by-value calls.
-[Chapter 30](../chapter30/README.md) is the workspace for the next project,
-starting from the completed Chapter 29 compiler. Its topic is not yet selected;
-the projects below remain candidates, not a committed scope.
+## Compilation and optimization
-[Chapter 29](../chapter29/README.md) carries the parser split into grammar-sized
-components, with `Var` in `parser/` and `IterPeeps` in `codegen/`. Chapter 29
-retains that cleanup snapshot; Chapter 28b retains the preceding compiler.
+### General cleanup
-Constructor completion now uses private-memory field types and the existing
-TypeCheck walk, after Iter/Opto propagation. The
-[incomplete-reference design](../chapter29/docs/construction-groups.md) uses a
-partial qualifier plus forward field views for local read-only borrowers.
-Inferred field requirements, nested guarantees, and imported construction
-contracts remain longer-term proposals.
Keep the recursive-descent path aligned with the grammar. Review moving `TYPES`
-to `CodeGen`. Consider whether
-an allocation-free use of try-with-resources makes keep/unkeep pairing clearer;
-this remains an experiment, not a new resource-management convention.
-
-Source privacy is deferred until after this initial cleanup. The archived
-implementation and tests are experiments, not part of Chapter 29's behavior.
+to `CodeGen`.
-## Compilation and optimization
+Consider whether an allocation-free use of try-with-resources makes keep/unkeep
+pairing clearer; this remains an experiment, not a new resource-management
+convention.
-### Better error messages
-Improve compiler diagnostics: identify the offending source expression, explain
-what was expected and what was found, and point to relevant declarations or call
-sites. Preserve useful source locations through optimization and imports, and
-avoid cascading errors that obscure the original problem.
### Phase ordering and incremental analysis
@@ -52,10 +62,8 @@ Remaining work:
unit Stops now reuse its result, but updating that result still scans all
calls and unloaded summaries. Handle contributors changing or disappearing,
rather than only accumulating unions. The
- [outside-world escape flow](../chapter29/docs/escape-flow.md), now in
- Chapters 28a/28b/29, has removed the
- frozen public inventory and previous-result escape normalization; the shared
- scan remains.
+ [outside-world escape flow](../chapter28a/README.md#escaping-or-the-startstop-cycle) already shares
+ the result; the remaining cost is recomputing it.
- Improve precision for identities discovered after Opto's foreign seed is
established. They currently remain in the cofinite foreign tail. Preserve
monotone propagation and imported callback behavior when refining this bound.
@@ -130,6 +138,72 @@ loop, or adding partial peeling. Preserve condition side effects, loop-local
scope, and the destinations of `break` and `continue`. The current parser
constructs a loop with its test at the head.
+### Integer instruction widths
+
+Keep the Chapter 30 language rule: `int` aliases `i32`, but inferred values
+and intermediate arithmetic retain their existing rules. Two `int` operands
+can produce a 33-bit sum; narrowing on assignment is a separate operation.
+
+Start with local instruction-selection improvements, without a new analysis:
+
+- Recognize `(x<<32)>>32` as sign-extension instead of two shifts.
+- Add an operand-width choice to integer machine operations and the shared
+ encoder. On x86, dropping REX.W can remove a prefix byte when no extended
+ register needs REX. The encoder already has `rexF(..., wide, ...)`, but
+ register arithmetic currently forces wide operations.
+- Use range proofs for suitable 32-bit arithmetic with a result in `u32`,
+ and 32-bit signed comparisons between operands proven to fit `i32`.
+- Combine arithmetic with explicit narrowing: a narrowed add can use a
+ 32-bit add followed by sign-extension. Bypass input extensions where only
+ low bits are consumed, preserving shared producers' other wide uses.
+
+An x86 32-bit result zeroes the upper half; negative signed results still
+need sign-extension before a full-width consumer. Keep shifts and division
+under separate rules: operand width changes shift-count masking and which
+high bits can flow down, and narrow signed division introduces the
+`INT_MIN / -1` overflow case. Do not infer signed representability solely
+from `TypeInteger.log_size()`; use actual range bounds.
+
+Longer term, distinguish computed bits from demanded bits. `TypeInteger`
+already provides ranges and a derived possible-bits mask. Tracking which
+bits consumers observe could keep arithmetic chains and loops narrow and
+extend only at wide consumers, but requires backwards propagation through
+shared uses and Phis. This is a separate analysis proposal, not part of the
+local selection work. Narrow operations still occupy whole registers.
+Diagnose the current allocator retry failure before changing its input graph.
+
+### Per-function calling conventions
+
+Allocate functions independently first. Later, give known direct-call targets
+their own machine contracts: argument/return locations, clobbered registers,
+stack argument layout/alignment, and integer-extension requirements. Start
+with precise clobber summaries under the existing convention, then consider
+custom argument/return locations. Allocate callees before callers and freeze
+each contract; caller preferences can guide choices without forcing every
+call site to agree on its preferred registers.
+
+Preserve machine-call relationships before Unlink discards the ideal call
+graph. Replace compilation-wide parameter, return, preservation, and call-kill
+masks with contract lookups. Keep C entry points and callbacks on the C ABI,
+using wrappers if an internal custom entry is worthwhile. Source privacy
+alone does not establish that all calls are known.
+
+Indirect calls need a convention shared by all possible targets, entry
+thunks, or a richer function-pointer representation. Public Simple functions
+could export their selected contract in target-specific metadata after
+allocation; ideal IR is currently serialized before allocation. A changed
+contract requires rebuilding callers or rejecting stale objects even when
+the source type is unchanged.
+
+Joint caller/callee coloring is a larger, separate proposal. Model argument
+and result transfers as preferences, retaining moves when needed; current
+LRGs and spill slots belong to individual functions. Values live across a
+call must survive the callee's transitive clobbers. Recursive activations
+require saves/spills and cannot be represented just by merging static live
+ranges. Initially use fixed conventions for recursive groups. If interfaces
+are later renegotiated, budget retries by function or recursive group, not
+one global round counter shared by unrelated functions.
+
### Constant instance-field storage
When a final instance field has the same constant value in every instance,
@@ -141,12 +215,45 @@ instance-layout optimization.
## Language boundaries
+### C byte pointers
+
+Introduce an eight-byte C pointer type, initially pointing only to bytes, with
+indexed byte Loads and Stores, null checks, and defined offset operations.
+Track whether the storage is C-managed or Simple-managed separately from
+pointer width and address range. The prospective low-4-GiB Simple heap does not
+make low-address C allocations Simple-owned; high-address buffers require the
+full-width C pointer. A low-address C buffer might admit a compact reference,
+but only with compatible layout and retained C ownership. Revisit whether that
+is useful when designing the heap and automatic memory management.
+
+Allow Simple buffers to be borrowed by C under a documented promise that C
+does not retain the pointer. Initially this lifetime promise is not enforced.
+Returning a pointer must not silently transfer ownership, and Simple memory
+management must not free or traverse C-owned storage.
+
+Use normal memory SSA. C views of Simple buffers can alias Simple Loads and
+Stores, so representation alone cannot select a disjoint native-memory alias
+class. Native calls must expose effects on borrowed Simple memory as well as
+C memory. Lengths and writable access need API contracts; a raw C pointer
+carries no Simple array header or length.
+
+This would let the runtime scan and copy argument bytes in Simple. Access to
+`argv[i]` would still need the small C helper: `char **` requires pointer loads,
+which are outside the initial byte-only scope.
+
+Replace existing I/O's `i64` address convention with explicit native views of
+Simple buffers, preserving their ownership and the non-retention contract.
+A later Simple `C` struct facility could supply C-compatible field layout for
+libc interfaces. Such structs have no Simple constructor/destructor and do not
+intermingle with the managed Simple object graph; specify native layout and
+ownership independently of ordinary Simple structs.
+
### Source privacy
Underscores already select the public interface, but source access to another
class's private fields is not rejected. Revisit source enforcement and its
-preservation across imports after the Chapter 29 cleanup. This language change
-still needs review; it is not a correction to impose on earlier chapters.
+preservation across imports. This language change still needs review; it is not
+a correction to impose on earlier chapters.
- A field or method beginning with `_` is accessible only from its declaring
class, including constructors and methods. Enclosing, nested, and sibling
@@ -217,7 +324,8 @@ need not overlap or live near zero; choose absolute addresses or offsets from
an arena base when implementing placement and indirect calls. Native function
thunks can occupy the code arena while native libraries live elsewhere.
Currently function pointers occupy four bytes and use low-address thunks;
-ordinary data pointers still occupy eight bytes.
+ordinary data pointers still occupy eight bytes. See [Automatic memory
+management](#automatic-memory-management).
Give full-width native pointers separate types and foreign lifetime contracts.
Opaque device addresses may need to pass through without CPU dereference.
@@ -226,6 +334,9 @@ ownership, borrowing, cycles, and native retention. Compact storage alone
does not establish those rules. Preserve static class images and explicit
native boundary conversions when choosing the representation.
+Defer the four-byte data-pointer switch to a later chapter alongside memory
+management, which will also establish the allocator and native ownership rules.
+
## Longer-term language and library proposals
These are language-design alternatives. The sketches below are not executable
@@ -233,10 +344,11 @@ examples of the current Simple grammar.
### Inferred partial-initialization contracts — deferred
-Allow partial arguments to expose initialized fields, retain partial references,
-and propagate nested initialization guarantees, while reserving final-field
-writes to the original constructor. Cliff decided against implementing this
-now (2026-11-07): tracing requirements through aliases, nested fields, forwarding,
+Chapter 29 already permits local reads of proven initialized fields and limited
+retention of partial references through normal forward type flow. The deferred
+extension is inferred field requirements, nested guarantees, and contracts for
+separately compiled callees, while reserving final-field writes to the original
+constructor. Tracing requirements through aliases, nested fields, forwarding,
recursive calls, and returned/stored references adds more infrastructure than
is justified for this feature alone.
@@ -244,8 +356,7 @@ Revisit alongside a general backwards/liveness analysis, which may also serve
future memory management. The normal-type contract proposal and its unresolved
details are recorded in the
[construction notes](../chapter29/docs/construction-groups.md#deferred-field-aware-contracts-2026-11-07).
-The forward-only prototype permits local reads of proven initialized fields;
-it adds no inferred contracts. Keep the separate-compilation restriction meanwhile.
+Keep the separate-compilation restriction on `^` functions meanwhile.
### Automatic memory management
@@ -255,9 +366,10 @@ as design references. Decide ownership, borrowing, sharing, cycles, and native
retention together with the existing object and array model. This is a language
design candidate, independent of compact pointer storage.
-### A small interactive game
+### A small graphical game
-Build a small game in Simple with 2-D graphics, keyboard input, and sound.
+Follow ASCII tic-tac-toe with a small game using 2-D graphics, keyboard input,
+and sound.
Use it to exercise practical library APIs and native bindings; choose the game
and supporting graphics/audio library when this project is taken up.
@@ -298,14 +410,14 @@ allocation does not provide this constructor operation.
### String libraries
-Design a family of byte-based strings with UTF-8 decoding supplied by library
-functions. Consider these distinct representations and interfaces:
+`sys.aryu8` already provides a growable buffer wrapper; `sys.text` formats into
+it and copies the result into an immutable byte array. Extend these foundations
+with explicit ownership contracts and UTF-8 decoding. Consider:
- A growable string with inline bytes and cached metadata, returning a
potentially new receiver when capacity increases.
-- An immutable string constructed all at once, with defensive copying where
- necessary. Read-only access alone does not establish exclusive ownership.
-- A StringBuilder-style wrapper holding a replaceable pointer to its buffer.
+- Immutable-string ownership and sharing beyond the existing formatting result.
+ Read-only access alone does not establish exclusive ownership.
- Unicode decoding and iteration over the byte representation, including
behavior at invalid character boundaries.
- A C-compatible string with a trailing zero and directly available length.
@@ -313,7 +425,7 @@ functions. Consider these distinct representations and interfaces:
Keep mutable storage private by default and specify sharing, concatenation,
capacity growth, and native I/O contracts. Existing `sys` byte-array and
-buffer helpers are a starting point, not this complete string design.
+buffer helpers provide the basic builder and formatting operations.
### Struct values
@@ -328,8 +440,3 @@ or expose an interior address with an invalid lifetime. Decide how consuming
an enclosing object's lifetime could permit such an address. Keep method
syntax uniform and define the calling convention for by-value arguments.
-### Default integer width
-
-Consider making `int` mean `i32` instead of the current `i64`. This changes
-overflow behavior, layouts, APIs, and native interfaces, so it needs a language
-decision and a deliberate compatibility boundary.
diff --git a/skills/chapter25-codex-notes.md b/skills/chapter25-codex-notes.md
index 280d52ca..3a9b2571 100644
--- a/skills/chapter25-codex-notes.md
+++ b/skills/chapter25-codex-notes.md
@@ -18,6 +18,8 @@ The [incremental build design](incremental-builds.md) expands the roadmap's
dependency and output-ownership requirements.
For shared viewer work, read [the graph viewer notes](graph-viewer-codex-notes.md).
+Use LF line endings for all new text files, including files created on Windows.
+
## Partial-initialization decision (2026-11-07)
Cliff declined inferred partial-initialization contracts for now. The normal-type,
@@ -36,10 +38,21 @@ callers promise no initialized fields. No new passes or backward demand walks.
Cliff committed the Chapter 29 work as `bb9b30b2c` and requested merging
`escape-flow` into `main`, then starting Chapter 30. Local `main` now includes
that commit. The working branch is `ch30`, with a complete `chapter30/` copy
-and build/IDE entries. Its topic is not yet selected. Continue new work there;
+and build/IDE entries. Small games now drive compiler and sys-library improvements,
+starting with ASCII tic-tac-toe; the games are workloads, not the chapter's theme.
+Runnable examples belong in `chapter30/docs/examples`, not `src/test/smp/examples`.
+Do not add Python to the codebase or use Python test drivers; use the existing
+Java test harness.
+Continue new work there;
Chapter 29 retains the completed compiler and documentation. Inherited Java
test names keep their historical identities.
+`ScopeNode.dup()` shares `Kind` boundaries and `Var` records. When pruning a
+saved break/continue scope, use `_pop()` and copy surviving forward references
+with `balanceLoopFRefs`; `pop()` promotes them by mutating shared boundaries
+and indices while the active scope still has its old binding order. Merge jump
+scopes with loop-reference balancing, not `balanceIf`'s declaration checks.
+
## Historical restart handoff (2026-11-06; superseded by Chapter 30)
Cliff requested this handoff before restarting. Active branch is `escape-flow`,
@@ -486,7 +499,11 @@ and a small game with graphics, keyboard input, and sound.
Lexer snapshots keep their original CompUnit through inlining and Call/CLInit
serialization; recover representative sites by rescanning only on rejection.
Source text is assumed available. Imported source paths are diagnostic data,
- not `_smp` rebuild/discovery state. General error-message migration is deferred.
+ not `_smp` rebuild/discovery state. Chapter 30's CLI also renders
+ `ParseException.diagnostic()` through this formatter; `getMessage()` retains
+ the plain message for compiler clients and tests. Snapshot the error location
+ before reading a token for the diagnostic. Use underscores plus a caret and
+ one-based display columns (eight-column tabs) for Emacs's default navigation.
- Constructors start in 16, independent binding/access permissions in 17a,
syntax sugar and inference in 17b. Historical Chapter 17 references mean 17b.
@@ -808,10 +825,24 @@ and a small game with graphics, keyboard input, and sound.
## Register allocation review
+- Chapter 30 allocates scheduled functions independently, retains completed
+ register assignments, and applies the eight-round limit per function. Stage
+ failures previously blocked unrelated functions before they reached coloring.
+ BuildLRG, IFG, and coalescing must all use the active function's blocks.
+- A null operand register mask means no runtime register use. IFG must ignore
+ these inputs just as BuildLRG does; otherwise hidden class arguments create
+ false liveness across calls and unnecessary constant rematerialization.
+- Do not bypass a def-side split just because a use-side split shares its
+ block. A register kill between them can require both. Leave their removal to
+ interference-aware coalescing; bypassing and repairing them wastes a round.
- Phi self-conflict splitting must isolate inputs shared with another Phi,
not only the entry input and result. Two loop Phis sharing a backedge value
otherwise stay in one conflicting LRG across every round. The correction
starts in 20; 24-25 retain their first cold-only attempt.
+- In Chapter 30, distinct loop backedge nodes can still join the same LRG
+ through a chain of two-address instructions. On a repeated Phi self-conflict,
+ split the loop backedges even when the inputs are not identical nodes;
+ repeatedly copying only the Phi outputs cannot break that interference.
- Chapter 25's cloned-call relinking must visit old nodes in node-ID order.
Iterating the clone IdentityHashMap changed call edges and optimizer worklist
order across JVMs. The SystemV sys build reproduced on Windows with
@@ -1122,6 +1153,10 @@ Consequences:
Preserve `testBrainFuck`, `testWhileWithNullInside`, `testNullGuardErrors`,
`testLinkedList0`, and `testCoRecur`, including their expected errors.
+- Chapter 30's integer widening first uses `[0, INT_MAX]` for nonnegative
+ ranges, then `i32` or `u32` as the range expands. Widening straight to `i32`
+ invents negative values in unsigned loops and can make a valid `u32`
+ argument fail TypeCheck. Keep boundary-crossing and wraparound regressions.
- Integer widening starts in Chapter 24. `nonZero()` preserves `_widen` when
narrowing a range. B10's correction is in 24, with `TypeTest.testNonZeroWidening`
in 24-25. Both now use widening 0-3 and dual widening `3-widen`, preserve