diff options
| author | Cliff Click <cliffc@acm.org> | 2026-11-08 14:10:02 -0800 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-11-08 14:10:02 -0800 |
| commit | 06fb5aceeb65a75b1cd643debd667c52fdcafcb9 (patch) | |
| tree | c99813eaf27d98f840245302c4d6e923c964307e /chapter30 | |
| parent | 6560a92433a38825fd1d44d9c66ffba9f26990ff (diff) | |
| download | Simple-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.
Diffstat (limited to 'chapter30')
106 files changed, 2693 insertions, 522 deletions
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); |
