diff options
| author | Cliff Click <cliffc@acm.org> | 2026-08-10 11:11:18 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-10-24 15:56:44 -0700 |
| commit | 741d78b6e840c88b7b183010c812ad982b342da1 (patch) | |
| tree | 84a75eb7ad351bffd20cd5fb152bab032b74271c | |
| parent | 8d90d2292bbdb23596849c3c3d1cda2f6cad783e (diff) | |
| download | Simple-741d78b6e840c88b7b183010c812ad982b342da1.tar.gz Simple-741d78b6e840c88b7b183010c812ad982b342da1.zip | |
Chapter 28a: Modules and Lazy Class Initializationlinear-chapter28a
Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com>
Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com>
Co-authored-by: Symious <14933944+symious@users.noreply.github.com>
Co-authored-by: Jun Kawasaki <jun784@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
345 files changed, 13886 insertions, 9960 deletions
@@ -68,7 +68,7 @@ include $(firstword $(wildcard ../graph/graph.mk graph/graph.mk)) include $(firstword $(wildcard ../print/print.mk print/print.mk)) include $(firstword $(wildcard ../isa/isa.mk isa/isa.mk)) test_classes := $(patsubst $(TST)/%java,$(CLZDIR)/test/%class,$(test_javas)) -test_cp := $(patsubst $(TST)/$(SIMPLE)/%.java,com.seaofnodes.simple.%,$(wildcard $(TST)/$(SIMPLE)/*Test.java)) com.seaofnodes.simple.codegen.X86EncodingTest +test_cp := $(patsubst $(TST)/$(SIMPLE)/%.java,com.seaofnodes.simple.%,$(wildcard $(TST)/$(SIMPLE)/*Test.java)) classes = $(main_classes) $(test_classes) # All the libraries libs = $(wildcard lib/*jar) @@ -184,3 +184,18 @@ spill-stats: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag .PHONY: build build: $(CLZDIR)/main/.mtag + +# The ABI shim gives standalone native tests the same entry contract as clients. +CPU_PORT := x86_64_v2 +ifeq ($(OS),Windows_NT) +CALL_CONV := win64 +NATIVE_ABI := ms_abi +else +CALL_CONV := SystemV +NATIVE_ABI := sysv_abi +endif +runtime_obj := build/release/$(CPU_PORT)_$(CALL_CONV)/simple_crt.o +$(runtime_obj): src/main/c/simple_crt.c Makefile + @mkdir -p $(dir $@) + gcc -c -O2 -DCALL_CONV=$(NATIVE_ABI) $< -o $@ +tests: $(runtime_obj) diff --git a/README.ja.md b/README.ja.md index 40990585..37c89fe6 100644 --- a/README.ja.md +++ b/README.ja.md @@ -66,7 +66,7 @@ Simple 言語のスタイルは、C または Java のサブセットに倣っã� * [第25章](docs/chapter25/README.ja.md):型が決まる前の SSA * [第26章](docs/chapter26/README.ja.md):構築とプライベートメモリ * [第27章](docs/chapter27/README.ja.md):エスケープ解析と未知の呼び出し元 -* 第28a章:モジュールと遅延クラス初期化 +* [第28a章](docs/chapter28a/README.ja.md):モジュールと遅延クラス初期化 * 第28b章:分離コンパイル 付録:[ネイティブライブラリ](docs/appendices/native-libraries.md)、 @@ -75,7 +75,7 @@ The Simple language is styled after a subset of C or Java. * [Chapter 25](docs/chapter25/README.md): SSA Before Types Are Known * [Chapter 26](docs/chapter26/README.md): Construction and Private Memory * [Chapter 27](docs/chapter27/README.md): Escape Analysis and Unknown Callers -* Chapter 28a: Modules and Lazy Class Initialization +* [Chapter 28a](docs/chapter28a/README.md): Modules and Lazy Class Initialization * Chapter 28b: Separate Compilation Appendices: [Native libraries](docs/appendices/native-libraries.md), diff --git a/docs/chapter28a/README.ja.md b/docs/chapter28a/README.ja.md new file mode 100644 index 00000000..84dcdda2 --- /dev/null +++ b/docs/chapter28a/README.ja.md @@ -0,0 +1,94 @@ +# 第28a章: モジュールと遅延クラス初期化 + +[次の章: 分離コンパイル](../chapter28b/README.ja.md) + +[English](README.md) | 日本語 + +[Previous: Chapter 27](../chapter27/README.ja.md) | +[Next: Chapter 28b](../chapter28b/README.ja.md) + +この章では複数のソースファイルを一度にコンパイルします。各ファイルは +一つのクラスと共有オブジェクトを定義し、トップレベルの文は隠れた +`<clinit>` 関数になります。最初の能動的な使用で初期化し、その後は +同じ結果を使います。クラスへの参照を保持するだけでは初期化しません。 + +## 必要なときだけ表を作る + +実行できる例は [`src/test/smp/lazy`](../../src/test/smp/lazy) にあります。 +`App.smp` は Main、Tables、Stats への参照を公開します。 +`App/Stats.smp` の `builds` は初期化回数を数えます。 +`App/Tables.smp` は次のように表を構築します。 + +```simple +Stats.builds++; +int[] _squares=new int[256]; +for(int i=0; i<256; i++) _squares[i]=i*i; +val square={u8 n -> _squares[n];}; +``` + +`App/Main.smp` が入口です。 + +```simple +return arg ? Tables.square(3)+Tables.square(4)+100*Stats.builds : Stats.builds; +``` + +引数が 0 なら結果は 0 で、Tables は初期化されません。引数が 1 なら +9 + 16 + 100 = 125 です。二度の呼び出しでも表は一度しか作りません。 +`_squares` は非公開のフィールドで、公開関数を通して読み出します。 +完全なファイル一覧と評価用 Java コードは [英語版](README.md) を参照してください。 + +## ファイルと解析の境界 + +`ParseAll` は未解決のクラス名や型から必要なソースを探します。 +ディレクトリ階層が名前空間になり、`App/Main` から `Tables` は兄弟の +`App/Tables` を指せます。親の初期化は子より先なので、親も解析します。 +相互参照する型はソースの発見後に閉じます。 + +`CompUnit` はソース、クラス型、関数をまとめます。各単位には +`StartCU` と `StopCU` があり、全体の Start/Stop の下で解析します。 +公開関数には未知の呼び出し元が存在します。非公開の関数は引き続き +エスケープ解析によって特殊化できます。フィールド名の先頭の `_` は +非公開を表します。ただし、現実装はこれを公開インターフェースの選択に使うだけで、 +兄弟クラスからの直接アクセスをまだ拒否しません。このアクセス検査は未実装です。 + +標準ライブラリ `sys` もソースから解析します。ローカルの `sys` が +なければ `src/main/smp` を使います。ネイティブの `"C"` 宣言は従来通り +ネイティブリンカーで解決します。 + +## 初期化の順序と循環 + +クラスのフィールドの読み書き、メソッド呼び出し、インスタンス生成は +初期化を必要とします。パーサーは `CLInit` を挿入し、最適化中は制御と +メモリ効果を持つ不透明なノードとして残します。実行後は隠れた `$init` +フラグが真なので、通常のメモリ最適化で重複した検査を除去できます。 + +型やクラスへの参照の循環は合法です。しかし A の初期化が B を使い、 +B の初期化が A を使う場合、先に完了できるクラスがありません。 +`ClassInitDependencies` は最適化後の呼び出しと `CLInit` をたどり、 +複数のクラス初期化を含む循環をソース位置付きで報告します。 +通常の再帰や同じ初期化関数内の再帰は許されます。 + +検査後、`Lower` はフラグ検査と未初期化時の呼び出しに展開します。 +親を先に初期化し、フラグは初期化関数の入口で真にします。同じクラスのヘルパーから +再び使えるようにするためです。他クラスを経由して未完成のクラスへ戻る循環は拒否します。 +初期化関数 A が B の初期化を完了させてから再開することは可能です。 + +## コード生成とビルド + +この章では必要なソースをすべて同時にコンパイルします。`FinishCalls` +が解析用の呼び出し辺を切り離し、`StaticData` がクラスの共有領域と +定数フィールドを配置します。ELF は発見したソースのうち生きているコードを含みますが、 +理想 IR のシリアライズ情報は含みません。 + +リポジトリのルートで次を実行します。 + +```sh +make tests CHAPTERS=chapter28a +``` + +このディレクトリでは初回に `make lib`、続いて `make tests` を使えます。 +`make view` は共有グラフビューアーを起動します。 + +次の章ではライブラリのソースがなくてもコンパイルできるように、型と +理想 IR の保存、インポート時の識別子変換、インポート先を含む初期化検査 +を追加します。ライブラリ構築と最適化の調整は付録として扱います。 diff --git a/docs/chapter28a/README.md b/docs/chapter28a/README.md new file mode 100644 index 00000000..07f34b3a --- /dev/null +++ b/docs/chapter28a/README.md @@ -0,0 +1,274 @@ +# Chapter 28a: Modules and Lazy Class Initialization + +English | [日本語](README.ja.md) + +[Previous: Chapter 27](../chapter27/README.md) | +[Next: Chapter 28b](../chapter28b/README.md) + +[Next: Separate Compilation](../chapter28b/README.md) + + +The compiler can now discover several source files and compile them together. +Each file defines a class with one shared class object; files can nest in +directories forming a module structure. + +The shared class object is a normal object with fields holding function pointers +or other state. The top-level code in the file forms the `<clinit>` +function, which runs once only on the first active use of the class - i.e., +lazy class initialization. An initializer can trigger another class's +initialization and wait for it to finish. It cannot observe that other class +half-initialized. Cycles between class initializers are not allowed. + +This chapter adds file and class semantics to the escape analysis we already +have. All required Simple source is available in one compilation. + + +## Example: Lazy class initialization, or Pay-As-You-Go + +Suppose an application sometimes needs a `Table` but not often; many runs may +never need it. Putting its construction in the application entry would make +every run pay (the eager-init model). Putting it in a getter would need a test +and cached storage. A class initializer provides `Table` lazily, on demand, +and once for all users of the class. This is similar to the Java class init +model except nothing ever sees a class mid-initialization, except the class +itself. + +The runnable example is in [`src/test/smp/lazy`](../../src/test/smp/lazy). +Its directory tree also defines the namespace: + +```text +App.smp +App/Main.smp +App/Stats.smp +App/Tables.smp +``` + +`App.smp` names its children: + +```simple +val Main=Main; +val Tables=Tables; +val Stats=Stats; +``` + +These are class references being stored into normal fields, to give easy access +to nested classes. Merely storing the reference does not trigger the class +initializer. + +`App/Stats.smp` supplies a visible count: + +```simple +int builds=0; // Static global counter +``` + +`App/Tables.smp` builds the table and exposes a function that reads it; the +array is marked private and its public function is the intended interface +(see the current access-check limitation below): + +```simple +Stats.builds++; // Number of table builds... only ever zero or 1! + +// Build a private table of squares +int[] _squares = new int[256]; +for(int i=0; i<256; i++) _squares[i]=i*i; + +// Public accessor to get a square +val square = { u8 n -> _squares[n];}; +``` + +`App/Main.smp` is the main entry: + +```simple +return arg ? Tables.square(3)+Tables.square(4)+100*Stats.builds : Stats.builds; +``` + +With `arg == 0`, the result is 0 and `Tables` never initializes. With `arg == 1`, +the result is 125: the two lookups produce 9 and 16, and the table is built once. +From this chapter directory, the test driver can evaluate it: + +```sh +java -ea -cp "build/classes/main:build/classes/test:lib/*" \ + com.seaofnodes.simple.Simple --root src/test/smp/lazy --eval App/Main.smp 1 +``` + +On Windows, use `;` instead of `:` between classpath entries. + +The evaluator can also run the example directly after type checking: + +```java +CodeGen code=new CodeGen("src/test/smp/lazy", "build/objs", "App/Main", + null, 123, true, TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); +Eval2.eval(code,0); // "0" +Eval2.eval(code,1); // "125"; a fresh evaluation starts a fresh program +``` + +`BoundaryTest.testLazyTables` checks these results. `ClassInitDependenciesTest` +also runs native code that calls a class repeatedly in one process, checking +both the cold and already-initialized paths. + +### Lazy initialization details + +The implementation tracks a hidden `$init` variable in every class. It changes +from `false` to `true` at initializer entry, allowing the class's own helpers +to use it while initialization is in progress. The separate cycle check prevents +another class's initializer from reaching back into that unfinished class. +A check before each active use calls the initializer if needed. The optimizer +tracks the flag as a normal alias, distinguishes `false`, `true`, and `bool`, +and can sometimes remove the test and call. + + +## Name lookup + +If a name misses in the file scope, the parser looks for a matching Simple +source file under the module root. It searches the current class's children, +then enclosing scopes. For example, in `App.smp`, the declaration +`val Main=Main;` defines a normal field, but the right-hand `Main` is unresolved. +The compiler finds `App/Main.smp`, which defines the referenced class. + +`ParseAll` starts with the requested file and follows unresolved class names +and types to source files. From `App/Main`, `Tables` can name the sibling +`App/Tables`; a longer name walks down the class tree. Long dotted paths are +also allowed: `App.Tables`. The parser also reads parents, since their +initialization must precede their children. Cyclic source references are +allowed: the compiler closes recursive types after discovery. + +`CompUnit` ties a source file, class type, and function graph together. They +form a tree that mirrors the source directories and tracks which files need +loading and parsing. Each `CompUnit` has a `StartCU` and `StopCU`, beneath +the whole program's `Start` and `Stop`. + +### Private + +A leading underscore marks a field or class as private. The intended rule is +that private fields are visible only inside their declaring class, not to +siblings or parents. A private enclosing class also keeps its children out of +the public interface. + +The current compiler uses these names to choose the public interface and seed +escape analysis, but does not yet enforce the source access rule. For example, +a sibling can currently read `A._secret`. Rejecting that access is still needed; +the underscore alone is not an enforced boundary in this snapshot. + +Like in other languages, private fields allow you to hide implementation +details. They can be exposed via the normal getter/setter route or any other +way that makes sense. Escape analysis can track private fields more precisely +when all uses stay in known code, helping the usual optimizations. + +### Other names + +The bundled `sys` library uses the same source mechanism. It is a normal module +in all ways except that lookup falls back to the bundled `src/main/smp` when +no local `sys` is found. Native `"C"` declarations still name functions +or data resolved by the native linker. + + +## Active use and initialization order + +Reading or writing a class field, calling its method, or constructing an +instance needs the class initialized; a class reference alone does not. The +parser inserts `CLInitNode` at these active uses, and uses the usual collection +of peepholes to optimize these away. E.g. a forward reference *might* be a +class reference, so a `CLInit` is conservatively inserted. Later the fref +resolves to a normal instance... so the `CLInit` will peephole away. +`CLInit` has peeps for removing repeated checks; for telling that the check +must always be `true` or `false`, etc. + + +Parents initialize before children. In the example, `App` initializes before +`App.Main` and `App.Tables`. Its passive references to the children do not run +them while `App` is still being initialized. + +The compiler keeps `CLInit` opaque through optimization. It carries control +and whole-memory effects, plus the fact that this class's hidden `$init` flag +is `true` afterward. At the `Lower` pass, the `CLInit` explodes into a true +CFG diamond guarding the `<clinit>` call. At this point we end up with a real +runtime guard check, i.e. real overhead. Removing redundant checks reduces +that cost; the first active use may still need a runtime check. + + +## Reject cycles that would observe unfinished classes + +An initializer can use a different class after that class finishes initializing. +For example, `A` can start, initialize `B`, then resume. A cycle would instead +ask one of them to use the other before it finishes. + +Passive references do not create initialization dependencies. These two sibling +files can refer to each other: + +```simple +// A.smp +val b = B; +// B.smp +val a = A; +``` + +Reading each other's fields during initialization does create a cycle: + +```simple +// A.smp +int value = B.value; +// B.smp +int value = A.value; +``` + +Parents add another dependency: a parent must finish before its child starts. +A parent initializer therefore cannot actively use its child. It can keep a +passive reference, or define a helper that uses the child when called after +the parent finishes. Calling that helper during parent initialization would +still create a cycle. + +The resulting dependency graph is a DAG, but initialization remains lazy. +Execution respects the dependencies of each active use; classes that are never +actively used need not initialize. + + +After optimization and during `TypeCheck`, `ClassInitDependencies` follows +surviving `CLInit` markers and calls. It checks strongly connected components +and reports cycles involving more than one class initializer. Ordinary +recursion and recursive helpers within a single initializer are allowed. Since +this is after SCCP, eliminated code cannot make a cycle. Surviving calls are +checked conservatively, even if a more powerful analysis could prove them dead. + +After checking, `Lower` expands each remaining marker into a flag test and a +call on the cold path. Parent initialization comes first. The initializer itself +sets the flag. Generated calls have conservative memory effects and do not +reopen SCCP's analysis of the optimized bodies. + + +## Emitting code and data + +After cleanup by `FinishCalls`, the live code and data from the discovered +sources are emitted in one ELF file. Final constant fields are emitted as +static data in the ELF using the helper `StaticData`; commonly this includes +static class fields holding function pointers, but any constant data can be +stored here. These fields then do not need any `<clinit>` code. + +Mutable field initialization remains in `<clinit>`, even if the initial value +is a constant. A final binding whose value is computed at runtime also needs +initializer code. For example, in `Theme.smp`: + +```simple +{int} read_setting = "C"; // Supplied by the native application +val scale = read_setting(); // Final binding; value only known at runtime +``` + + +## Build and review + +From the repository root: + +```sh +make tests CHAPTERS=chapter28a +``` + +Or, in this directory, `make lib` once for JUnit, then `make tests`. The suite +covers source discovery, initialization cycles and parent order, native cold/hot +calls, shared class storage, constructor reads and loops, private callbacks, +and x86/ARM/RISC-V code generation. `make view` starts the shared graph viewer. + +The next chapter will keep this language behavior while allowing a client to +compile without the library's sources. That requires saving types and ideal IR, +remapping identities on import, and extending the same initialization checks +across imported bodies. Library construction and optimizer tuning remain +appendix topics in the [approved split](../chapter-split.md). diff --git a/simple.bat b/simple.bat deleted file mode 100644 index 05732113..00000000 --- a/simple.bat +++ /dev/null @@ -1,2 +0,0 @@ -echo off -java -cp "build/classes/test;build/classes/main;lib/hamcrest-core-1.3.jar;lib/junit-4.12.jar" com.seaofnodes.simple.Simple %* diff --git a/simple.sh b/simple.sh deleted file mode 100644 index d8335f44..00000000 --- a/simple.sh +++ /dev/null @@ -1,2 +0,0 @@ -#! /bin/sh -java -ea -cp "build/classes/test:build/classes/main:lib/hamcrest-core-1.3.jar:lib/junit-4.12.jar" com.seaofnodes.simple.Simple $@ diff --git a/src/main/c/simple_crt.c b/src/main/c/simple_crt.c new file mode 100644 index 00000000..8656a3cc --- /dev/null +++ b/src/main/c/simple_crt.c @@ -0,0 +1,61 @@ +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +// errno may be thread-local or a macro calling a platform accessor. +int32_t simple_errno(void) { return errno; } + +// 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. +typedef struct SimpleBytes { + uint32_t len; + unsigned char bytes[]; +} SimpleBytes; + +typedef struct SimpleArgs { + uint32_t len; + uint32_t pad; + SimpleBytes *args[]; +} SimpleArgs; + +__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; +} + +int main(int argc, char **argv) { + SimpleArgs *args = simple_args(argc, argv); + if (args == NULL) + return 1; + // The language-level hidden receiver is not part of the emitted ABI for + // free functions; source argument zero occupies native argument zero. + return (int)simple_main(args); +} diff --git a/src/main/java/com/seaofnodes/simple/IterPeeps.java b/src/main/java/com/seaofnodes/simple/IterPeeps.java index 2fe4e172..e65ef6d9 100644 --- a/src/main/java/com/seaofnodes/simple/IterPeeps.java +++ b/src/main/java/com/seaofnodes/simple/IterPeeps.java @@ -1,12 +1,14 @@ package com.seaofnodes.simple; import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Opto; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.util.Ary; import com.seaofnodes.simple.node.*; import java.util.Arrays; import java.util.BitSet; +import java.util.IdentityHashMap; import java.util.Random; /** @@ -38,7 +40,7 @@ import java.util.Random; * <li>Our strong invariant is that for all Nodes, either they are on the worklist * OR no peephole applies. This invariant is easy to check, although expensive. * Basically the normal "iterate peepholes to a fixed point" is linear, and this - * check is linear at each peephole step... so quadratic overall. Its a useful + * check is linear at each peephole step... so quadratic overall. It's a useful * assert, but one we can disable once the overall algorithm is stable - and * then turn it back on again when some new set of peepholes is misbehaving. * The code for this is turned on in `IterPeeps.iterate` as `assert @@ -48,11 +50,11 @@ import java.util.Random; public class IterPeeps { public final WorkList<Node> _work; - - private final WorkList<CallEndNode> _workInline; + final WorkList<CallEndNode> _workInline; + private int _cnt; public IterPeeps( long seed ) { - _work = new WorkList<>(seed); + _work = new WorkList<>(seed); _workInline = new WorkList<>(seed); } @@ -62,36 +64,53 @@ public class IterPeeps { public void addAll( Ary<Node> ary ) { _work.addAll(ary); } /** - * Iterate peepholes to a fixed point + * Iterate peepholes and inlining to a fixed point */ public void iterate( CodeGen code ) { + boolean didInline = false; + Ary<Node> defer = new Ary<>(Node.class); while( true ) { + // Clean up everything that does not grow the code iteratePeeps(code); + + // Pick an inline candidate, no real heuristic, first come, first served + boolean inlined = false; CallEndNode cend; - while( (cend=_workInline.pop())!=null ) - if( cend.inlineSmall() ) break; - if( cend==null ) return; + while( (cend=_workInline.pop()) != null) { + if( cend.isDead() ) continue; + byte inline = cend.maybeInline(); + if( inline == -1 ) defer.add(cend); + if( inline > 0 ) { + inlined = true; + break; + } + } + // Inlined, run peeps until clean again + if( inlined ) { didInline = true; continue; } + // No more candidates, check the defer list + if( !didInline ) break; // No more progress, so all the "maybe inline after cleanup" do not progress + // Some inlining happened, retry all the "try again after cleanup" calls + didInline = false; + code.addAll(defer); + defer.clear(); } } - // Only try growing inlines once the shrinking peepholes reach a fixed point. + // Run all the code-reduction and type-lifting peeps as possible private void iteratePeeps( CodeGen code ) { - assert progressOnList(code, _work, true); - int cnt=0; + assert !CodeGen.expensiveAssert(0) || (progressOnList(code, _work) && schedulableUses(code)); Node n; while( (n=_work.pop()) != null ) { if( n.isDead() ) continue; - if( n instanceof CallEndNode cend ) _workInline.push(cend); - cnt++; // Useful for debugging, searching which peephole broke things - var obs = CodeGen.CODE._obs; + _cnt++; // Useful for debugging, searching which peephole broke things + var obs = code._obs; // Collect peephole neighbors for graph display if( obs != null ) obs.before(n); Node x = n.peepholeOpt(); + if( n instanceof CallEndNode cend ) + _workInline.push(cend); if( x != null ) { - if( x.isDead() ) { - if( obs != null ) obs.after(n, x, true); - continue; - } + assert !x.isDead(); // Peepholes return alive answers // peepholeOpt can return brand-new nodes, needing an initial type set if( x._type==null ) x.setType(x.compute()); // Changes require neighbors onto the worklist @@ -101,22 +120,31 @@ public class IterPeeps { // Everybody gets a free "go again" in case they didn't get // made in their final form. _work.push(x); + // A self-returning peephole can have rewritten its input + // edges. The new defs gained a user and may have + // backwards, user-sensitive peepholes of their own. + for( Node z : x._inputs ) _work.push(z); // If the result is not self, revisit all inputs (because // there's a new user), and replace in the graph. if( x != n ) { - for( Node z : n. _inputs ) _work.push(z); + //for( Node z : n. _inputs ) _work.push(z); for( Node z : x._outputs ) _work.push(z); n.subsume(x); } } // If there are distant neighbors, move to worklist n.moveDepsToWorklist(); - assert progressOnList(code, _work, true); // Very expensive assert + // Very expensive assert. + assert !CodeGen.expensiveAssert(_cnt) || (progressOnList(code, _work) && schedulableUses(code)); } - if( n.isUnused() && !(n instanceof StopNode) ) + boolean unused = n.isUnused(); + if( unused ) { + assert !(n instanceof StopNode); // StopNodes can die if all code in the compunit dies n.kill(); // Just plain dead - if( obs != null ) obs.after(n, x, true); + } + if( obs != null ) obs.after(n, x!=null ? x : unused ? n : null, true); } + assert !CodeGen.expensiveAssert(0) || schedulableUses(code); } @@ -133,27 +161,112 @@ public class IterPeeps { // {@link #Node.addDep} which is a side effect in an assert. The {@link // #midAssert} is used to stop this side effect. // Pessimistic solver assert - public static boolean progressOnList(CodeGen code, WorkList<Node> list, boolean dir ) { + public static boolean progressOnList(CodeGen code, WorkList<Node> list ) { code._midAssert = true; Node changed = code._stop.walk( n -> { - Node m = n; + // Ignore most in-progress things + if( n.iskeep() ) return null; + // Types must be forwards, even if on the worklist. Type nval = n.compute(); - if( (!n.iskeep() || n._nid<=8) && // Types must be forwards, even if on worklist - ( dir - ? nval.isa(n._type) // Pesi: new value lifts over old - : n._type.isa(nval) // Opto: new value falls over old - ) ) { - if( list.on(n) ) return null; - m = n.peepholeOpt(); - if( m==null ) return null; + assert nval.isa(n._type) : "Non-monotonic peep: "+n+"#"+n._nid+" old="+n._type+" new="+nval+" inputs="+inputTypes(n)+" peep="+n; + if( list.on(n) ) + return null; // On worklist is ok! + if( n instanceof CallEndNode cend ) { + if( code._iter._workInline.on(cend) ) + return null; // On inline worklist is ok! + assert cend.maybeInline() <= 0 : "Inline fired and not on worklist, CallEndNode#"+cend._nid; } - System.err.println("BREAK HERE FOR BUG"); + // Off list: must be live, in GVN table, and no peeps. + assert n.nOuts() > 0 : "Unused live node: "+n; + assert code._gvn.get(n)==n : "Not in GVN table: "+n; + Node m = n.peepholeOpt(); + assert m==null : "Peep fired and not on worklist, "+n.getClass().getSimpleName()+"#"+n._nid+" -> "+m; return m; }); code._midAssert = false; return changed==null; } + private static String inputTypes(Node n) { + StringBuilder sb = new StringBuilder("["); + for( Node in : n._inputs ) + sb.append(in==null ? "null" : in.getClass().getSimpleName()+"#"+in._nid+"="+in+":"+in._type+":keep="+in.iskeep()).append(','); + return sb.append(']').toString(); + } + + // GCM assumes every movable node can be placed no earlier than its inputs + // and no later than the LCA of its uses. Catch graphs where a use escapes + // above an input-defined branch before the later scheduling pass walks off + // the top of the idom tree. + private static final IdentityHashMap<Node,CFGNode> earlyCache = new IdentityHashMap<>(); + private static final BitSet VISIT = new BitSet(); + public static boolean schedulableUses(CodeGen code) { + earlyCache.clear(); + Node bad = code._stop.walk( n -> { + if( n.iskeep() || n.isDead() || n.isConst() || + n instanceof CFGNode || n instanceof PhiNode || n instanceof ProjNode ) + return null; + assert VISIT.isEmpty(); + CFGNode early = CFGNode.earlyCFG(n,earlyCache,VISIT); + VISIT.clear(); + if( early == null ) return null; + CFGNode lca = null; + for( Node use : n._outputs ) { + CFGNode ublk = useBlock(n,use); + if( ublk != null ) + lca = ublk.domLCA(lca,null); + } + if( lca == null ) return null; + if( !early.sameFun(lca) ) return null; + assert early.dominates(lca,null) : badSchedule(n,early,lca); + return null; + }); + return bad == null; + } + + private static CFGNode useBlock(Node n, Node use) { + if( use == null ) + return null; + if( use instanceof PhiNode phi ) { + CFGNode found = null; + for( int i=1; i<phi.nIns(); i++ ) + if( phi.in(i)==n ) { + if( i >= phi.region().nIns() ) + return null; + found = phi.region().cfg(i).domLCA(found,null); + } + return found; + } + return CFGNode.safeCFG(use); + } + + private static String badSchedule(Node n, CFGNode early, CFGNode lca) { + StringBuilder sb = new StringBuilder("Unschedulable data node ") + .append(n.getClass().getSimpleName()).append('#').append(n._nid) + .append(" early=").append(cfg(early)) + .append(" use-lca=").append(cfg(lca)) + .append(" node=").append(n).append("\ninputs:"); + for( int i=0; i<n.nIns(); i++ ) { + Node in = n.in(i); + sb.append("\n in").append(i).append(": ").append(node(in)) + .append(" cfg=").append(cfg(CFGNode.safeCFG(in))); + } + sb.append("\noutputs:"); + for( Node use : n._outputs ) + if( use != null ) + sb.append("\n use ").append(node(use)) + .append(" block=").append(cfg(useBlock(n,use))); + return sb.toString(); + } + + private static String node(Node n) { + return n==null ? "null" : n.getClass().getSimpleName()+"#"+n._nid+" "+n; + } + + private static String cfg(CFGNode cfg) { + return cfg==null ? "null" : cfg.getClass().getSimpleName()+"#"+cfg._nid+" "+cfg; + } + /** * Classic WorkList, with a fast add/remove, dup removal, random pull. * The Node's nid is used to check membership in the worklist. @@ -201,12 +314,16 @@ public class IterPeeps { for( E n : ary ) push(n); } + public void addAll( E[] es ) { + for( E n : es ) + push(n); + } + /** * True if Node is on the WorkList */ public boolean on( E x ) { return _on.get(x._nid); } - public boolean isEmpty() { return _len==0; } /** * Removes a random Node from the WorkList; null if WorkList is empty @@ -220,11 +337,13 @@ public class IterPeeps { return x; } - public void clear() { - _len = 0; - _on.clear(); - _R.setSeed(_seed); - _totalWork = 0; - } + public boolean isEmpty() { return _len==0; } + //Node[] asAry() { return Arrays.copyOf(_es,_len); } + //public void clear() { + // _len = 0; + // _on.clear(); + // _R.setSeed(_seed); + // _totalWork = 0; + //} } } diff --git a/src/main/java/com/seaofnodes/simple/Parser.java b/src/main/java/com/seaofnodes/simple/Parser.java index 145feb15..c3ad0284 100644 --- a/src/main/java/com/seaofnodes/simple/Parser.java +++ b/src/main/java/com/seaofnodes/simple/Parser.java @@ -1,11 +1,13 @@ package com.seaofnodes.simple; -import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.Ary; -import com.seaofnodes.simple.util.Utils; import com.seaofnodes.simple.node.ScopeNode.Kind; + +import static com.seaofnodes.simple.util.Utils.TODO; + import java.util.*; /** @@ -17,16 +19,32 @@ import java.util.*; */ public class Parser { - public static ConstantNode ZERO; // Very common node, cached here - public static ConstantNode NIL; // Very common node, cached here - public static XCtrlNode XCTRL; // Very common node, cached here - // Compile driver public final CodeGen _code; // The Lexer. Thin wrapper over a byte[] buffer with a cursor. private Lexer _lexer; + // Class prefix string; TODO: use something short: "%" + public static final String CLZ = "class:"; + private static final String clzPrefix = CLZ; + public static String addClzPrefix( String x ) { + assert !x.startsWith(clzPrefix); + return (clzPrefix+x).intern(); + } + public static boolean startsClzPrefix( String s ) { + return s.startsWith(clzPrefix); + } + public static boolean isPublicClassName( String name ) { + if( startsClzPrefix(name) ) name = name.substring(clzPrefix.length()); + return !name.isEmpty() && name.charAt(0)!='_' && name.indexOf("._")==-1; + } + + // Source file for compilation + public CompUnit _ref; + + // Current classname being parsed + private String _nestedType; /** * Current ScopeNode - ScopeNodes change as we parse code, but at any point of time * there is one current ScopeNode. The reason the current ScopeNode can change is to do with how @@ -78,34 +96,20 @@ public class Parser { */ public final Stack<ScopeNode> _xScopes = new Stack<>(); - ScopeNode _continueScope; - ScopeNode _breakScope; // Merge all the while-breaks here - FunNode _fun; // Current function being parsed + ScopeNode _breakScope; // Merge all the while-breaks here + ScopeNode _continueScope; // Merge all the while-continues here + ScopeNode _returnScope; // Merge all the function exits here + Ary<FRefNode> _frefs; // Forward refs created during this parse + TypeStruct _ctorOpenStruct; // Open struct being initialized by an allocation constructor block. + // Constructor exit types survive scope teardown until all fields are known. + // A null entry denotes a constructor with no returning path. + private ArrayList<IdentityHashMap<Var,Type>> _ctorExits; // Mapping from a type name to a Type. The string name matches // `type.str()` call. - public static HashMap<String, Type> TYPES = defaultTypes(); - - // Mapping from a type name to the constructor for a Type. - public final HashMap<String, StructNode> INITS; - - - public Parser(CodeGen code, TypeInteger arg) { - _code = code; - _scope = new ScopeNode(); - _continueScope = _breakScope = null; - ZERO = con(TypeInteger.ZERO).keep(); - NIL = con(Type.NIL).keep(); - XCTRL= new XCtrlNode().peephole().keep(); - TYPES = defaultTypes(); - INITS = new HashMap<>(); - } - - @Override - public String toString() { return _lexer.toString(); } - - public static HashMap<String, Type> defaultTypes() { - return new HashMap<>() {{ + public static final HashMap<String, Type> TYPES = new HashMap<>(); + public static final HashMap<String, Type> INIT_TYPES = + new HashMap<>() {{ put("bool",TypeInteger.U1 ); put("byte",TypeInteger.U8 ); put("f32" ,TypeFloat .F32); @@ -123,242 +127,442 @@ public class Parser { put("val" ,Type.TOP); // Marker type, indicates type inference put("var" ,Type.BOTTOM); // Marker type, indicates type inference }}; + + + public Parser(CodeGen code ) { + _code = code; } + @Override + public String toString() { return _lexer==null ? "" : _lexer.toString(); } + // Debugging utility to find a Node by index public Node f(int nid) { return _code.f(nid); } + // Read and set CTRL private Node ctrl() { return _scope.ctrl(); } private <N extends Node> N ctrl(N n) { return _scope.ctrl(n); } - - public void parse() { - // Graph callbacks during scope setup need a source position. - _lexer = new Lexer(sys.SYS); - + // Read and set Memory + private Node mem() { return _scope.mem(1); } + private void mem(Node mem) { _scope.mem(mem); } + // Structural label for precise memory Phis. The '$' prefix cannot be + // referenced in Simple source. + public static String memName(int alias) { return ("$"+alias).intern(); } + + // Ordinary parsing carries one whole-memory value. Alias partitioning is + // graph optimization, not parser state. + private void storeMem( Node st, int alias, Node prior ) { + mem(alias==1 ? st : mergeAlias(prior,alias,st)); + } + + // A semantic graph partition representing the complete memory after a + // precise alias update. ScopeNode treats this as one opaque value. + private Node mergeAlias(Node bulk, int alias, Node precise) { + assert bulk != null : "Missing bulk memory for alias #"+alias; + MemMergeNode merge = new MemMergeNode(null,bulk); + merge.alias(alias,precise); + return peep(merge); + } + + + public Ary<FRefNode> parse( CompUnit ref ) { + assert _scope == null && _breakScope == null && _continueScope == null && _returnScope == null; + assert _frefs == null; + _lexer = new Lexer(ref); + _frefs = new Ary<>(FRefNode.class); + // Starting Scope has control, memory, initial arguments + _scope = new ScopeNode(); _scope.define(ScopeNode.CTRL, Type.CONTROL , false, null, _lexer); _scope.define(ScopeNode.MEM0, TypeMem.BOT , false, null, _lexer); - _scope.define(ScopeNode.ARG0, TypeInteger.BOT, false, null, _lexer); - - ctrl(XCTRL); - _scope.mem(con(TypeMem.BOT)); + // Track active scopes for Graph display + _xScopes.push(_scope); + ctrl(_code.XCTRL); + mem(con(TypeMem.BOT)); - // Parse the sys import - while( !_lexer.isEOF() ) { - parseStatement(); - _lexer.skipWhiteSpace(); - } + // File-level struct declaration + _ref = ref; + _nestedType = ref._cname; + String typeName = addClzPrefix(ref._cname); - // Reset lexer for program text - _lexer = new Lexer(_code._src); - _xScopes.push(_scope); + ReturnNode clzret = parseStruct( true, typeName ); - // Parse whole program, as-if function header "{ int arg -> body }" - parseFunctionBody( _code._main,loc(),"arg"); - - // Kill an empty default main. Keep only if it was explicitly defined - // (programmer asked for a "main") or it has stuff (i.e. beginner - // default main). - FunNode main = _code.link(_code._main); - StopNode stop = _code._stop; - // Gather an explicit main - FunNode xmain = null; - for( Node n : stop._inputs ) - if( n instanceof ReturnNode ret && "main".equals(ret.fun()._name) ) - if( xmain != null ) throw Utils.TODO(); - else xmain = ret.fun(); - - if( main.ret().expr()._type==Type.TOP && main.uctrl()==null ) { - // Kill an empty default main; so it does not attempt to put a - // "main" in any final ELF file - main.setDef(1,XCTRL); // Delete default start input - stop.delDef(stop._inputs.find(main.ret())); - if( xmain != null ) - _code.setMain(xmain); + if( !_lexer.isEOF() ) throw _errorSyntax("unexpected"); - } else { - // We have a non-empty default main. - if( xmain != null ) - // Found an explicit "main" AND we have a default "main" - throw error("Cannot define both an explicit main and a default main"); - main._name = "main"; + // All still-unresolved forward refs + // should immediately resolve to a file-based name which is a Simple + // class type. + 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(_code,_ref,fref._name); + fref._con = TypeMemPtr.make((byte)2,TypeStruct.make(addClzPrefix(cu._cname),true),true); + frefs.push(fref); } - if( !_lexer.isEOF() ) throw _errorSyntax("unexpected"); + // The class being published + ref._clz = (TypeStruct)TYPES.get(typeName); // Clean up and reset _xScopes.pop(); _scope.kill(); - for( StructNode init : INITS.values() ) - init.unkeep().kill(); - INITS.clear(); - for( Ary<Node> captures : CAPTURES.values() ) - for( Node value : captures ) value.unkill(); - CAPTURES.clear(); - stop.peephole(); - // Helpers are built with init(), so their complete call graph exists - // before any inlining. Seed those nodes now along with the parsed body. - _code._start.walk(n -> { _code.add(n); return null; }); + _scope = null; + _frefs = null; + + return frefs; } + /** * Parses a function body, assuming the header is parsed. */ - private ReturnNode parseFunctionBody( TypeFunPtr sig, Lexer loc, String... ids) { + private ReturnNode parseFunctionBody( TypeFunPtr sig, Lexer loc, String[] ids) { return parseFunctionBody(sig, loc, null, ids); } - private FunNode _privateFun; - private HashMap<String,Var> _fieldVars; - private record Constructor(TypeFunPtr sig, Lexer loc, int body, String[] ids, java.util.BitSet finals) {} - private Constructor _constructor; - private final HashMap<String,TypeFunPtr> ALLOCS = new HashMap<>(); - private final HashMap<String,ReturnNode> INITIALIZERS = new HashMap<>(); - private final HashMap<String,Ary<Node>> CAPTURES = new HashMap<>(); - private Ary<Node> _captures; + private ReturnNode parseFunctionBody( TypeFunPtr sig, Lexer loc, java.util.BitSet finals, String[] ids) { + // Record & restore the existing scope vars. The function parse might + // update fields, so when the parse is done reset back to the pre-parse + // state. + Node[] preParse = _scope.save(); + + // Parse function body normally + ReturnNode ret = _parseFunctionBody(null, sig,loc,finals,ids); + + // Reset all fields to pre-parse days + _scope.restore(preParse); + return ret; + } + + + // Parse a function body + + // Stack the {break,continue,return} scopes and a new lexical scope. + // Build a FunNode and parameters, extending scope by parm names. + // Parse the function body + // - while( not end-of-function ) + // - - parseStatement + // - - If isInit, also incrementally extend the self-type. + // If isInit + // - upgrade and close the self-type + // - Gather fields and store values + // - <init> returns self, not last + // Unstack the {break,continue,return} scopes. - private ReturnNode parseFunctionBody(TypeFunPtr sig, Lexer loc, java.util.BitSet finals, String... ids) { - return parseFunctionBody(sig,loc,finals,false,ids); + private ReturnNode _parseFunctionBody( String funName, TypeFunPtr sig, Lexer loc, String[] ids) { + return _parseFunctionBody(funName, sig, loc, null, ids); } - private ReturnNode parseFunctionBody(TypeFunPtr sig, Lexer loc, java.util.BitSet finals, boolean initializer, String... ids) { - // If this is a method, record & restore the existing struct fields, - // which are also the struct initializers. The method might update - // mutable fields, so when the function parse is done reset back to - // pre-function-parse values. - Node[] preParse = new Node[_scope.nIns()]; - for( int i=0; i<_scope.nIns(); i++ ) - preParse[i] = _scope.in(i)==null ? null : _scope.in(i).keep(); + private ReturnNode _parseFunctionBody( String funName, TypeFunPtr sig, Lexer loc, java.util.BitSet finals, String[] ids) { + Constructor outerCtor=_pendingCtor; + _pendingCtor=null; + boolean isInit = FunNode.isInit(funName); + boolean isCtor = isInit || isExplicitConstructorName(funName); + String typeName = isCtor ? ((TypeMemPtr)sig._sig[0])._obj._name : null; // Stack parser state on the local Java stack, and unstack it later - FunNode oldfun = _fun; - FunNode oldPrivate = _privateFun; - HashMap<String,Var> oldFields = _fieldVars; - Constructor oldConstructor = _constructor; - _fieldVars = new HashMap<>(); - _constructor = null; - Ary<Node> oldCaptures = _captures; - _captures = initializer ? new Ary<>(Node.class) : null; - ScopeNode breakScope = _breakScope; _breakScope = null; + _scope.push(new Kind.Func(funName,isCtor && !funName.endsWith(".<clinit>"))); + ScopeNode breakScope = _breakScope; _breakScope = null; ScopeNode continueScope = _continueScope; _continueScope = null; + ScopeNode returnScope = _returnScope; _returnScope = null; - FunNode fun = _fun = (FunNode)peep(new FunNode(loc(),sig,_nestedType,null,_code._start)); + int oldUID = _code.UID(); // Used to approximate function size + FunNode fun = (FunNode)peep(new FunNode(loc(),sig, funName, _ref, null, _ref._start)); // Once the function header is available, install in linker table - // allowing recursive functions. Linker matches on declared args and // exact fidx, and ignores the return (because the fidx will only match // the exact single function). _code.link(fun); - _privateFun = sig.ret() instanceof TypeMem ? fun : null; - fun._constructor = _privateFun!=null; - - Node rpc = new ParmNode("$rpc",0,TypeRPC.BOT,fun,con(TypeRPC.BOT)).peephole(); - - // Build a multi-exit return point for all function returns - RegionNode r = new RegionNode( null,null,null).init(); - assert r.inProgress(); - PhiNode rmem = new BulkMemPhiNode(ScopeNode.MEM0,r,null).init(); - PhiNode rrez = (sig.ret() instanceof TypeMem - ? new BulkMemPhiNode("#selfMem",r,null) : new PhiNode(ScopeNode.ARG0,r,null)).init(); - ReturnNode ret = new ReturnNode(r, rmem, rrez, rpc, fun).init(); - fun.setRet(ret); - assert ret.inProgress(); - _code._stop.addDef(ret); + Node rpc = new ParmNode("$rpc",0,TypeRPC.BOT,fun,ConstantNode.seed(TypeRPC.BOT).peephole()).peephole(); // Pre-call the function from Start, with worse-case arguments. This // represents all the future, yet-to-be-parsed functions calls and // external calls. - _scope.push(new Kind.Func(_privateFun!=null)); - ctrl(fun); // Scope control from function - // Unknown callers supply whole memory; field aliases are discovered lazily. - _scope.mem(new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,new ProjNode(_code._start,1,ScopeNode.MEM0).peephole()).peephole()); + _scope.ctrl(fun); // Scope control from function + // Private mem alias tracking per function + Node defaultMem = new ProjNode(_ref._start,1,ScopeNode.MEM0).peephole(); + Node privMem = new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,defaultMem).peephole(); + mem(privMem); // All args, "as-if" called externally for( int i=0; i<ids.length; i++ ) { Type t = sig.arg(i); - _scope.define(ids[i], t, finals==null ? t instanceof TypeNil : finals.get(i), new ParmNode(ids[i],i+2,t,fun,con(t)).peephole(), loc); - } - - // Fields stay in Scope as ordinary SSA values throughout the body. - // Private memory is read at helper entry and written once at exit. - int fields = _scope.nIns(); - if( initializer ) - _scope.push(new Kind.Define((TypeMemPtr)TYPES.get(_nestedType))); - else if( _privateFun==fun ) { - TypeStruct shape=((TypeMemPtr)TYPES.get(_nestedType))._obj; - StructNode defaults=INITS.get(_nestedType); - for( int i=0; i<shape._fields.length; i++ ) { - Field f=shape._fields[i]; - Var v=null; - for( int j=preParse.length-1; j>=0; j-- ) - if( _scope.var(j)._name.equals(f._fname) ) { v=_scope.var(j); break; } - assert v!=null; - _fieldVars.put(f._fname,v); - if( !f._one ) { - Node value=missingInit(defaults.in(i),new java.util.BitSet()) ? con(Type.BOTTOM) - : new LoadNode(loc(),f._fname,f._alias,privateMem(),self(),off(shape,f._fname),true).peephole(); - _scope.update(v,value); - } + // Args take a default input if called from Start/external-world + _scope.define(ids[i], t, (i==0 && ids[0]=="self") || (finals!=null && finals.get(i)), new ParmNode(ids[i],i+2,t,fun,ConstantNode.seed(t).peephole()).peephole(), loc); + } + + if( isCtor && !isInit ) { + int lex=_scope.klast()._lexSize; + TypeStruct shape=(TypeStruct)TYPES.get(typeName); + for( Field fld : shape._fields ) { + int idx=fieldVarIdxBefore(lex,fld._fname); + if( idx<0 ) continue; + Var v=_scope.var(idx); + if( v._fref || v._extern!=0 || v._uninit || _scope.in(v)._type.isConstant() ) continue; + Node value=convertExpr( + new LoadNode(loc,fld._fname,fld._alias,ctrl(),fun.parm(3),fun.parm(2),off(shape,fld._fname)).peephole(),v.type()); + _scope.update(v,value); } } - Node last=ZERO; + + // The initializer owns its run-once flag store. Parent initialization + // is a structural requirement, executed before this body by CLInit lowering. + if( fun.isClz() ) { + Node self = fun.parm(2); + Field flag = ((TypeMemPtr)self._type)._obj.field("$init"); + Node prior = mem().keep(); + Node st = peep(new StoreNode(loc,"$init",flag._alias,flag._t,ctrl(),prior,self, + fldoff(self,"$init"),con(1),false)); + storeMem(st,flag._alias,prior); + prior.unkeep(); + } + + // Parse the function body. + Node last=_code.ZERO; // Last statement as the default result + + int nvar = 0; while (!peek('}') && !_lexer.isEOF()) { - last = parseStatement(); - if( initializer ) fields = initFields(fields); - } - if( initializer ) { - finishFields(); - CAPTURES.put(_nestedType,_captures); - if( _constructor!=null ) { - Constructor ctor=_constructor; - int end=pos(ctor.body()); - ReturnNode body=parseFunctionBody(ctor.sig(),ctor.loc(),ctor.finals(),ctor.ids()); - body.fun().setName(shortName(_nestedType)); - require("}"); - pos(end); + // Parse a statement; record last statement as default return + last = _code.add(parseStatement()); + // For <init> and <clinit>, local vars are really struct fields and + // get exposed as discovered. This allows late local functions to + // use early declared fields. + if( isInit ) + nvar = updateSelfAsFieldsDiscovered( typeName, nvar ); + } + + if( isInit ) finishConstructor(); + _pendingCtor=outerCtor; + + ParmNode self = fun.parm(2); + // Last expression is the return + if( ctrl()._type==Type.CONTROL ) { + // Constructors get special return types + if( fun.isClz() ) { + assert last!=null; + } else if( isCtor ) { + last = fun.parm(3); + } + addReturn(last); + // exit path is now dead (nothing live after a "return") + } + if( isCtor ) + upgradeSelfTypeAndStoreFields( typeName, fun.isClz(), self, fun.parm(3), isInit ); + + // Build a return from the _returnScope. + // Can be no returns for never-exit functions + Node rctl = _returnScope==null ? _code.XCTRL : _returnScope.ctrl().peephole(); + Node rmem = rctl==_code.XCTRL ? con(TypeMem.TOP) : _returnScope.mem(); + Node expr = rctl==_code.XCTRL ? con(Type.TOP) : _returnScope._inputs.last().peephole(); + ReturnNode ret = (ReturnNode)peep(new ReturnNode(rctl, rmem, expr, rpc, fun)); + fun.setRet(ret); + // Approximate function size, for inlining heuristics + fun._approxUIDs = _code.UID() - oldUID; + for( Node cend : ret.outs() ) + if( cend instanceof CallEndNode ) + throw TODO(); //_code.add(cend); // have call ends ALREADY? just barely made the Return + + // Export (and keep-alive) functions until prove dead + _ref.addFun(_code,fun); + + // Unstack parser state + if( _returnScope != null ) _returnScope.kill(); + _returnScope = returnScope; + _continueScope = continueScope; + _breakScope = breakScope; + _scope.pop(); + + return ret; + } + + private int updateSelfAsFieldsDiscovered( String typeName, int nvar ) { + int lex = _scope.klast()._lexSize + 2/*Skip self and selfMem*/; + // Load self type in and out of the TYPES hashtable. + TypeStruct tself = (TypeStruct)TYPES.get(typeName); + // Start looking for newly declared vars at the lexical scope start, + // skipping two ("self" and "selfmem") and fields seen already. + while( lex + nvar < _scope.nIns() ) { + Var v = _scope.var(lex + nvar++); + // Field with sharper type + Field old = tself.field(v._name); + Field fld = v._extern!=0 + ? Field.make(v._name,v.type(),v._extern,false,true) + : old == null + ? Field.make(v._name,v.type(),_code.alias(_ref._cname),v._final) + : old.makeFrom(v.type()); + tself = tself.addOrUpdate(fld); + } + TYPES.put(typeName,tself); + return nvar; + } + + + void upgradeSelfTypeAndStoreFields( String typeName, boolean isClz, ParmNode self, ParmNode smem, boolean upgrade ) { + int lex = _scope.klast()._lexSize; + assert _scope.var(lex)._name=="self"; + TypeStruct tself = (TypeStruct)TYPES.get(typeName); + int base = lex+2; // Skip ctrl, mem + if( upgrade ) { + // Default <init>/<clinit> defines the struct and can sharpen fields. + // A named constructor only initializes the already-closed struct. + for( int i=0; i<tself.nkids(); i++ ) { + Field tfld = tself._fields[i]; + int vidx = fieldVarIdx(base,tfld._fname); + if( vidx == -1 ) + continue; + Var v = _scope.var(vidx); + // Forward-ref fields where placed in tself but will get promoted + // to an outer scope - and should not be in tself. + if( v._fref ) + tself = tself.remove(i--); + else if( v.type() != tfld._t ) { + assert v.type().isa(tfld._t); + tself = tself.replace( tfld.makeFrom( v.type() ) ); + } + } + + // Close Struct type after parsing struct body. + TYPES.put( tself._name, tself = tself.close() ); + // Improve self, selfMem types in scope. + // Class self-type is a singleton pointer. Instance <init> receives the + // broad allocation pointer, while private memory tracks initialized fields. + self._declaredType = self._type = isClz ? TypeMemPtr.make((byte)2,tself,true) : TypeMemPtr.make(constructorRecv(tself)); + FunNode fun = self.fun(); + fun.setSig(fun.sig().makeFrom(self._type,0)); + if( !isClz ) // <init> returns upgraded private memory + smem._declaredType = smem._type = TypeMem.makePrivate(tself); + } + + if( !upgrade ) { + IdentityHashMap<Var,Type> exit = null; + if( _returnScope!=null && !_returnScope.ctrl()._type.isHigh() ) { + exit = new IdentityHashMap<>(); + int first = _scope._kinds.at(_scope.depth()-2)._lexSize+2; + for( int i=first; i<lex; i++ ) + exit.put(_scope.var(i),_returnScope.in(i)._type); } + _ctorExits.add(exit); + } else if( !isClz ) { + checkInitFields(tself,base); } - if( _privateFun==fun ) last = storeFields(initializer); - // Last expression is the return except for the top-level main - if( ctrl()._type==Type.CONTROL ) - if( fun.sig() != _code._main ) - fun.addReturn(ctrl(), _scope.mem(), last); - else { - fun.setDef(1,XCTRL); // Kill default main - _code.addAll(fun._outputs); + // When can _returnScope be null here? A never-exit constructor will + // not have any returns, and thus no need to gather values and store + // them into the (never) constructed object + if( _returnScope==null ) + return; + + // A MemMerge to gather field updates as stores. Classes update the + // normal public memory. <init> updates the private memory that got + // passed in - is treated like a normal argument and not like memory. + Node mmm = isClz ? _returnScope.mem(1) : smem; + + + // Store constructor results into fields + for( int i=0; i<tself.nkids(); i++ ) { + Field fld = tself._fields[i]; + int vidx = upgrade + ? fieldVarIdx(base,fld._fname) + : fieldVarIdxBefore(lex,fld._fname); + if( vidx == -1 ) + continue; + Var v = _scope.var(vidx); + if( v._fref || v._extern!=0 ) continue; // C storage has no Simple initializer + // NewNode's TOP field is the zero-code poison for a required + // field. Leave it alone unless the declaration body supplied a + // value; otherwise a user constructor must initialize it. + Node val = _returnScope.in(vidx); + if( upgrade && v._uninit && + !initialized(val._type) ) continue; + // A numeric merge can still be untyped while parsing. Retain its + // declared conversion at the helper boundary, as assignments do. + if( fld._t instanceof TypeInteger || fld._t instanceof TypeFloat ) + val=convertExpr(val,fld._t); + // Store value into extended struct + Node prior = mmm.keep(); + Node st = peep(new StoreNode(null, fld._fname, fld._alias, fld._t, null, prior, self, off(tself,fld._fname), val, fld._final)); + mmm = mergeAlias(prior,fld._alias,st); + prior.unkeep(); + } + mmm = mmm.peephole(); + + if( !isClz ) + // Stuff private mem into normal expression return value. + _returnScope.setDef(_returnScope.nIns()-1,mmm); + else + // Parser memory is one opaque whole-memory edge. Publish the + // completed class-field aggregate as the <clinit> return memory. + _returnScope.mem(mmm); + //_code.add(mmm); + } + + // Validate each user constructor, or an implicit empty one, before the + // declaration scope goes away. Later fields use their declaration defaults. + private static boolean initialized(Type t) { + return t!=Type.BOTTOM && !t.isHigh() && (!(t instanceof TypeNil tn) || tn.notNull()); + } + + private void checkInitFields(TypeStruct ts, int base) { + if( _returnScope==null || _returnScope.ctrl()._type.isHigh() ) return; + boolean implicit = _ctorExits.isEmpty(); + if( implicit ) _ctorExits.add(new IdentityHashMap<>()); + for( IdentityHashMap<Var,Type> exit : _ctorExits ) { + if( exit==null ) continue; + for( Field fld : ts._fields ) { + int idx = fieldVarIdx(base,fld._fname); + if( idx == -1 ) continue; + Var v = _scope.var(idx); + if( !v._uninit || v._fref ) continue; + Type t = exit.getOrDefault(v,_returnScope.in(idx)._type); + if( !initialized(t) ) + throw error("'"+ts._name+"' is not fully initialized, field '"+fld._fname+ + (implicit ? "' needs to be set in a constructor" : + "' is only partially set in the constructor"),v._loc); } + } + } - // Pop off the inProgress node on the multi-exit Region merge - assert r.inProgress(); - r ._inputs.pop(); - rmem._inputs.pop(); - rrez._inputs.pop(); - r._loc = loc(); // Final position - assert !r.inProgress(); - - // Force peeps, which have been avoided due to inProgress - ret.setDef(1,rmem.peephole()); - ret.setDef(2,rrez.peephole()); - ret.setDef(0,r.peephole()); - ret = (ReturnNode)ret.peephole(); - - // Function scope ends - if( initializer ) _scope.pop(); - _scope.pop(); - _fun = oldfun; - _privateFun = oldPrivate; - _fieldVars = oldFields; - _constructor = oldConstructor; - _captures = oldCaptures; - _breakScope = breakScope; - _continueScope = continueScope; + private int fieldVarIdx( int base, String name ) { + for( int i=base; i<_scope.nIns(); i++ ) + if( _scope.var(i)._name == name ) + return i; + return -1; + } - // Reset all fields to pre-parse days - for( int i=0; i<preParse.length; i++ ) - _scope.setDef(i,preParse[i]==null ? null : preParse[i].unkeep()); + // Named constructors update field Vars from the enclosing initializer + // scope. Constructor arguments and locals begin at limit and must not + // shadow the field state gathered into private memory. + private int fieldVarIdxBefore( int limit, String name ) { + for( int i=limit-1; i>=0; i-- ) + if( _scope.var(i)._name == name ) + return i; + return -1; + } - return ret; + private boolean isConstructorDecl(String name) { + TypeStruct self = _scope.constructorSelf(); + return self != null && name == initFieldName(self._name); + } + + private boolean inExplicitConstructor() { + Kind.Func func = (Kind.Func)_scope._kinds.at(_scope.enclosingFuncOrDecl()); + return isExplicitConstructorName(func._name); } + private boolean isExplicitConstructorName(String funName) { + TypeStruct self = _scope.constructorSelf(); + return self != null && funName == (self._name+"."+initFieldName(self._name)).intern(); + } + + private static TypeStruct constructorRecv(TypeStruct self) { + Field[] fs = new Field[self._fields.length]; + for( int i=0; i<fs.length; i++ ) { + Field fld = self._fields[i]; + fs[i] = fld.makeFrom(fld._t.makeStorage()); + } + return TypeStruct.make(self._name,self._open,fs); + } + + /** * Parses a block * @@ -371,9 +575,11 @@ public class Parser { private Node parseBlock(Kind kind) { // Enter a new scope _scope.push(kind); - Node last = ZERO; - while (!peek('}') && !_lexer.isEOF()) + Node last = _code.ZERO; + while (!peek('}') && !_lexer.isEOF()) { + _code.add(last); last = parseStatement(); + } // Exit scope last.keep(); _scope.pop(); @@ -396,8 +602,8 @@ public class Parser { else if (matchx("break") ) return parseBreak(); else if (matchx("continue")) return parseContinue(); else if (matchx("struct") ) return parseStruct(); - else if (_scope.inConstructor() && matchx("new")) return parseConstructorDeclaration(); - else if (matchx(";") ) return null; // Empty statement + else if (inStructDeclaration() && matchx("new")) return parseConstructorDeclaration(); + else if (matchx(";") ) return _code.ZERO; // Empty statement // Break ambiguity around leading function types and starting a block else if (peek('{') && !isTypeFun() ) { match("{"); @@ -407,22 +613,69 @@ public class Parser { else return parseDeclarationStatement(); } - // In a struct declaration, new names the constructor. Its body still - // parses as a function; in expressions, new allocates and calls it. + private boolean inStructDeclaration() { + return _scope.klast() instanceof Kind.Func fun && FunNode.isInstance(fun._name); + } + + // Constructor declaration: new TypeName = { args -> body }; + private record Constructor(String name, Lexer body) {} + private Constructor _pendingCtor; + private Node parseConstructorDeclaration() { - Lexer loc=loc(); - String name=requireId(); - String expect=shortName(_nestedType); - if( !name.equals(expect) ) - throw error("Constructor name '"+name+"' must match type name '"+expect+"'"); + TypeStruct self = _scope.constructorSelf(); + assert self != null; + String name = requireId(); + String expect = initFieldName(self._name); + if( name != expect ) + throw error("Constructor name '" + name + "' must match type name '" + expect + "'"); require("="); - Node ctor=constructorFunc(); - TypeFunPtr sig=(TypeFunPtr)ctor._type; - Node lift=liftExpr(ctor,sig,true); - if( !_scope.define(name,sig,true,lift,loc) ) - throw error("Redefining constructor '"+name+"'"); - _scope.lookup(name)._init=true; - return require(lift,";"); + if( _pendingCtor!=null ) throw error("Redefining constructor '"+name+"'"); + _pendingCtor=new Constructor(name,loc()); + // Parse the body after all field declarations are known, so self.x + // and x share a local slot even when x is declared below this body. + require("{"); + int depth=1; + while( depth>0 ) { + _lexer.skipWhiteSpace(); + if( _lexer.isEOF() ) throw error("Unclosed constructor body"); + char ch=_lexer.nextChar(); + if( ch=='{' ) depth++; + else if( ch=='}' ) depth--; + else if( ch=='"' || ch=='\'' ) { + char quote=ch; + while( !_lexer.isEOF() ) { + ch=_lexer.nextChar(); + if( ch=='\\' ) _lexer.nextChar(); + else if( ch==quote ) break; + } + } + } + require(";"); + return _code.ZERO; + } + + private void finishConstructor() { + if( _pendingCtor==null ) return; + Constructor ctor=_pendingCtor; + _pendingCtor=null; + Lexer after=_lexer; + _lexer=new Lexer(ctor.body()); + defineConstructor(ctor.name()); + _lexer=after; + } + + private void defineConstructor(String name) { + TypeStruct self=_scope.constructorSelf(); + Node ctor = constructorFunc(name); + TypeFunPtr sig = (TypeFunPtr)ctor._type; + String clzName = addClzPrefix(self._name); + TypeStruct clz = (TypeStruct)TYPES.get(clzName); + Field old = clz==null ? null : clz.field("<ctor>"); + if( old != null ) + throw error("Redefining constructor '" + name + "'"); + Field fld = Field.make("<ctor>",sig,_code.alias(_ref._cname),true); + TYPES.put(clzName,clz==null ? TypeStruct.make(clzName,true,fld) : clz.add(fld)); + require(ctor,";"); } /** @@ -475,7 +728,7 @@ public class Parser { // used as an indicator to switch off peepholes of the region and // associated phis; see {@code inProgress()}. - ctrl(new LoopNode(loc(),ctrl()).peephole()); // Note we set back edge to null here + ctrl(new LoopNode(loc(),null,ctrl()).peephole()); // Note we set back edge to null here // At loop head, we clone the current Scope (this includes all // names in every nesting level within the Scope). @@ -494,8 +747,8 @@ public class Parser { // IfNode takes current control and predicate Node ifNode = new IfNode(ctrl(), pred.keep()).peephole(); // Setup projection nodes - Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); - Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole(); + CFGNode ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + CFGNode ifF = (CFGNode)new CProjNode(ifNode.unkeep(), 1, "False").peephole(); // for( ;;next ) body int nextPos = -1, nextEnd = -1; @@ -548,6 +801,8 @@ public class Parser { // it sets the second input to the corresponding input from the back // edge. If the phi is redundant, it is replaced by its sole input. var exit = _breakScope; + head.balanceLoopFRefs(_scope); + exit.balanceLoopFRefs(_scope); head.endLoop(_scope, exit); head.unkeep().kill(); @@ -565,12 +820,12 @@ public class Parser { _xScopes.push( exit ); } _scope = exit; - return ZERO; + return _code.ZERO; } private ScopeNode jumpTo(ScopeNode toScope) { ScopeNode cur = _scope.dup(); - ctrl(XCTRL); // Kill current scope + ctrl(_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 while( cur.depth() > _breakScope.depth() ) @@ -588,7 +843,7 @@ public class Parser { private void checkLoopActive() { if (_breakScope == null) throw error("No active loop for a break or continue"); } - private Node parseContinue() { checkLoopActive(); _continueScope = require(jumpTo( _continueScope ),";"); return ZERO; } + private Node parseContinue() { checkLoopActive(); _continueScope = require(jumpTo( _continueScope ),";"); return _code.ZERO; } private Node parseBreak () { checkLoopActive(); // At the time of the break, and loop-exit conditions are only valid if @@ -596,8 +851,8 @@ public class Parser { // conditions here, not the union. _breakScope.removeGuards(_breakScope.ctrl()); _breakScope = require(jumpTo(_breakScope ),";"); - _breakScope.addGuards(_breakScope.ctrl(), null, false); - return ZERO; + _breakScope.addGuards((CFGNode)_breakScope.ctrl(), null, false); + return _code.ZERO; } // Look for an unbalanced `)`, skipping balanced @@ -607,7 +862,7 @@ public class Parser { // Next X char handles skipping complex comments switch( _lexer.nextXChar() ) { case Character.MAX_VALUE: - throw Utils.TODO(); + throw TODO(); case ')': if( --paren<0 ) { posT( pos() - 1 ); @@ -641,8 +896,8 @@ public class Parser { pred.keep(); Node ifNode = new IfNode(ctrl(), pred).peephole(); // Setup projection nodes - Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); - Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole().keep(); + CFGNode ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); + CFGNode ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole().keep(); // In if true branch, the ifT proj node becomes the ctrl // But first clone the scope and set it as current ScopeNode fScope = _scope.dup(); // Duplicate current scope @@ -658,7 +913,7 @@ public class Parser { case ":" -> parseAsgn(); // pred ? asgn; case "&&" -> parseLogical(); // pred && expr case "||" -> pred; // pred || expr_is_ignored - default -> throw Utils.TODO(); + default -> throw TODO(); }; lhs.keep(); _scope.removeGuards(ifT); @@ -681,12 +936,14 @@ public class Parser { Node rhs = switch( fside ) { case "else" -> (doRHS=match(fside)) ? parseStatement() : con(lhs._type.makeZero()); case ":" -> (doRHS=match(fside)) ? parseAsgn() : con(lhs._type.makeZero()); - case "&&" -> rhs = pred; - case "||" -> rhs = parseLogical(); - default -> throw Utils.TODO(); + case "&&" -> con(lhs._type.makeZero()); // pred && expr_is_ignored + case "||" -> parseLogical(); // pred || expr + default -> throw TODO(); }; rhs.keep(); _scope.removeGuards(ifF); + // The RHS of `||` is parsed on the false side, so retain its updated + // control, memory and locals just as an explicit else arm does. if( doRHS || fside.equals("||") ) fScope = _scope; pred.unkeep(); @@ -709,9 +966,10 @@ public class Parser { Node ret = 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 err; + ret.keep(); r.peephole(); - return ret; + return ret.unkeep(); } /** @@ -725,22 +983,69 @@ public class Parser { */ private Node parseReturn() { var expr = require(parseAsgn(), ";"); - // Force the lazy whole-memory Phi before closing this exit. - if( _privateFun==_fun ) - throw error("A constructor cannot return a value"); - _fun.addReturn(ctrl(), _scope.mem(), expr); - ctrl(XCTRL); // Kill control - return expr; + return addReturn(expr); } + private Node addReturn(Node expr) { + expr.keep(); + // Need default memory, since it can be lazy, need to force + // a non-lazy Phi + mem(); // Force a pending lazy bulk-memory Phi + // For <init> and <clinit> - all fields in scope will be live-on-exit + // and stored into `self` and need to have lazy-phi's inserted. + int lexN = _scope.enclosingFuncOrDecl(); + Kind.Func k = (Kind.Func)_scope._kinds.at(lexN); + if( FunNode.isInit(k._name) ) { + int nestedLexSize = lexN+1 < _scope.depth() ? _scope._kinds.at(lexN+1)._lexSize : _scope.nIns(); + for( int i=k._lexSize; i<nestedLexSize; i++ ) + _scope.update(_scope.var(i),null); + if( _returnScope != null ) { + Var vexpr = _returnScope._vars.pop(); + Node oldX = _returnScope.removeLast(); + int rlen = _returnScope.nIns(); + while( rlen < nestedLexSize ) { + _returnScope._vars.add(_scope.var(rlen)); + _returnScope.addDef(con(_scope.var(rlen++).type().makeZero())); + } + _returnScope._vars.add(vexpr); + _returnScope.addDef(oldX); + } + } else if( isExplicitConstructorName(k._name) ) { + // Field bindings live in the enclosing declaration, not among the + // constructor's arguments/locals. Force lazy Phis on every exit. + int first = _scope._kinds.at(lexN-1)._lexSize+2; + for( int i=first; i<k._lexSize; i++ ) + _scope.update(_scope.var(i),null); + } + // No prior merge point? Just clone and hang on to it + if( _returnScope == null ) { + _returnScope = _scope.dup(); + while( lexN+1 < _returnScope.depth() ) + _returnScope._pop(); // Pop a nested block scope until we hit the function scope + _returnScope.define("$expr", inferredReturnType(expr._type), true, expr, null); + } else { + // For <init> and <clinit> - ALL FIELDS IN LAST SCOPE are live + // and need to have lazy-phi's inserted. + + // And fields might not match.... will need matching Var/define with default values + int rlen = _returnScope.nIns()-1; + RegionNode r = ctrl(new RegionNode(null, null,_returnScope.ctrl(), _scope.ctrl()).init().keep()); + _returnScope._merge(_scope, r, rlen); + Node oldExpr = _returnScope.in(rlen); + _returnScope.setDef(rlen,new PhiNode("$expr", r, oldExpr, expr).peephole()); + _code.add(r); + _returnScope.ctrl(r.unkeep()); + } + ctrl(_code.XCTRL); // Kill control + return expr.unkeep(); + } /** Parse: [name '='] expr */ private Node parseAsgn() { int old = pos(); String name = _lexer.matchId(); - int old_pre_equals = pos(); // Just a plain expression, no assignment. // Distinguish `var==expr` from `var=expr` if( name==null || KEYWORDS.contains(name) || !matchOpx('=','=') ) @@ -749,16 +1054,12 @@ public class Parser { // Find variable to update Var def = _scope.lookup(name); if( def==null ) + def = defineOpenConstructorField(name); + if( def==null ) throw error("Undefined name '" + name + "'"); - // If a method var, allow nested blocks and methods, but not fcns or other structs. - // Else safety check for accessing fields out of function scope. - if( instance(def) ) { - pos(old_pre_equals); // setup for `self.name = expr` - Var selfVar = _scope.lookup("self"); - Node self = liftExpr(_scope.in(selfVar),selfVar.type(),true); - return parsePostfixName(self,name); - } + if( def._extern!=0 ) // Parse the C binding as a memory lvalue + { pos(old); return parseExpression(); } // TOP fields are for late-initialized fields; these have never // been written to, and this must be the final write. Other writes @@ -768,34 +1069,100 @@ public class Parser { // The alloc() call added the allocation scope !_scope.canInit(def) ) throw error("Cannot reassign final '"+name+"'"); - checkSharedField(def); + checkSharedFunction(def); // Parse assignment expression - checkCaptureWrite(def); Node expr = parseAsgn(); // Lift expression, based on type - Node lift = liftExpr(expr.keep(), def.type(), true); + Type decl = def.type(); // Declared type + Node lift = convertExpr(expr.keep(), decl).keep(); // Update _scope.update(name,lift); + + // Check for storing into an instance field instead of a local var + int kx = _scope.kindx(def); // Declaration scope + int fx = _scope.enclosingFunction(); // Enclosing function scope + Kind kk = _scope._kinds.at(kx); + Kind fk = _scope._kinds.at(fx); + // Access instance field from 'self' + if( kx+1 == fx && kk instanceof Kind.Func inst && FunNode.isInstance(inst._name) && + fk instanceof Kind.Func method && !inExplicitConstructor() ) { + // Named field offset on an unknown struct type + TypeMemPtr self = (TypeMemPtr)_scope.var(fk._lexSize).type(); + TypeStruct sobj = (TypeStruct)TYPES.get(self._obj._name); + Field fld = sobj.field(name); + int alias = fld._alias; + Node ptr = _scope.in(method._lexSize); + Node off = fldoff(ptr,name); + Node prior = mem().keep(); + Node st = new StoreNode(loc(), name, alias, decl, ctrl(), prior, ptr, off, lift, inExplicitConstructor()).peephole().keep(); + storeMem(st,alias,prior); + st.unkeep(); + prior.unkeep(); + } + lift.unkill(); + + // Return un-lifted expr return expr.unkeep(); } - // Apply declared access permissions; widen floats and narrow integers. + // Widen ints to floats; narrow wide int types. // Early error if types do not match variable. - private Node liftExpr(Node expr, Type t, boolean isLoad) { - if( t==Type.BOTTOM || t==Type.TOP ) return expr; - // Constructor sentinels record missing initialization, not a value - // needing conversion. Keep them visible to the completion check. - if( expr instanceof ConstantNode && !(expr instanceof FRefNode) && - (expr._type==Type.TOP || expr._type==Type.BOTTOM) ) return expr; + private Node convertExpr( Node expr, Type t ) { + if( !expr._type.accessISA(t) ) + throw error("Type "+expr._type.str()+" is not of declared type "+t.str()); if( t instanceof TypeMemPtr tmp && tmp.isFinal() ) expr = peep(new ReadOnlyNode(expr)); - if( expr instanceof FunPtrNode && t instanceof TypeFunPtr ) return expr; + // A FunPtr carries the semantic edge to its function's Return. Do not + // hide that edge behind a Convert which can fold to a plain constant. + if( expr instanceof FunPtrNode && t instanceof TypeFunPtr ) + return expr; + // Auto-widen array to i64 (cast ptr to raw int bits) + if( t == TypeInteger.BOT && expr._type instanceof TypeMemPtr tmp && tmp._obj.isAry() ) + return peep(new AddNode(peep(new PtrToIntNode(expr)),off(tmp._obj,"[]"))); + // Widen ints to floats, narrow wide types return peep(new ConvertNode(t,expr)); } + // Inferred `var` declarations describe writable slots, not the exact + // value used to initialize them. Explicitly typed locals retain the + // user's nullability and numeric-width guarantees; `val` retains the + // initializer's precise type. + private static Type mutableInferredType(Type t) { + return switch( t ) { + case TypeInteger ti -> TypeInteger.BOT; + case TypeFloat tf -> TypeFloat.F64; + case TypeMemPtr tmp -> tmp.makeVar(); + case TypeFunPtr tfp -> tfp; + case TypeConAry<?> a -> a; + case TypePtr ptr -> TypePtr.PTR; + case TypeScalar ts -> TypeScalar.BOT; + default -> t; + }; + } + + private Type constructorFieldType(Type t) { + if( t instanceof TypeMemPtr ptr && ptr._obj.isConAry() ) { + TypeStruct declared=(TypeStruct)TYPES.get(ptr._obj._name); + TypeStruct shape=typeAry(declared._fields[1]._t,ptr._obj._fields[1]._final); + return TypeMemPtr.make((byte)2,shape,false).withAccess(ptr._ro); + } + return mutableInferredType(t); + } + + // Seed an inferred return merge with the declared numeric family instead + // of a transient constant/range. Pointer nullability remains part of the + // inferred function result and is combined by the later Phi. + private static Type inferredReturnType(Type t) { + return switch( t ) { + case TypeInteger ti -> TypeInteger.BOT; + case TypeFloat tf -> TypeFloat.F64; + default -> t; + }; + } + private Node widenInt( Node expr, Type t ) { return (expr._type instanceof TypeInteger || expr._type==Type.NIL) && t instanceof TypeFloat ? peep(new ToFloatNode(expr)) : expr; @@ -822,23 +1189,6 @@ public class Parser { return require(n,";"); } - // Widen a reassignable binding without discarding the initializer's - // permissions. Lattice glb includes readonly memory and is too broad here. - private Type inferredType(Type t) { - if( t instanceof TypeMemPtr tmp ) { - if( tmp._obj.isAry() ) { - Type elem = tmp._obj._fields[1]._t; - if( elem instanceof TypeConAry ) - elem = ((TypeMemPtr)TYPES.get(tmp._obj._name))._obj._fields[1]._t; - return typeAry(elem,tmp._obj._fields[1]._final).withAccess(tmp._ro).makeNullable(); - } - Type base = TYPES.get(tmp._obj._name); - TypeMemPtr ptr = (TypeMemPtr)base; - return ptr.withAccess(tmp._ro).makeNullable(); - } - return t.glb(false); - } - // The binding modifier sits immediately before the name. It never // changes permissions on the referenced object. private boolean bindingFinal(Type t) { @@ -857,35 +1207,50 @@ public class Parser { Lexer loc = loc(); String name = requireId(); // Optional initializing expression follows - boolean fld_final = false; // Fixed field independent of pointee access - boolean initialized = match("="); + boolean fld_final = false; // Field is final, but not deeply final + boolean uninit = false; // Required non-null constructor field Node expr; - if( initialized ) { - expr = isExternDecl() ? externDecl(name,t) - : _scope.inConstructor() && name.equals(shortName(_nestedType)) - ? constructorFunc() : parseAsgn(); + if( match("=") ) { + if( isExternDecl() ) { + if( !(t instanceof TypeFunPtr) ) { + Node ptr = externData(name,t); + // The lexical binding is a fixed address; its contents are mutable. + if( !_scope.define(name,t,true,ptr,loc) ) + throw error("Redefining name '"+name+"'",loc); + _scope.lookup(name)._extern = _code.externAlias(name); + return _code.ZERO; + } + expr = externDecl(name,t); + t = expr._type; // Upgrade declared type to the exact extern decl type + } else if( isConstructorDecl(name) ) { + expr = constructorFunc(name); + } else { + expr = parseAsgn(); + } // var/val, then type comes from expression if( inferType ) { if( expr._type==Type.NIL ) throw error("a not-null/non-zero expression"); t = expr._type; - if( !xfinal ) t = inferredType(t); // Widen if not final - // A final field is still a local variable during construction. - // Its constant default is a value, not a one-value declaration - // type: later assignments and loop Phis may change it. - else if( _scope.inConstructor() && !(t instanceof TypeFunPtr) ) { - Type wide=inferredType(t); - t = t instanceof TypeMemPtr ptr && ptr.notNull() - ? TypeMemPtr.make((byte)2,((TypeMemPtr)wide)._obj,false).withAccess(ptr._ro) : wide; - } + if( !xfinal ) t = mutableInferredType(t); + // Instance fields may change until the constructor finishes. + // Keep the declared family/access, not the default's constant + // value or string contents. Class constants remain static. + else if( _scope.klast() instanceof Kind.Func fun && FunNode.isInstance(fun._name) && !(t instanceof TypeFunPtr) ) + t = constructorFieldType(t); } + // expr is a constant function - if( t instanceof TypeFunPtr && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { - if( expr instanceof ExternNode ) t = expr._type; // Upgrade declared type to exact function - else if( _code._link(tfp)!=null ) _code.link(tfp).setName(name); // Assign debug name to Simple function + if( t instanceof TypeFunPtr decl && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { + t = decl.makeFrom(tfp.fidx()); + FunNode fun = _code.link(tfp); + if( fun != null ) + fun.setName(name); // Assign debug name to Simple function } } else { + // Since no initializer, not-final but might need an initializer + // Need an expression to infer the type. // Also, if not-null then need an initializing expression. // Allowed in a class def, because type/init will happen in the constructor. @@ -893,281 +1258,393 @@ public class Parser { throw errorSyntax("=expression"); // Initial value for uninitialized struct fields. expr = switch( t ) { - // Nullable pointers get a NIL; not-null get a TOP which - // signals that they *must* be initialized in the constructor. - case TypeNil tn -> tn.nullable() ? NIL : con(Type.TOP); - case TypeInteger ti -> xfinal ? con(Type.TOP) : ZERO; - case TypeFloat tf -> con(xfinal ? Type.TOP : TypeFloat.FZERO); + // Required pointers retain their declared shape, but include + // null until every constructor path assigns them. + case TypeNil tn -> tn.nullable() ? con(Type.NIL) : con(t.meet(Type.NIL)); + case TypeInteger ti -> xfinal ? con(Type.BOTTOM) : _code.ZERO; + case TypeFloat tf -> con(xfinal ? Type.BOTTOM : TypeFloat.FZERO); // Bottom signals type inference: they must be initialized in // the constructor and that's when we'll discover the type. case Type tt -> { assert tt==Type.BOTTOM; yield con(tt); } }; - // Nullable fields are set in the constructor, but remain shallow final. - // e.g. final pointer to a r/w array - if( t instanceof TypeNil tn && !tn.nullable() && xfinal ) - fld_final = true; + // Every non-null pointer without a default is constructor-required. + // Without `!` it is also shallow-final; mutability and definite + // initialization are independent properties. + if( (xfinal && !(t instanceof TypeNil)) || (t instanceof TypeNil tn && !tn.nullable()) ) { + uninit = true; + if( xfinal ) fld_final = true; + } } // Lift expression, based on type - Node lift = liftExpr(expr, t, true); + // Do not let an authoritative Convert hide the nullable + // not-yet-initialized state from constructor-local type flow. + Node lift = uninit ? expr : convertExpr(expr, t); - // Lift type to the declaration. This will report as an error later if - // we cannot lift the type. // Define a new name if( !_scope.define(name,t,xfinal || fld_final,lift, loc) ) throw error("Redefining name '" + name + "'", loc); - _scope.lookup(name)._init = initialized; + _scope.lookup(name)._uninit = uninit; return lift; } - /** * Parse a struct declaration, and return the following statement. * Structs cannot be redefined, but can be nested. * * @return zero */ - private String _nestedType; - private static String shortName(String name) { return name.substring(name.lastIndexOf('.')+1); } + private Node parseStruct( ) { + // keyword "struct" already parsed, so expect the struct name next. + String typeName = requireId(); + require("{"); + + // create type class:NESTED.typeName; no fields. - private Node parseStruct() { String old = _nestedType; - String name = requireId(); - if( old!=null ) name=old+"."+name; - _nestedType=name; - TypeMemPtr ptr = TYPES.get(name) instanceof TypeMemPtr p ? p : TypeMemPtr.make(TypeStruct.open(name)); - TYPES.put(name,ptr); - require("{"); - TypeFunPtr sig = _code.makeFun(TypeFunPtr.make(false,false,new Type[]{ptr,TypeMem.BOT},TypeMem.BOT)); - ReturnNode init = parseFunctionBody(sig,loc(),null,true,"self","#selfMem"); - init.fun()._name=name+".<init>"; - INITIALIZERS.put(name,init); - TypeStruct shape = ((TypeMemPtr)TYPES.get(name))._obj; - ALLOCS.put(name,makeAllocator(shape,init)); - _nestedType=old; + String fullName = _scope.depth()==1 && _nestedType.endsWith(typeName) ? old : (old+"."+typeName).intern(); + _nestedType = fullName; // Recursive structs start with this as their basename + + ReturnNode ret = parseStruct( false, fullName ); + + _nestedType = old; + + // Insert a field into class:NESTED.typeName of "val typeName = fcn-ptr-to-init" + // Function does malloc internally. + // Future self: means subtyping has to deal with "who does the malloc" + String clzName = addClzPrefix(fullName); + TypeFunPtr sig = ret.fun().sig(); + TypeStruct ts = (TypeStruct)TYPES.get(clzName); + String initName = initFieldName(fullName); + Field oldFld = ts==null ? null : ts.field(initName); + Field fld = oldFld==null + ? Field.make(initName,sig,_code.alias(_ref._cname),true) + : oldFld.makeFrom(sig); + Field ctorFld = ts==null ? null : ts.field("<ctor>"); + // A same-named struct shares its file's class object and flag. + Field flag = ts==null ? null : ts.field("$init"); + ts = ctorFld==null + ? TypeStruct.make(clzName,false,fld) + : TypeStruct.make(clzName,false,fld,ctorFld); + if( flag!=null ) ts=ts.add(flag); + TypeFunPtr alloc = makeAllocator((TypeStruct)TYPES.get(fullName),ret,ctorFld); + ts = ts.add(Field.make("<new>",alloc,_code.alias(_ref._cname),true)); + TYPES.put(clzName,ts); + + // Insert a field in the containing class with the nested class type. + // This is basically sugar for "val CLZNAME = <init>" + // and is a nicer version of updateSelfAsFieldsDiscovered. + _scope.define(typeName, sig, true, new FunPtrNode(sig,_code._start,ret).peephole(), loc()); + require("}"); - require(";"); - return ZERO; + return require(_code.ZERO,";"); + } + + // Build the declaration-side allocation helper. Allocation sites only + // call this function; all layout, initialization and publication choices + // are made here, where the complete struct and constructor are known. + private TypeFunPtr makeAllocator(TypeStruct ts, ReturnNode initRet, Field ctorFld) { + TypeFunPtr ctor = ctorFld==null ? null : (TypeFunPtr)ctorFld._t; + int nargs = ctor==null ? 0 : ctor.nargs()-2; // Hide self and selfMem + Type[] atypes = new Type[nargs]; + for( int i=0; i<nargs; i++ ) atypes[i] = ctor.arg(i+2); + TypeMemPtr ptr = TypeMemPtr.make(ts); + TypeFunPtr sig = TypeFunPtr.make1((byte)2,true,atypes,ptr,_code.fidx(_ref._cname)); + String name = (ts._name+".<new>").intern(); + + int oldUID = _code.UID(); + FunNode fun = (FunNode)new FunNode(loc(),sig,name,_ref,null,_ref._start).peephole(); + _code.link(fun); + Node rpc = new ParmNode("$rpc",0,TypeRPC.BOT,fun,ConstantNode.seed(TypeRPC.BOT).peephole()).peephole(); + Node pub = new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun, + new ProjNode(_ref._start,1,ScopeNode.MEM0).peephole()).peephole(); + Node[] parms = new Node[nargs]; + for( int i=0; i<nargs; i++ ) + parms[i] = new ParmNode("arg"+i,i+2,atypes[i],fun,ConstantNode.seed(atypes[i]).peephole()).peephole(); + + NewNode nnn = new NewNode(ts,fun,off(ts," len")).init(); + Node self = new ProjNode(nnn,0,ts.str()).init(); + Node priv = new ProjNode(nnn,1,"#selfMem").init(); + Node[] initArgs = new Node[]{fun,pub,self,priv, + new FunPtrNode(initRet.fun().sig(),_code._start,initRet).peephole()}; + CallEndNode cend = rawCall(initArgs,initRet.fun()); + Node ctl = new CProjNode(cend,0,ScopeNode.CTRL).peephole(); + pub = new ProjNode(cend,1,ScopeNode.MEM0).peephole(); + priv = new ProjNode(cend,2,"#selfMem").peephole(); + + if( ctor != null ) { + Node[] cargs = new Node[nargs+5]; + cargs[0]=ctl; cargs[1]=pub; cargs[2]=self; cargs[3]=priv; + for( int i=0; i<nargs; i++ ) cargs[i+4]=parms[i]; + FunNode cfun = _code.link(ctor); + cargs[cargs.length-1] = new FunPtrNode(ctor,_code._start,cfun.ret()).peephole(); + cend = rawCall(cargs,cfun); + ctl = new CProjNode(cend,0,ScopeNode.CTRL).peephole(); + pub = new ProjNode(cend,1,ScopeNode.MEM0).peephole(); + priv = new ProjNode(cend,2,"#selfMem").peephole(); + } + + priv.keep(); + for( Field field : ts._fields ) { + Type storage = field._t.makeStorage(); + Field escaped = field._final ? field : field.makeFrom(storage).makeFrom(true); + pub.keep(); + Node esc = new EscapeNode(escaped,self,priv,pub).peephole(); + MemMergeNode merge = new MemMergeNode(null,pub); + merge.alias(field._alias,esc); + Node next = merge.peephole(); + pub.unkeep(); + pub = next; + } + priv.unkill(); + ReturnNode ret = (ReturnNode)new ReturnNode(ctl,pub,self,rpc,fun).peephole(); + fun.setRet(ret); + fun._approxUIDs = _code.UID() - oldUID; + _ref.addFun(_code,fun); + return sig; } - private int initFields(int index) { - TypeStruct shape = ((TypeMemPtr)TYPES.get(_nestedType))._obj; - while( index<_scope.nIns() ) { - Var v = _scope.var(index++); - if( v.isFRef() ) continue; - Node value = _scope.in(v); - // Only function declarations are shared across instances. A final - // data field can change in a named or inline constructor, so a - // constant default does not justify folding every future read. - boolean constant = v._final && v._init && value._type instanceof TypeFunPtr && value._type.isConstant(); - Field field = Field.make(v._name,v.type(),_code.getALIAS(),v._final,constant); - shape=shape.add(field); - TYPES.put(_nestedType,TypeMemPtr.make(shape)); - _fieldVars.put(v._name,v); - } - return index; + private CallEndNode rawCall(Node[] args, FunNode target) { + // Establish this known declaration-side edge immediately, but do not + // peephole or inline it while the surrounding factory is being built. + // IterPeeps therefore starts with the complete initializer caller set. + CallNode call = new CallNode(loc(),args).init(); + CallEndNode cend = new CallEndNode(call,TypeRPC.constant(_code.rpc(_ref._cname))).init(); + call.link(target); + return cend; + } + + // Parse a struct declaration (not an allocation); file-level is a class-init, and scope structs are normal + private ReturnNode parseStruct( boolean isClz, String typeName ) { + var oldExits = _ctorExits; + _ctorExits = new ArrayList<>(); + // Record & restore global state set during parsing the <init> code + Node oldCtrl= _scope.ctrl().keep(); + Node oldMem = _scope.mem ().keep(); + + // Make the future clazz/instance struct. + TypeStruct tself = (TypeStruct)TYPES.get(typeName); + if( tself==null ) + TYPES.put(typeName, tself = TypeStruct.make(typeName,true)); + else { + assert tself._open; + TYPES.put(typeName,tself = tself.defined()); + } + + // Class object keeps a runtime flag to signal the <clinit> has run. + if( isClz ) + TYPES.put(typeName,tself=tself.add(Field.make("$init",TypeInteger.BOOL,_code.alias(_ref._cname),false))); + + // Signature of the default constructor + TypeFunPtr sig = structInitSig(isClz,tself); + // Function and argument names of default constructor + String fname = (typeName + (isClz ? ".<clinit>" : ".<init>")).intern(); + String[] ids = new String[]{"self", isClz ? "arg" : "#selfMem"}; + + // Struct decls look like function bodies. Parse function body normally. + ReturnNode ret = _parseFunctionBody(fname,sig,loc(),ids); + _ctorExits = oldExits; + // Unwind global state. + ctrl(oldCtrl.unkeep()); + mem (oldMem .unkeep()); + return ret; } - private void finishFields() { - TypeStruct shape = ((TypeMemPtr)TYPES.get(_nestedType))._obj.close(); - TYPES.put(_nestedType,TypeMemPtr.make(shape)); - StructNode constants = new StructNode(shape); - int first = _scope._kinds.last()._lexSize; - for( int i=first; i<_scope.nIns(); i++ ) - if( !_scope.var(i).isFRef() ) constants.addDef(_scope.in(i)); - constants.setType(constants.compute()); - INITS.put(_nestedType,constants.keep()); + // <cl/init> signature: { self arg/selfMem -> BOT/selfMem } + // Self-memory is the rare *private* (never aliased) memory for the new object. + private TypeFunPtr structInitSig( boolean isClz, TypeStruct tself ) { + // Private uninitialized memory from new object, no escaped values. + TypeMem privMem = isClz ? null : TypeMem.make(1,tself,true,isClz,false,null,null); + Type targ = isClz ? TypeInteger.BOT : privMem; + // Return for private memory has to name all escaped fields, which are not known yet. + // Escape ALL fields, and sharpen later. + Type tret = isClz ? Type.BOTTOM : TypeMem.makePrivate(tself); + Type[] targs = new Type[]{ TypeMemPtr.make((byte)2,tself,isClz), targ }; + // FIDX for FREFs is in the global namespace, needs the correct clz name. + String clzBare = tself._name; + int order = 3; // 3 is always the <init> order, 2nd in the file just behind the <clinit> + if( isClz ) { + assert startsClzPrefix( clzBare ); + clzBare = clzBare.substring(clzPrefix.length()).intern(); + order = 2; // 2 is always the <clinit> order, first in file and skipping reserved #1 + } + return TypeFunPtr.make1((byte)2, true, targs, tret, _code.fidx(clzBare,order)); } - private Node self() { return _scope.in(_scope.lookup("self")); } - private Node privateMem() { return _scope.in(_scope.lookup("#selfMem")); } - - private boolean privateSelf(Node ptr) { - if( _privateFun!=_fun || _privateFun==null ) return false; - while( ptr instanceof ConvertNode || ptr instanceof ReadOnlyNode || ptr instanceof GuardNode ) ptr=ptr.in(1); - Node self=self(); - while( self instanceof ConvertNode || self instanceof ReadOnlyNode || self instanceof GuardNode ) self=self.in(1); - return ptr==self; + // A class type carries the constructor function as a required final field. + private TypeStruct classStruct( String typeName, TypeFunPtr init, boolean open ) { + return TypeStruct.make(addClzPrefix(typeName),open,Field.make(initFieldName(typeName),init,_code.alias(_ref._cname),true)); } - private boolean mayBeSelf(Node ptr, java.util.BitSet seen) { - if( _privateFun==null || _privateFun!=_fun || seen.get(ptr._nid) ) return false; - seen.set(ptr._nid); - if( privateSelf(ptr) ) return true; - if( ptr instanceof ConvertNode || ptr instanceof ReadOnlyNode || ptr instanceof GuardNode ) - return mayBeSelf(ptr.in(1),seen); - if( ptr instanceof PhiNode && !(ptr instanceof ParmNode) ) - for( int i=1; i<ptr.nIns(); i++ ) - if( ptr.in(i)!=null && mayBeSelf(ptr.in(i),seen) ) return true; - return false; + + private static String initFieldName( String typeName ) { + if( startsClzPrefix(typeName) ) + typeName = typeName.substring(clzPrefix.length()); + int idx = typeName.lastIndexOf('.'); + return (idx == -1 ? typeName : typeName.substring(idx+1)).intern(); } - private void checkPublication(Node value) { - // Private stores can replace a field only while no public alias can - // observe it. Early publication needs the next chapter's escape state. - if( mayBeSelf(value,new java.util.BitSet()) ) - throw error("Cannot publish self before the constructor finishes"); - } - // The only writes to the new object's fields are at helper exits. Branches - // and loops above this point merge values, not private-memory snapshots. - private Node storeFields(boolean initializer) { - TypeStruct shape=((TypeMemPtr)TYPES.get(_nestedType))._obj; - Node priv=privateMem().keep(); - for( Field f : shape._fields ) { - if( f._one ) continue; - Var v=_fieldVars.get(f._fname); - Node value=_scope.in(_scope.update(v,null)); // Force a lazy value Phi. - if( initializer && missingInit(value,new java.util.BitSet()) ) continue; - if( !initializer && ctrl()._type!=Type.XCONTROL && missingInit(value,new java.util.BitSet()) ) - throw error("'"+shape._name+"' is not fully initialized, field '"+f._fname+"' needs to be set in a constructor"); - checkPublication(value); - Node next=fieldStore(f,ctrl(),priv,self(),off(shape,f._fname),value); - priv.unkeep(); priv=next.keep(); - } - return priv.unkeep(); - } - - private Node fieldStore(Field f, Node ctrl, Node priv, Node ptr, Node off, Node value) { - Node st=new StoreNode(loc(),f._fname,f._alias,f._t,priv,ptr,off,value,true); - st.setDef(0,ctrl); - return new MemMergeNode(priv,f._alias,st.peephole()).peephole(); - } - - private Node constructorFunc() { - require("{"); - Ary<Type> types = new Ary<>(Type.class); - Ary<String> names = new Ary<>(String.class); - types.add(TYPES.get(_nestedType)); names.add("self"); - types.add(TypeMem.BOT); names.add("#selfMem"); - java.util.BitSet finals = new java.util.BitSet(); - finals.set(0); // Rebinding self would detach the pointer from its private memory. - while( true ) { - Type t=type(); - if( t==null ) break; - finals.set(names.size(),bindingFinal(t)); - types.add(t); names.add(requireId()); - match(","); - } - require("->"); - TypeFunPtr sig = _code.makeFun(TypeFunPtr.make(false,false,types.asAry(),TypeMem.BOT)); - // Bind the signature now, but parse the body after the field list is - // closed. Then even a later field has a local SSA slot before branches - // and loops are parsed. Strings/comments must not affect brace depth. - _constructor=new Constructor(sig,loc(),pos(),names.asAry(),finals); - int depth=1; - while( depth>0 ) { - if( peek('"') ) { parseString(); continue; } - if( match("'") ) { parseChar(); continue; } - char ch=_lexer.nextXChar(); - if( ch==Character.MAX_VALUE ) throw errorSyntax("}"); - if( ch=='{' ) depth++; - if( ch=='}' ) depth--; - } - return con(sig); + + private TypeStruct latestStruct( TypeStruct ts ) { + Type t = TYPES.get(ts._name); + return t instanceof TypeStruct ts2 ? ts2 : ts; } - private CallEndNode rawCall(Node ctrl, Node pub, ReturnNode target, Node... args) { - Node[] inputs = new Node[args.length+3]; - inputs[0]=ctrl; inputs[1]=pub; - System.arraycopy(args,0,inputs,2,args.length); - inputs[inputs.length-1]=new FunPtrNode(target.fun().sig(),target).peephole(); - // Keep the complete declaration-side caller set before any inlining. - CallNode call = new CallNode(loc(),inputs).init(); - CallEndNode end = new CallEndNode(call).init(); - call.link(target.fun()); - return end; + private Field fieldOrOpen( TypeStruct ts, String name, boolean xfinal ) { + ts = latestStruct(ts); + Field fld = ts.field(name); + if( fld != null ) return fld; + if( !ts._open || !ts._fref ) return null; + fld = Field.make(name,Type.BOTTOM,_code.alias(_ref._cname),xfinal); + TYPES.put(ts._name,ts = ts.add(fld)); + if( _ctorOpenStruct != null && _ctorOpenStruct._name == ts._name ) + _ctorOpenStruct = ts; + return fld; } - private Node publish(TypeStruct shape, Node ctrl, Node pub, Node priv, Node ptr) { - priv.keep(); ptr.keep(); - for( Field f : shape._fields ) if( !f._one ) { - pub.keep(); - Node esc = new EscapeNode(loc(),f,ctrl,pub,priv,ptr).peephole(); - Node next = new MemMergeNode(pub,f._alias,esc).peephole(); - pub.unkeep(); pub=next; - } - ptr.unkeep(); priv.unkill(); - return pub; - } - - private TypeFunPtr makeAllocator(TypeStruct shape, ReturnNode init) { - Field cf=shape.field(shortName(shape._name)); - TypeFunPtr ctor=cf!=null && cf._t instanceof TypeFunPtr f ? f : null; - int nargs=ctor==null ? 0 : ctor.nargs()-2; - Ary<Node> captures=CAPTURES.get(shape._name); - Type[] types=new Type[nargs+captures.size()]; - for( int i=0; i<nargs; i++ ) types[i]=ctor.arg(i+2); - for( int i=0; i<captures.size(); i++ ) types[nargs+i]=captures.at(i)._type; - TypeMemPtr ptr=TypeMemPtr.make(shape); - TypeFunPtr sig=_code.makeFun(TypeFunPtr.make(false,false,types,ptr)); - FunNode fun=new FunNode(loc(),sig,shape._name,null,_code._start).init(); - fun._name=shape._name+".<new>"; fun._constructor=true; - _code.link(fun); - Node rpc=new ParmNode("$rpc",0,TypeRPC.BOT,fun,con(TypeRPC.BOT)).init(); - Node pub=new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,new ProjNode(_code._start,1,ScopeNode.MEM0).peephole()).init(); - Node[] args=new Node[nargs+2]; - for( int i=0; i<nargs; i++ ) args[i+2]=new ParmNode("arg"+i,i+2,types[i],fun,con(types[i])).init(); - Node alloc=new NewNode(ptr,fun,null,off(shape," len")).init(); - Node self=new ProjNode(alloc,0,shape._name).init(); - Node priv=new ProjNode(alloc,1,"#selfMem").init(); - Node[] initArgs=new Node[captures.size()+2]; - initArgs[0]=self; initArgs[1]=priv; - for( int i=0; i<captures.size(); i++ ) - initArgs[i+2]=new ParmNode("capture"+i,nargs+i+2,types[nargs+i],fun,con(types[nargs+i])).init(); - CallEndNode end=rawCall(fun,pub,init,initArgs); - Node ctl=new CProjNode(end,0,ScopeNode.CTRL).init(); - pub=new ProjNode(end,1,ScopeNode.MEM0).init(); - priv=new ProjNode(end,2,"#selfMem").init(); - if( ctor!=null ) { - args[0]=self; args[1]=priv; - end=rawCall(ctl,pub,_code.link(ctor).ret(),args); - ctl=new CProjNode(end,0,ScopeNode.CTRL).init(); - pub=new ProjNode(end,1,ScopeNode.MEM0).init(); - priv=new ProjNode(end,2,"#selfMem").init(); - } - pub=publish(shape,ctl,pub,priv,self); - ReturnNode ret=new ReturnNode(ctl,pub,self,rpc,fun).init(); - fun.setRet(ret); _code._stop.addDef(ret); - return sig; + private Var defineOpenConstructorField( String name ) { + if( _ctorOpenStruct==null ) return null; + Field fld = fieldOrOpen(_ctorOpenStruct,name,true); + if( fld == null ) return null; + _scope.define(name,fld._t,fld._final,con(Type.TOP),loc()); + return _scope.lookup(name); } + // Parse and return a type or null. Valid types always are followed by an // 'id' which the caller must parse. This lets us distinguish forward ref // types (which ARE valid here) from local vars in an (optional) forward // ref type position. // t = int|i8|i16|i32|i64|u8|u16|u32|u64|byte|bool | flt|f32|f64 | val | var | struct[?] + + // Structs have nested names, and can be discovered with a partial name + // match. Example, searching for a type name "C.D" in the namespace "A.B". + // This will match "A.B.C.D", then "A.C.D", then "C.D". + + // Will also match external names like "sys.io" or "externModule.subtype.subtype" + private Type type() { - int old1 = pos(); - boolean writable = match("!"); - boolean readonly = !writable && match("~"); // Only type with a leading `{` is a function pointer... if( peek('{') ) return typeFunPtr(); // Otherwise you get a type name + int old1 = pos(); + boolean writable = match("!"); + boolean readonly = !writable && match("~"); String tname = _lexer.matchId(); if( tname==null ) return posT(old1); // Convert the type name to a type. - Type t0 = TYPES.get(tname); + Type t0 = TYPES.get(tname), t1 = t0; // No new types as keywords - if( t0 == null && KEYWORDS.contains(tname) ) - return posT(old1); - if( t0 == Type.BOTTOM || t0 == Type.TOP ) return t0; // var/val type inference + if( KEYWORDS.contains(tname) ) { + if( t0 == null ) return posT(old1); + // Something like "int" or "f64" or "var". + // Can be "int[]" so still need to check array-ness + } else { + if( t0 == Type.BOTTOM || t0 == Type.TOP ) return t0; // var/val type inference + + if( t0 != null ) { + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + sname = tname+"."+sname; + Type t = TYPES.get(sname); + if( t==null ) { pos(old2); break; } + tname = sname; + t0 = t; + } + } else { + // Type A.B.C starts with just A. Search our nested type path, + // e.g. path "X.Y.Z", in order: + + // If A==X its now a local type. + // If another dot, repeat for B==Y then C==Z + // Else if AX.B exists, done + // Else AX.B is a local forward ref. + // + // Else its a short cut form, one of: + // X.Y.Z.A + // X.Y.A + // X.A + // Else this all failed, assume a forward ref with A as an external symbol: + // A + + String root = _nestedType; + int ridx = root.indexOf('.'); + if( ridx != -1 ) root = root.substring(0,ridx); + if( tname.equals(root) ) { + boolean localForward = false; + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + sname = tname+"."+sname; + Type t = TYPES.get(sname); + if( t==null && !(_nestedType.equals(tname) || _nestedType.startsWith(tname+".")) && !localForward ) { + pos(old2); + break; + } + tname = sname; + t0 = t; + localForward |= t==null; + } + } else { + String nest = _nestedType, fullq = tname; + while( true ) { + fullq = nest+"."+tname; // Full qualified name + t0 = TYPES.get(fullq); + if( t0 != null ) + { tname = fullq; break; } // Search succeeded + // Subtract a layer from nested typenames + int idx = nest.lastIndexOf('.'); + if( idx== -1 ) break; // Search failed; leave as an external symbol + nest = nest.substring(0,idx); + } + if( t0==null ) { + CompUnit cu = ParseAll.findCompUnit(_code,_ref,tname); + if( cu != null ) { + tname = cu._cname; + t0 = TYPES.get(tname); + } + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + tname = tname+"."+sname; + t0 = TYPES.get(tname); + } + } else { + while( true ) { + int old2 = pos(); + if( !match(".") ) break; + String sname = matchId(); + if( sname==null ) { pos(old2); break; } + sname = tname+"."+sname; + Type t = TYPES.get(sname); + if( t==null ) { pos(old2); break; } + tname = sname; + t0 = t; + } + } + } + } + + tname = tname.intern(); + // Still no type found? Assume forward reference + t1 = t0 == null + ? TypeStruct.forward(tname) // Null: assume a forward ref type + : t0; + } + + // Structs are always actually references + if( t1 instanceof TypeStruct ts1 ) + t1 = TypeMemPtr.make(ts1); - // Check for subtype. - while( true ) { - int old2 = pos(); - if( !match(".") ) break; - String sname = _lexer.matchId(); - if( sname==null ) { pos(old2); break; } - String tsname = tname+"."+sname; - t0 = TYPES.get(tsname); - if( t0==null ) { pos(old2); t0 = TYPES.get(tname); break; } - tname = tsname; - } - - // Still no type found? Assume forward reference - Type t1 = t0 == null ? TypeMemPtr.make(TypeStruct.open(tname)) :t0; // Null: assume a forward ref type // A qualifier before the name applies to the struct reference, not // to an enclosing array. Arrays have their own [] / [~] qualifier. if( t1 instanceof TypeMemPtr tmp ) { @@ -1179,14 +1656,15 @@ public class Parser { Type t2 = t1; while( true ) { if( match("?") ) { - if( !(t2 instanceof TypeMemPtr tmp) ) + if( t2 instanceof TypeMemPtr tmp ) { + if( tmp.nullable() ) throw error("Type "+t2+" already allows null"); + t2 = tmp.makeNullable(); + } else throw error("Type "+t0+" cannot be null"); - if( tmp.nullable() ) throw error("Type "+t2+" already allows null"); - t2 = tmp.makeNullable(); } else if( match("[~]") ) { - t2 = typeAry(t2,true).makeRO(); + t2 = TypeMemPtr.make(typeAry(t2,true)).makeRO(); } else if( match("[]") ) { - t2 = typeAry(t2,false); + t2 = TypeMemPtr.make(typeAry(t2,false)); } else break; } @@ -1198,27 +1676,64 @@ public class Parser { int old2 = pos(); if( !match("!") ) match("~"); String id = _lexer.matchId(); - if( !(peek(',') || peek(';') || match("->")) || id==null ) + if( !(peek(',') || peek(';') || peek('=') || match("->")) || id==null ) return posT(old1); // Reset lexer to reparse pos(old2); // Reset lexer to reparse // Yes a forward ref, so declare it - TYPES.put(tname,((TypeMemPtr)t1).withAccess(false)); + forwardRefType(tname,(TypeStruct)((TypeMemPtr)t1)._obj); // Return the (array, final) type return t2; } - // Make an array type of t - private TypeMemPtr typeAry( Type t, boolean efinal ) { - if( t instanceof TypeMemPtr tmp && tmp.notNull() ) - throw error("Arrays of reference types must always be nullable"); - String tname = "[]"+t.str(); - TypeMemPtr ta = (TypeMemPtr)TYPES.get(tname); - if( ta==null ) - // Remember final version - TYPES.put(tname, ta = TypeMemPtr.make( TypeStruct.makeAry(tname,TypeInteger.U32,_code.getALIAS(),t,_code.getALIAS(),true) )); - // Reuse aliases, but preserve this occurrence's element permissions. - TypeStruct ary = ta._obj; - return TypeMemPtr.make(TypeStruct.makeAry(tname,TypeInteger.U32,ary._fields[0]._alias,t,ary._fields[1]._alias,efinal)); + private TypeMemPtr forwardRefType( String tname, TypeStruct tself ) { + TYPES.put(tname,tself); + String clzName = addClzPrefix( tname ); + TYPES.put(clzName, classStruct(tname,structInitSig(false,tself),true)); + return TypeMemPtr.make(tself); + } + + private TypeMemPtr forwardRefType( String tname ) { + tname = fullTypeName(tname); + return forwardRefType(tname,TypeStruct.forward(tname)); + } + + private String fullTypeName( String tname ) { + String nest = _nestedType, fullq; + while( true ) { + fullq = nest+"."+tname; + if( TYPES.get(fullq) != null ) + return fullq.intern(); + int idx = nest.lastIndexOf('.'); + if( idx== -1 ) return fullq.intern(); + nest = nest.substring(0,idx); + } + } + + // Make an array type of t. Always record a mutable version, + // but return the requested version. + private TypeStruct typeAry( Type t, boolean efinal ) { + // Non-null reference arrays are valid types, even though an ordinary + // zero-filled `new T[len]` cannot construct one. Allocation retains + // that check in alloc(); trusted runtimes (notably argv construction) + // may fill the private array completely before publishing it. + String tname = ("[]"+t.str()).intern(); + TypeStruct ta = (TypeStruct)TYPES.get(tname); + if( ta==null ) { + // Primitive arrays use the null compunit + String rcname = TYPES.get(t) instanceof TypeStruct ? _ref._cname : ("_."+t).intern(); + ta = TypeStruct.makeAry(tname,TypeInteger.U32,_code.alias(rcname),t,_code.alias(rcname), false ); + TYPES.put(tname,ta); + } + // Already have the aliases, just efinal is wrong + return TypeStruct.makeAry(tname,TypeInteger.U32,ta._fields[0]._alias,t,ta._fields[1]._alias,efinal ); + } + + private Type implicitSelfType() { + TypeStruct self = _scope.constructorSelf(); + // Ordinary instance methods accept the declared object shape. The + // constructor-only receiver starts at the low/default field values, + // and is built explicitly by constructorFunc(). + return self == null ? TypePtr.PTR : TypeMemPtr.make(self); } // A function type is `{ type... -> type }` or `{ type }`. @@ -1227,16 +1742,19 @@ public class Parser { match("{"); // Skip already-peeked '{' Type t0 = type(); // Either return or first arg if( t0==null ) return posT(old); // Not a function - if( match("}") ) // No-arg function { -> type } - return TypeFunPtr.make(match("?"),false,TypeFunPtr.TEMPTY,t0); Ary<Type> ts = new Ary<>(Type.class); + // Always reserve a receiver slot. Outside constructor scopes it is + // broad and unused, so call sites can uniformly pass their self. + ts.push(implicitSelfType()); + if( match("}") ) // No-arg function { -> type } + return TypeFunPtr.make(match("?"),ts.asAry(),t0); ts.push(t0); // First argument while( true ) { if( match("->") ) { // End of arguments, parse return Type ret = type(); if( ret==null || !match("}") ) return posT(old); // Not a function - return TypeFunPtr.make(match("?"),false,ts.asAry(),ret); + return TypeFunPtr.make(match("?"),ts.asAry(),ret); } Type t1 = type(); if( t1==null ) return posT(old); // Not a function @@ -1300,7 +1818,6 @@ public class Parser { } - /** * Parse an eq/ne expression */ @@ -1475,15 +1992,17 @@ public class Parser { // Pre-dec/pre-inc int old = pos(); if( match("--") || match("++") ) { - int delta = _lexer.peek(-1)=='+' ? 1 : -1; // Pre vs post + int delta = _lexer.peek(-1)=='+' ? 1 : -1; // Inc vs Dec String name = _lexer.matchId(); if( name!=null ) { Var n = _scope.lookup(name); if( n != null && !(n.type() instanceof TypeMemPtr) ) { + if( n._extern!=0 ) + return parsePostfixName(_scope.in(n),name,delta); if( n._final && !_scope.canInit(n) ) throw error("Cannot reassign final '"+n._name+"'"); - checkSharedField(n); - Node expr = liftExpr(peep(new AddNode(_scope.in(n),con(delta))),n.type(),true); + checkSharedFunction(n); + Node expr = convertExpr(peep(new AddNode(_scope.in(n),con(delta))),n.type()); _scope.update(n,expr); return expr; } @@ -1503,151 +2022,125 @@ public class Parser { * <pre> * primaryExpr : integerLiteral | "string" | 'char' | true | false | null | * new Type | '(' expression ')' | Id['++','--'] | - * [static.]* Id + * type['.' Id]* * </pre> * @return a primary {@link Node}, never {@code null} */ private Node parsePrimary() { if( _lexer.isNumber(_lexer.peek()) ) return parseLiteral(); - if( _lexer.peek('"') ) return newString(parseString()); + if( _lexer.peek('"') ) return parsePostfix(newString(parseString())); if( matchx("true" ) ) return con(1); - if( matchx("false") ) return ZERO; - if( matchx("null" ) ) return NIL; + if( matchx("false") ) return _code.ZERO; + if( matchx("null" ) ) return con(Type.NIL); if( match ("'" ) ) return parseChar(); if( match ("(" ) ) return parsePostfix(require(parseAsgn(), ")")); if( matchx("new" ) ) return parsePostfix(alloc()); if( match ("{" ) ) return parsePostfix(require(func(),"}")); - // Parse static variable lookup: `type.fld` in the type namespace - // CNC: not a full type parse, just a typename parse int pos = pos(); - Type t = type(); - if( t!=null ) { - if( peek('.') ) - // A namespace lookup is not a reference declaration. - return parsePostfix(con(TYPES.get(((TypeMemPtr)t)._obj._name))); - //pos(pos); // TODO: not a `type.fld` - //return null; - throw Utils.TODO(); - } - // Expect an identifier now - - // - if null, make fref, normal var handling - // - if final constant, normal var handling - // - if method var, scan skipping blocks & methods, must hit correct struct no fcns - // - if normal var, scan skipping blocks, allowing last fcn - // - NO fcns nested in methods, NO methods nested in fcns - String id = _lexer.matchId(); if( id == null || KEYWORDS.contains(id) ) throw errorSyntax("an identifier or expression"); + int pos2 = pos(); + // Attempt a local var lookup first Var var = _scope.lookup(id); - if( var==null ) { - // If missing, assume a forward reference - _scope.define(id, FRefNode.FREF_TYPE, true, XCTRL, loc()); - var = _scope.lookup(id); - } + // Not a local var, try again as a type name + pos(pos); + Type t = type(); + if( t!=null ) { + // Static <clinit> typename lookup + if( peek('.') ) // type.fld, which is really a field lookup in the class object + return parsePostfix(con(t)); + // Direct reference to a <clinit> type. Convert the instance + // type to a class type name + if( !(t instanceof TypeMemPtr tmp) ) { + //return new FRefNode(addClzPrefix(tmp._obj._name), loc()).peephole(); + // Not a local var, but yes a bare type... disallow a local + // FREF and return null, not-a-primary parse. + pos(pos); + return null; + } + } - // If a method var, allow nested blocks and methods, but not fcns or other structs. - // Else safety check for accessing fields out of function scope. - if( instance(var) ) { - Var selfVar = _scope.lookup("self"); - Node self = liftExpr(_scope.in(selfVar),selfVar.type(),true); - return parsePostfixName(self,id); + // Insert FREF outside enclosing Kind.Func scope. + pos(pos2); + t = FRefNode.FREF_TYPE; + // Check if this is a class. + CompUnit cu = ParseAll.findCompUnit(_code,_ref,id); + if( cu!=null ) { + // Must be a final class-type + String clzName = addClzPrefix(cu._cname); + TypeStruct clz = (TypeStruct)TYPES.get(clzName); + if( clz == null ) + TYPES.put(clzName,clz = TypeStruct.make(clzName,true)); + t = TypeMemPtr.make((byte)2,clz,true); + } + // Define the FRef + var = _scope.defineFRef(id,t,true,loc()); + _frefs.push((FRefNode)_scope.in(var)); } // Load local value - Node oldValue = _scope.in(var); - if( oldValue instanceof ConstantNode && !(oldValue instanceof FRefNode) && oldValue._type==Type.BOTTOM ) - throw error("Cannot read uninitialized field '"+id+"'"); - Node rvalue = liftExpr(oldValue,var.type(),true); - if( rvalue!=oldValue ) _scope.update(var,rvalue); - if( rvalue instanceof ConstantNode && rvalue._type == Type.BOTTOM ) - if( rvalue instanceof FRefNode ) - return parsePostfix(rvalue); - else throw error("Cannot read uninitialized field '"+id+"'"); + Node rvalue = _scope.in(var); + if( var._extern!=0 ) + return parsePostfixName(rvalue,id,0); + if( var._fref ) + return parsePostfix(rvalue); + // A constructor may read the unfinished object, but each field it + // reads must already be initialized on every path. Read the current + // local value; other fields may still be waiting for their first write. + if( inExplicitConstructor() && var._uninit && missingInit(rvalue,new BitSet()) ) + throw error("Field '"+id+"' might not be initialized",var._loc); + + // Check for an instance field load + int kx = _scope.kindx(var); // Declaration scope + int fx = _scope.enclosingFunction(); // Enclosing function scope + Kind kk = _scope._kinds.at(kx); + Kind fk = _scope._kinds.at(fx); + // Access instance field from 'self' + if( kx+1 == fx && kk instanceof Kind.Func inst && FunNode.isInstance(inst._name) && fk instanceof Kind.Func method && !inExplicitConstructor() ) + return parsePostfixName(_scope.in(method._lexSize),id,0); + + // Check for a function-escaping variable; these require true + // closures. Final constants are OK; final vars require a hidden var + // argument; not-final fields can just use an explicit struct arg. + if( kx < fx && !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 error("Variable '"+var._name+"' is out of function scope and must be a final constant"); + + // var.idx==2 is self argument, 3 is args, 4 or more are now static globals + if( var._idx >= 4 ) { + // Read from global scope. + // Surely a nicer way to get the top-level scope compile name? + Var clzptr = _scope.var( 2 ); + // Get the most recent sharpen global + TypeStruct clz = (TypeStruct) TYPES.get( ((TypeMemPtr) clzptr.type())._obj._name ); + TypeMemPtr clztmp = TypeMemPtr.make( (byte) 2, clz, true ); + return parsePostfixName( con( clztmp ), id,0 ); + //return parsePostfixName(_scope.in(2),id); + } + } // Check for assign-update, x += e0; char ch = _lexer.matchOperAssign(); - if( ch==0 ) // Normal primary, check for postfix updates - return parsePostfix(rvalue); + if( ch==0 ) { // Normal primary, check for postfix updates + int selfx = _scope.kindFcn(var)._lexSize; + // Might be a method call from inside a method, so no explicit "self". + // Pass the in-scope "self" + return parsePostfixMethod(rvalue,_scope.in(_scope.update(_scope.var(selfx),null))); + } // Assign-update direct into Scope if( var._final && !_scope.canInit(var) ) throw error("Cannot reassign final '"+var._name+"'"); - checkSharedField(var); - checkCaptureWrite(var); + checkSharedFunction(var); Node op = opAssign(ch,rvalue, var.type() ); _scope.update(var,op); return postfix(ch) ? rvalue.unkeep() : op; } - // true if instance variable lookup (var is in any number of nested blocks, - // then an instance function then inside a struct declaration). - private boolean instance(Var var ) { - // If a method var, allow nested blocks and methods, but not fcns or other structs - Kind kind = _scope.kind(var); - if( kind instanceof Kind.Define ) { - if( var.isFRef() ) return false; // Promote an unresolved lexical name, not self.name. - if( _privateFun==_fun && _privateFun!=null ) return true; - for( int i = _scope.depth()-1; i>=0 && var._idx < _scope._kinds.at(i)._lexSize; i-- ) { - Kind xkind = _scope._kinds.at(i); - if( xkind instanceof Kind.Block ) continue; - if( xkind instanceof Kind.Func ) { - if( _scope._kinds.at(i-1)==kind ) - return true; // Instance variable: lookup in a method in a decl - } - throw Utils.TODO(); - } - } else { - // If a normal var, allow any nested blocks, and a last fcn - - // but not functions further out (which involves capture). - Node def = _scope.in(var._idx); - if( _captures!=null && _scope.kind(var) instanceof Kind.Func && - !(var._final && def._type.isConstant()) && !(def instanceof FRefNode) ) { - if( !(def instanceof ParmNode parm && parm.fun()==_fun) ) { - _captures.add(def.keep()); - Type type=def._type; - int index=_fun.sig().nargs(); - _fun.addParseArg(type); - _scope.update(var,new ParmNode(var._name,index+2,type,_fun,con(type)).init()); - } - return false; - } - for( int i = _scope.depth()-1; i>=0 && var._idx < _scope._kinds.at(i)._lexSize; i-- ) - if( _scope._kinds.at(i) instanceof Kind.Func && - !(def instanceof FRefNode) && !(var._final && def._type.isConstant()) ) - throw error("Variable '"+var._name+"' is out of function scope and must be a final constant"); - } - return false; - } - - private void checkCaptureWrite(Var var) { - if( _captures==null ) return; - for( int i=_scope.depth()-1; i>=0 && var._idx<_scope._kinds.at(i)._lexSize; i-- ) - if( _scope._kinds.at(i) instanceof Kind.Func ) - throw error("Cannot assign captured variable '"+var._name+"' in an initializer"); - } - - // Inline constructors have local field slots too, but shared function - // declarations have no instance slot to publish. Do not silently discard - // an assignment to one of those declarations. - private void checkSharedField(Var var) { - if( !_scope.canInit(var) ) return; - if( _scope.kind(var) instanceof Kind.Define ) { - checkPrivateWrite(var._name); - return; - } - for( int i=_scope.depth()-1; i>0; i-- ) - if( _scope._kinds.at(i) instanceof Kind.Alloc alloc ) { - Field field=alloc._tmp._obj.field(var._name); - if( field!=null && field._one ) - throw error("Cannot reassign shared function '"+var._name+"'"); - return; - } - } - // Check for assign-update, "x += e0". // Returns "x + e0"; the caller assigns value. // if ch is postfix, then lhs is kept and caller will unkeep and return it. @@ -1670,17 +2163,17 @@ public class Parser { case '-' -> new SubNode(lhs,rhs); case '*' -> new MulNode(lhs,rhs); case '/' -> new DivNode(lhs,rhs); - case '&' -> new AndNode(loc(),lhs,rhs); - case '|' -> new OrNode(loc(),lhs,rhs); + case '|' -> new OrNode(null,lhs,rhs); + case '&' -> new AndNode(null,lhs,rhs); case '^' -> new XorNode(loc(),lhs,rhs); case Lexer.SHL -> new ShlNode(loc(),lhs,rhs); case Lexer.SAR -> new SarNode(loc(),lhs,rhs); case Lexer.SHR -> new ShrNode(loc(),lhs,rhs); - default -> throw Utils.TODO(); + default -> throw TODO(); }; // Convert to float ops, or narrow int types; error if not declared type. // Also, if postfix LHS is still keep() - return liftExpr(peep(op),t,true); + return convertExpr(peep(op),t); } @@ -1689,161 +2182,119 @@ public class Parser { */ private Node alloc() { Type t = type(); - if( t==null ) throw error("Expected a type"); + if( t==null ) { + String tname = _lexer.matchId(); + if( tname==null || KEYWORDS.contains(tname) ) + throw error("Expected a type"); + t = forwardRefType(tname); + } // Parse ary[ length_expr ] if( match("[") ) { if( !t.makeZero().isa(t) ) - throw error("Cannot allocate a non-nullable, since arrays always zero/null fill"); - Node len = parseAsgn(); - if( !(len._type instanceof TypeInteger) ) - throw error("Cannot allocate an array with length "+len._type); + throw error("Cannot allocate a non-nullable, since arrays are always zero/null filled"); + Node len = parseAsgn().keep(); require("]"); - TypeMemPtr tmp = typeAry(t,false); - return newArray(tmp._obj,len); - } - - if( !(t instanceof TypeMemPtr tmp) ) - throw error("Cannot allocate a "+t.str()); - - TypeStruct shape=tmp._obj; - TypeFunPtr sig=ALLOCS.get(shape._name); - if( sig==null ) throw error("Unknown struct type '"+shape._name+"'"); - if( !peek('{') ) { - // Obvious missing defaults can still be diagnosed here. A user - // constructor or a merge needs the later publication check. - if( shape.field(shortName(shape._name))==null ) { - StructNode defaults=INITS.get(shape._name); - Type initial=INITIALIZERS.get(shape._name).expr()._type; - for( int i=0; i<shape._fields.length; i++ ) { - Field f=shape._fields[i]; - Field written=initial instanceof TypeMem m && m._t instanceof TypeStruct ts ? ts.field(f._fname) : null; - if( missingInit(defaults.in(i),new java.util.BitSet()) && (written==null || written._t==Type.BOTTOM) ) - throw error("'"+shape._name+"' is not fully initialized, field '"+f._fname+"' needs to be set in a constructor"); - } - } - Node fptr=new FunPtrNode(sig,_code.link(sig).ret()).peephole(); - Ary<Node> args=new Ary<>(Node.class); - if( match("(") ) { - while( !peek(')') ) { - Node arg=parseAsgn(); - checkPublication(arg); - args.add(arg.keep()); - if( !match(",") ) break; - } - require(")"); - } - for( Node capture : CAPTURES.get(shape._name) ) { - checkPublication(capture); - args.add(capture.keep()); - } - if( args.size()!=sig.nargs() ) throw error("Wrong number of constructor arguments for '"+shape._name+"'"); - for( Node arg : args ) arg.unkeep(); - return callWithArgs(fptr,args.asAry()); - } - require("{"); - Node alloc=new NewNode(TypeMemPtr.make(shape),ctrl(),null,off(shape," len")).init(); - Node ptr=new ProjNode(alloc,0,shape._name).init().keep(); - Node priv=new ProjNode(alloc,1,"#selfMem").init(); - Ary<Node> captures=CAPTURES.get(shape._name); - Node[] args=new Node[captures.size()+2]; - args[0]=ptr; args[1]=priv; - for( int i=0; i<captures.size(); i++ ) { - checkPublication(captures.at(i)); - args[i+2]=captures.at(i); - } - CallEndNode end=rawCall(ctrl(),mem(),INITIALIZERS.get(shape._name),args).keep(); - ctrl(new CProjNode(end,0,ScopeNode.CTRL).peephole()); - mem(new ProjNode(end,1,ScopeNode.MEM0).peephole()); - priv=new ProjNode(end,2,"#selfMem").peephole().keep(); - end.unkeep(); - int first=_scope.nIns(); - _scope.push(new Kind.Block()); - StructNode constants=INITS.get(shape._name); - for( int i=0; i<shape._fields.length; i++ ) { - Field f=shape._fields[i]; - Type initial=INITIALIZERS.get(shape._name).expr()._type; - Field written=initial instanceof TypeMem m && m._t instanceof TypeStruct ts ? ts.field(f._fname) : null; - boolean missing=missingInit(constants.in(i),new java.util.BitSet()) && - (written==null || written._t==Type.BOTTOM); - Node value=f._one ? constants.in(i) - : missing ? con(Type.BOTTOM) - : new LoadNode(loc(),f._fname,f._alias,priv,ptr,off(shape,f._fname),true).peephole(); - _scope.define(f._fname,f._t,f._final,value,loc()); - } - require(parseBlock(new Kind.Alloc(tmp)),"}"); - for( int i=0; i<shape._fields.length; i++ ) { - Field f=shape._fields[i]; - if( f._one ) continue; - Node value=_scope.in(first+i); - checkPublication(value); - if( ctrl()._type!=Type.XCONTROL && missingInit(value,new java.util.BitSet()) ) - throw error("'"+shape._name+"' is not fully initialized, field '"+f._fname+"' needs to be set in a constructor"); - Node next=fieldStore(f,ctrl(),priv,ptr,off(shape,f._fname),value); - priv.unkeep(); priv=next.keep(); + return allocArray(typeAry(t,false),len); + } else { + if( !(t instanceof TypeMemPtr tmp) ) + throw error("Cannot allocate a "+t.str()); + return allocStruct(tmp._obj); + } + } + + private Node allocStruct(TypeStruct ts) { + 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 + // to the completed nested type before selecting its class helper. + String typeName = ts._name.indexOf('.') == -1 ? fullTypeName(ts._name) : ts._name; + Type resolved = TYPES.get(typeName); + if( resolved instanceof TypeStruct rts ) ts = rts; + TypeStruct clz = (TypeStruct)TYPES.get(addClzPrefix(typeName)); + if( clz==null ) + throw error("Unknown struct type '" + ts._name + "'"); + Field afld = clz.field("<new>"); + Node alloc; + if( afld != null ) { + TypeFunPtr tfp = (TypeFunPtr)afld._t; + FunNode fun = _code.link(tfp); + alloc = fun==null + ? new FRefNode((addClzPrefix(typeName)+".<new>").intern(),loc()) + : new FunPtrNode(tfp,_code._start,fun.ret()).peephole(); + } else { + if( !ts._fref ) + throw error("Constructor arguments for '" + ts._name + "' but no constructor is defined"); + Type[] sig = new Type[ctorArgs._len]; + for( int i=0; i<ctorArgs._len; i++ ) + sig[i] = ctorArgs.at(i)._type; + alloc = new FRefNode((addClzPrefix(typeName)+".<new>").intern(),loc(), + TypeFunPtr.make(false,sig,TypeMemPtr.make(ts))); + } + Ary<Node> args = new Ary<>(Node.class); + args.add(ctrl()); args.add(mem()); + for( Node arg : ctorArgs ) args.add(arg.unkeep()); + args.add(alloc); + return functionCall(args); + } + + private Ary<Node> constructorArgs() { + Ary<Node> args = new Ary<>(Node.class); + if( !match("(") ) + return args; + while( !peek(')') ) { + Node arg = parseAsgn(); + if( arg==null ) break; + args.push(arg.keep()); + if( !match(",") ) break; } - _scope.pop(); - mem(publish(shape,ctrl(),mem(),priv.unkeep(),ptr)); - return ptr.unkeep(); - } - - // The old constructor model requires each field on every path. An - // unresolved expression also has BOTTOM now, so inspect the actual - // sentinel and merges instead of mistaking an unknown type for no write. - private boolean missingInit(Node n, java.util.BitSet visit) { - if( n==null || visit.get(n._nid) || n instanceof FRefNode ) return false; - visit.set(n._nid); - if( n instanceof ConstantNode ) return n._type==Type.BOTTOM || n._type==Type.TOP; - if( n instanceof ConvertNode || n instanceof ReadOnlyNode ) return missingInit(n.in(1),visit); - if( n instanceof PhiNode phi ) - for( int i=1; i<n.nIns(); i++ ) - if( phi.region().in(i)!=null && phi.region().in(i)._type!=Type.XCONTROL && missingInit(n.in(i),visit) ) return true; - return false; + require(")"); + return args; } + private Node allocArray(TypeStruct ts, Node len) { + Field lenFld = ts.field("#" ); + Field bodyFld = ts.field("[]"); + + ConFldOffNode base = off(ts, "[]"); + int scale = ts.aryScale(); + Node size = peep(new AddNode(base,peep(new ShlNode(null,len,con(scale))))); + + // Build a NewNode; takes in and puts out all aliases. + NewNode nnn = new NewNode(ts, ctrl(), size ).init(); + ProjNode self = new ProjNode(nnn,0,ts.str()).init().keep(); + ProjNode smem = new ProjNode(nnn,1,"#selfMem").init().keep(); + + // Store length. Rest of array is zero'd via CALLOC during CodeGen. + // Length is casted to sanity. + // TODO: Needs runtime check. + Node st = peep(new StoreNode(null, "#", 1, TypeInteger.U32, null, smem, self, off(ts,"#"), len.unkeep(), true)); + + // TODO: Allow add-on "constructor" to init the array elements - private Node newArray(TypeStruct ary, Node len) { - ConFldOffNode base=off(ary,"[]"); - Node size=peep(new AddNode(base,peep(new ShlNode(null,len.keep(),con(ary.aryScale()))))); - Node alloc=new NewNode(TypeMemPtr.make(ary),ctrl(),null,size).init(); - Node ptr=new ProjNode(alloc,0,ary._name).init().keep(); - Node priv=new ProjNode(alloc,1,"#selfMem").init(); - // calloc supplied the zero elements; initialize the length before - // either alias becomes visible to the public heap. - Field length=ary.field("#"); - priv=fieldStore(length,ctrl(),priv,ptr,off(ary,"#"),len.unkeep()); - mem(publish(ary,ctrl(),mem(),priv,ptr)); - return ptr.unkeep(); + Node prior = mem(); + Node esc = peep(new EscapeNode(lenFld,self,st,prior)); + mem(mergeAlias(prior,lenFld._alias,esc)); + prior = mem(); + esc = peep(new EscapeNode(bodyFld,self,smem,prior)); + mem(mergeAlias(prior,bodyFld._alias,esc)); + smem.unkeep(); + return self.unkeep(); } + private Node newString(String s) { - TypeMemPtr tmp = typeAry(TypeInteger.U8,true); - int lenAlias = tmp._obj.field("#" )._alias; - int elemAlias = tmp._obj.field("[]")._alias; - TypeConAryB con = TypeConAryB.make(s); - Type elem = con.elem(); + TypeStruct ts = 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()); // Make a TMP, not-null (byte)2, singleton (true) (requires text // strings are hash-interned), with a TypeStruct having a constant // array body. - TypeMemPtr str = TypeMemPtr.make((byte)2,TypeStruct.makeAry("[]u8", TypeInteger.constant(s.length()), lenAlias, con, elemAlias, true),true); + TypeMemPtr str = TypeMemPtr.make((byte)2,TypeStruct.makeAry("[]u8", slen, lenAlias, body, elemAlias, true),true); assert str.isConstant(); return con(str.makeRO()); } - // Memory is one hidden SSA variable, including across branches and loops. - private Node mem() { return _scope.mem(); } - private void mem(Node n) { _scope.mem(n); } - - private void store(String name, int alias, Type glb, Node ptr, Node off, Node val, boolean init, Node ctrl) { - if( !init && ptr._type instanceof TypeMemPtr view && view._ro && ctrl()._type!=Type.XCONTROL ) - throw error("Cannot modify final field '"+name+"'"); - checkPublication(val); - Node prior = mem().keep(); - Node st = new StoreNode(loc(),name,alias,glb,prior,ptr,off,val,init); - st.setDef(0,ctrl); - st = st.peephole(); - mem(new MemMergeNode(prior,alias,st).peephole()); - prior.unkill(); - } - /** * Parse postfix expression; this can be a field expression, an array * lookup or a postfix operator like '#' @@ -1856,15 +2307,87 @@ public class Parser { * expr '(' [args,]* ')' // Function call * </pre> */ + private Node parsePostfixMethod(Node expr, Node self) { + // TODO: This is too broad because the parser lacks strong types here. + // Some normal functions can end up with a "self" argument. + + // Self method call? Self is a TMP-not-CLZ, then assume and pass self. + if( match("(") ) { + expr = parsePostfix(require(functionCall(expr,self),")")); + } else if( expr != self ) + self.isKill(); // Not a method call, no need for self + return parsePostfix(expr); + } private Node parsePostfix(Node expr) { + _lexer.skipWhiteSpace(); + Lexer site = loc(); String name; if( match(".") ) name = requireId(); else if( match("#") ) name = "#"; else if( match("[") ) name = "[]"; - else if( match("(") ) return parsePostfix(require(functionCall(expr,null),")")); + // Can get here without an explicit receiver: "f()" or + // "self.method()(next)(next)(next)". Pass the current function self. + else if( match("(") ) return parsePostfix(require(functionCall(expr,defaultSelf()),")")); else return expr; // No postfix - return parsePostfixName(expr,name); + return parsePostfixName(expr,name,0,site); + } + + private boolean privateSelf(Node ptr) { + while( ptr instanceof ConvertNode || ptr instanceof ReadOnlyNode || ptr instanceof GuardNode ) ptr=ptr.in(1); + Node self=defaultSelf(); + while( self instanceof ConvertNode || self instanceof ReadOnlyNode || self instanceof GuardNode ) self=self.in(1); + return ptr==self; + } + + // An explicit self.x read sees the same current value as bare x. Loading + // the allocation here would miss updates still held in local SSA values. + private Node privateField(String name, int pre) { + int idx=fieldVarIdxBefore(_scope._kinds.at(_scope.enclosingFuncOrDecl())._lexSize,name); + if( idx<0 ) throw error("Cannot resolve field '"+name+"'"); + Var v=_scope.var(idx); + if( pre==0 && matchOpx('=','=') ) { + checkSharedFunction(v); + Node value=parseAsgn().keep(); + _scope.update(v,convertExpr(value,v.type())); + return value.unkeep(); + } + Node value=_scope.in(_scope.update(v,null)); + if( v._uninit && missingInit(value,new BitSet()) ) + throw error("Field '"+name+"' might not be initialized",v._loc); + char ch=pre==0 ? _lexer.matchOperAssign() : (char)pre; + if( ch!=0 ) { + checkSharedFunction(v); + Node op=opAssign(ch,value,v.type()); + _scope.update(v,op); + if( pre!=0 && postfix(ch) ) value.unkeep(); + return pre==0 && postfix(ch) ? value.unkeep() : op; + } + return parsePostfixMethod(value,defaultSelf()); + } + + private void checkSharedFunction(Var v) { + if( _scope.canInit(v) && v._final && v.type() instanceof TypeFunPtr t && t.isConstant() ) + throw error("Cannot reassign shared function '"+v._name+"'"); + } + + // A loop Phi is temporarily bottom before its backedge is parsed. That is + // not an unwritten field: follow its live inputs to the actual defaults. + private boolean missingInit(Node value, BitSet seen) { + if( value==null || seen.get(value._nid) ) return false; + seen.set(value._nid); + if( value instanceof PhiNode phi && !(value instanceof ParmNode) ) { + for( int i=1; i<phi.nIns(); i++ ) + if( phi.region().in(i)!=null && phi.region().in(i)._type!=Type.XCONTROL && missingInit(phi.in(i),seen) ) + return true; + return false; + } + return !initialized(value._type); + } + + private Node defaultSelf() { + int selfx = _scope._kinds.at(_scope.enclosingFuncOrDecl())._lexSize; + return _scope.in(_scope.update(_scope.var(selfx),null)); } /** @@ -1877,184 +2400,127 @@ public class Parser { * expr ('[' expr ']')* [ [op]= expr ] // Array reference * </pre> */ - private Node parsePostfixName(Node expr, String name) { - if( privateSelf(expr) ) return parsePrivateField(name); - if( !privateSelf(expr) && mayBeSelf(expr,new java.util.BitSet()) ) - throw error("Cannot access self through a merged alias during construction"); - - if( expr._type==Type.NIL ) - throw error("Accessing unknown field '" + name + "' from 'null'"); - - // Sanity check expr for being a reference - if( !(expr._type instanceof TypeMemPtr ptr) ) - return parseUnknownField(expr,name); - - // Find the field from the Type. Lookup in the base object field names. - TypeMemPtr ptr2 = (TypeMemPtr)TYPES.get(ptr._obj._name); - if( !ptr._obj.isAry() ) - ptr = ptr.makeFrom(ptr2._obj); // The declaration fixes the field shape; retain this view's access. - TypeStruct base = ptr._obj; - int fidx = base.find(name); - if( fidx==-1 && base._open ) return parseUnknownField(expr,name); - if( fidx == -1 ) - throw error("Accessing unknown field '" + name + "' from '" + ptr.str() + "'"); - - // Get field type and layout offset from base type and field index fidx - Field f = base._fields[fidx]; // Field from field index - Type tf = f._t; - if( tf instanceof TypeFunPtr ctor && ctor.ret() instanceof TypeMem ) - throw error("A constructor is only called by new"); - if( tf instanceof TypeMemPtr ftmp && ftmp.isFRef() ) - tf = ftmp.makeFrom(((TypeMemPtr)(TYPES.get(ftmp._obj._name)))._obj); - if( base.isAry() && tf instanceof TypeConAry con ) - tf = con.elem(); + // pre is +/-1 for a prefix update, otherwise parse a postfix operation. + private Node parsePostfixName(Node expr, String name, int pre) { + return parsePostfixName(expr,name,pre,loc()); + } + + private Node parsePostfixName(Node expr, String name, int pre, Lexer site) { + if( inExplicitConstructor() && privateSelf(expr) ) return privateField(name,pre); + // Keep expr across possible updates expr.keep(); + + // Get the best known TypeStruct from a pointer type. + TypeStruct ts = null, ts2; + if( expr._type instanceof TypeMemPtr tmp && (ts2=((TypeStruct)TYPES.get(tmp._obj._name))) != null ) + ts = (TypeStruct)tmp._obj.join(ts2); + // A Load's flow type is intentionally not promised during parsing, + // but its declaring field type is still known. Recover that static + // type for chained accesses such as `_grow(1).buf[len++]`; otherwise + // the array Store is parsed as alias #1 and can hide the independent + // side effect of the postfix `len++`. + if( ts==null && expr instanceof LoadNode load ) { + TypeStruct ots = load.off() instanceof ConFldOffNode coff ? coff._ts : null; + if( ots==null && load.ptr()._type instanceof TypeMemPtr owner ) { + Type owner0 = TYPES.get(owner._obj._name); + ots = owner0 instanceof TypeStruct obj ? obj : owner._obj; + } + if( ots != null ) { + Field loaded = ots.field(load._name); + if( loaded != null && loaded._t instanceof TypeMemPtr ltmp ) { + Type loaded0 = TYPES.get(ltmp._obj._name); + ts = loaded0 instanceof TypeStruct obj ? obj : ltmp._obj; + } + } + } + + Field fld = ts==null ? null : ts.field(name); + if( fld==null && ts!=null && !startsClzPrefix(ts._name) ) { + Type clz0 = TYPES.get(addClzPrefix(ts._name)); + if( clz0 instanceof TypeStruct clz && (fld = latestStruct(clz).field(name)) != null ) { + expr.unkeep(); + ts = latestStruct(clz); + expr = con(TypeMemPtr.make((byte)2,ts,true)).keep(); + } + } + if( fld==null && ts!=null ) + fld = fieldOrOpen(ts,name,false); + if( fld==null && ts!=null && !latestStruct(ts)._open ) + throw error("Accessing unknown field '"+name+"' from '*"+ts._name+"'"); + + // We have to record "active touches" of a new class, to track when we + // need to force class initialization. Basically, we insert an CLInit, + // and if it does not fold away it'll turn into a runtime check. + Kind.Func func = (Kind.Func)_scope._kinds.at(_scope.enclosingFuncOrDecl()); + if( (ts==null || ts==TypeStruct.BOT || ts.isHigh() || startsClzPrefix(ts._name)) && + !(ts!=null && (ts._name+".<clinit>").equals(func._name)) ) + classInit(site,expr); + + // With no field, we will update the bulk memory + int alias = fld==null ? 1 : fld._alias; + Type decl = fld==null ? Type.BOTTOM : fld._t; + // Field offset; fixed for structs, computed for arrays - Node off = (name.equals("[]") // If field is an array body - // Array index math - ? peep(new AddNode(off(base,"[]"),peep(new ShlNode(null,require(parseAsgn(),"]"),con(base.aryScale()))))) - // Struct field offsets are hardwired - : off(base,name)).keep(); + Node off = (name=="[]" + // Array element math, with unknown base offset and shift amount. + ? peep(new AddNode(fldoff(expr,"[]"),peep(new ShlNode(null,require(parseAsgn(),"]"),fldoff(expr,"<<"))))) + // Named field offset on an unknown struct type + : fldoff(expr,name)).keep(); // Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment - if( matchOpx('=','=') ) { + if( pre==0 && matchOpx('=','=') ) { + // Field assignment Node val = parseAsgn().keep(); - Node lift = liftExpr( val, tf, false ); + // Lift value for store + Node lift = fld==null ? val : new ConvertNode(fld._t,val).peephole(); + // Memory for store, post assignment expression + Node mem = mem(); + // Store to field + mem.keep(); + if( expr._type instanceof TypeMemPtr view && view._ro && ctrl()._type!=Type.XCONTROL ) + throw error("Cannot modify final field '"+name+"'"); + Node st = new StoreNode(loc(), name, alias, decl, ctrl(), mem, expr.unkeep(), off.unkeep(), lift, false).peephole().keep(); + storeMem(st,alias,mem); + st.unkeep(); + mem.unkeep(); - store(name,f._alias,tf,expr.unkeep(),off.unkeep(),lift,false,base.isAry() ? ctrl() : null); return val.unkeep(); // "obj.a = expr" returns the expression while updating memory } + // Memory for load + Node mem = mem(); + // Loading from a constant array, the declared type is the + // meet-over-elements and not the array itself. + if( name=="[]" && decl instanceof TypeConAry conary ) + decl = conary.elem(); + // Load field - Node load = f._one && INITS.containsKey( base._name ) - ? INITS.get(base._name).in(base.find(name)) - : new LoadNode(loc(),name, f._alias, mem(), expr, off); - // Arrays include control, as a proxy for a safety range check - // Structs don't need this; they only need a NPE check which is - // done via the type system. - if( base.isAry() && !name.equals("#") ) load.setDef(0,ctrl()); - load = peep(load); + Node load = peep(new LoadNode(loc(),name, alias, ctrl(), mem, expr, off)); // Check for assign-update, "ptr.fld += expr" or "ary[idx]++" - char ch = _lexer.matchOperAssign(); + char ch = pre==0 ? _lexer.matchOperAssign() : (char)pre; if( ch!=0 ) { - Node op = opAssign(ch,load,tf); - store(name,f._alias,tf,expr.unkeep(),off.unkeep(),op,false,base.isAry() ? ctrl() : null); - load = postfix(ch) ? load.unkeep() : op; - // And use the original loaded value as the result - return load; - //throw Utils.TODO(); // NO POSTFIX AFTER STORE, JUST RETURN - } else { - off.unkill(); - // Method call: Loaded a function from a named field, AND the base - // field type is final. - if( load._type instanceof TypeFunPtr && name!="[]" && - _code.externFunc((TypeFunPtr)load._type)==null && - ((TypeMemPtr)TYPES.get(base._name))._obj.field(name)._final && - // And calling a function - match("(") ) { - return parsePostfix(require(functionCall(load,expr),")")); - } - - // Drop expr, keeping load alive - load.keep(); - expr.unkill(); - load.unkeep(); - } - - return parsePostfix(load); - } - - - // Syntax fixes the access even when a forward definition or a loop - // leaves the receiver untyped. Resolve its alias and layout in the graph. - private Node parseUnknownField(Node expr, String name) { - expr.keep(); - Node off = new FldOffNode(expr,name).peephole(); - if( name.equals("[]") ) { - Node index = require(parseAsgn(),"]"); - Node scale = new FldOffNode(expr,"<<").peephole(); - off = peep(new AddNode(off,peep(new ShlNode(loc(),index,scale)))); - } - off.keep(); - if( matchOpx('=','=') ) { - Node value = parseAsgn().keep(); - unknownStore(name,expr,off,value); - expr.unkill(); - off.unkill(); - return value.unkeep(); - } - Node load = new LoadNode(loc(),name,1,mem(),expr,off); - load.setDef(0,ctrl()); - load = peep(load); - char ch = _lexer.matchOperAssign(); - if( ch!=0 ) { - Node op = opAssign(ch,load,Type.BOTTOM).keep(); - unknownStore(name,expr,off,op); - // The field width is not known yet. A compound assignment returns - // the stored value after conversion; postfix returns the old load. - // Reading our own Store folds once its alias and width are known. + if( fld!=null && decl==Type.BOTTOM ) + throw error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor" ); + Node op = opAssign(ch,load, decl ).keep(); + mem.keep(); + if( expr._type instanceof TypeMemPtr view && view._ro && ctrl()._type!=Type.XCONTROL ) + throw error("Cannot modify final field '"+name+"'"); + Node st = new StoreNode(loc(), name, alias, decl, ctrl(), mem, expr.unkeep(), off.unkeep(), op, false).peephole().keep(); + storeMem(st,alias,mem); + st.unkeep(); + mem.unkeep(); + + op.unkeep(); // A narrow Store may have folded away the expression's conversion. if( postfix(ch) ) load.unkeep(); - else { - load = new LoadNode(loc(),name,1,mem(),expr,off); - load.setDef(0,ctrl()); - load = peep(load); - } - load.keep(); - op.unkill(); - } else if( !name.equals("[]") && match("(") ) { - Node result=functionCall(load,expr,name); - off.unkill(); - return parsePostfix(require(result,")")); - } else load.keep(); - expr.unkill(); - off.unkill(); - return parsePostfix(load.unkeep()); - } - - private void unknownStore(String name, Node ptr, Node off, Node value) { - checkPublication(value); - Node st=new StoreNode(loc(),name,1,Type.BOTTOM,mem(),ptr,off,value,false); - st.setDef(0,ctrl()); - mem(st.peephole()); - } - - // Explicit self.x and an unqualified field name use the same local slot. - // The map also keeps self.x distinct from a parameter/local named x. - private Node parsePrivateField(String name) { - Var v=_fieldVars.get(name); - if( v==null ) throw error("Cannot resolve field '"+name+"'"); - if( v.type() instanceof TypeFunPtr ctor && ctor.ret() instanceof TypeMem ) - throw error("A constructor is only called by new"); - if( matchOpx('=','=') ) { - checkPrivateWrite(name); - Node value=parseAsgn().keep(); - checkPublication(value); - _scope.update(v,liftExpr(value,v.type(),false)); - return value.unkeep(); + return pre==0 && postfix(ch) ? load : op; } - Node value=_scope.in(_scope.update(v,null)); - if( value instanceof ConstantNode && (value._type==Type.BOTTOM || value._type==Type.TOP) ) - throw error("Cannot read uninitialized field '"+name+"'"); - char ch=_lexer.matchOperAssign(); - if( ch!=0 ) { - checkPrivateWrite(name); - Node op=opAssign(ch,value,v.type()); - checkPublication(op); - _scope.update(v,op); - return postfix(ch) ? value.unkeep() : op; - } - if( value._type instanceof TypeFunPtr && match("(") ) - return parsePostfix(require(functionCall(value,self()),")")); - return parsePostfix(value); + off.unkill(); + // Might be a method call, so pass expr as 'self' + return parsePostfixMethod(load,expr.unkeep()); } - private void checkPrivateWrite(String name) { - Field field=((TypeMemPtr)TYPES.get(_nestedType))._obj.field(name); - if( field!=null && field._one ) - throw error("Cannot reassign shared function '"+name+"'"); - } // zero/sign extend. "i" is limited to either classic unsigned (min==0) or // classic signed (min=minus-power-of-2); max=power-of-2-minus-1. @@ -2089,15 +2555,9 @@ public class Parser { Ary<Type> ts = new Ary<>(Type.class); Ary<String> ids = new Ary<>(String.class); java.util.BitSet finals = new java.util.BitSet(); - - // Defined in constructor? Add `self` argument. "static" calls still - // add a `self` they just ignore it. - if( _scope._kinds.last() instanceof Kind.Define define ) { - // Upgrade defined struct to latest fields - TypeMemPtr tmp = (TypeMemPtr)TYPES.get(define._tmp._obj._name); - ts .push(tmp); - ids.push("self"); - } + Type self = implicitSelfType(); + ts.push(self); + ids.push(self == TypePtr.PTR ? "#self" : "self"); // Parse other arguments _lexer.skipWhiteSpace(); @@ -2113,9 +2573,44 @@ public class Parser { } require("->"); // Make a concrete function type, with a fidx - TypeFunPtr tfp = _code.makeFun(TypeFunPtr.make(false,false,ts.asAry(),Type.BOTTOM)); + TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),Type.BOTTOM,_code.fidx(_ref._cname)); ReturnNode ret = parseFunctionBody(tfp,loc,finals,ids.asAry()); - return new FunPtrNode(ret._fun.sig(),ret).peephole(); + return new FunPtrNode(tfp,_code._start,ret).peephole(); + } + + private Node constructorFunc(String name) { + require("{"); + Ary<Type> ts = new Ary<>(Type.class); + Ary<String> ids = new Ary<>(String.class); + java.util.BitSet finals = new java.util.BitSet(); + TypeStruct self = _scope.constructorSelf(); + assert self != null; + ts.push(TypeMemPtr.make(constructorRecv(self))); + ids.push("self"); + // Explicit constructors use the same hidden allocation arguments as + // <init>: public memory is the call memory edge (index 1), followed by + // self (index 2) and the private, unescaped object memory (index 3). + ts.push(TypeMem.makePrivate(self)); + ids.push("#selfMem"); + + _lexer.skipWhiteSpace(); + Lexer loc = loc(); + while( true ) { + Type t = type(); + if( t==null ) break; + finals.set(ids.size(),bindingFinal(t)); + String id = requireId(); + ts .push(t ); + ids.push(id); + match(","); + } + require("->"); + TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),TypeMem.makePrivate(self),_code.fidx(_ref._cname)); + Node[] preParse = _scope.save(); + ReturnNode ret = _parseFunctionBody((self._name+"."+name).intern(),tfp,loc,finals,ids.asAry()); + _scope.restore(preParse); + require("}"); + return new FunPtrNode(tfp,_code._start,ret).peephole(); } /** @@ -2125,35 +2620,38 @@ public class Parser { * </pre> */ private Node functionCall(Node fcn, Node self) { - return functionCall(fcn,self,null); - } - private Node functionCall(Node fcn, Node self, String method) { + Lexer site = loc(); + site._position--; // Opening '('; arguments can contain nested calls. if( fcn._type == Type.NIL ) throw error("Calling a null function pointer"); - if( !(fcn instanceof FRefNode) && fcn._type!=Type.BOTTOM && fcn._type!=TypeScalar.BOT && !fcn._type.isa(TypeFunPtr.BOT) ) - throw error("Expected a function but got "+fcn._type.glb(false).str()); fcn.keep(); // Keep while parsing args Ary<Node> args = new Ary<>( Node.class ); args.push(null); // Space for ctrl,mem args.push(null); - if( self != null ) checkPublication(self); if( self != null ) // Method call has a self - args.push(self); + args.push(self.keep()); while( !peek(')') ) { Node arg = parseAsgn(); if( arg==null ) break; - checkPublication(arg); args.push(arg.keep()); if( !match(",") ) break; } // Control & memory after parsing args args.set(0,ctrl().keep()); - args.set(1,_scope.mem().keep()); + args.set(1,mem().keep()); args.push(fcn); // Function pointer // Unkeep them all for( Node arg : args ) arg.unkeep(); + return functionCall(args,site); + } + + private Node functionCall( Ary<Node> args ) { + return functionCall(args,loc()); + } + + private Node functionCall( Ary<Node> args, Lexer site ) { // Dead into the call? Skip all the node gen if( ctrl()._type == Type.XCONTROL ) { for( Node arg : args ) @@ -2163,33 +2661,18 @@ public class Parser { } // Into the call - CallNode call = new CallNode(loc(), args.asAry()); - call._method=method; - call=(CallNode)call.peephole(); + CallNode call = (CallNode)new CallNode(site, args.asAry()).peephole(); // Post-call setup - CallEndNode cend = (CallEndNode)new CallEndNode(call).peephole(); + CallEndNode cend = (CallEndNode)new CallEndNode(call,TypeRPC.constant(_code.rpc(_ref._cname))).peephole(); call.peephole(); // Rerun peeps after CallEnd, allows early inlining + cend = (CallEndNode)cend.keep().peephole(); // TODO: Might inline // Control from CallEnd ctrl(new CProjNode(cend,0,ScopeNode.CTRL).peephole()); - // A call may update any alias, so its memory result is the new whole state. - _scope.mem(new ProjNode(cend,1,ScopeNode.MEM0).peephole()); + // Memory from CallEnd + Node mem = new ProjNode(cend,1,ScopeNode.MEM0).peephole(); + mem(mem); // Call result - return new ProjNode(cend,2,"#2").peephole(); - } - - private Node callWithArgs(Node fptr, Node[] args) { - Node[] inputs=new Node[args.length+3]; - inputs[0]=ctrl(); inputs[1]=mem(); - System.arraycopy(args,0,inputs,2,args.length); - inputs[inputs.length-1]=fptr; - CallNode call=new CallNode(loc(),inputs).init(); - CallEndNode end=new CallEndNode(call).init().keep(); - if( fptr._type instanceof TypeFunPtr sig && _code.link(sig)!=null ) call.link(_code.link(sig)); - ctrl(new CProjNode(end,0,ScopeNode.CTRL).peephole()); - mem(new ProjNode(end,1,ScopeNode.MEM0).peephole()); - Node result=new ProjNode(end,2,"#2").peephole(); - end.unkeep(); - return result; + return new ProjNode(cend.unkeep(),2,"#2").peephole(); } // Just after parsing "type foo = " can parse `"C"` @@ -2201,10 +2684,27 @@ public class Parser { return false; } // External linked constant - private Node externDecl( String ex, Type t ) { - if( t instanceof TypeFunPtr tfp ) // Generic TFP from type parse - t = _code.makeFun(tfp); // Get a FIDX, becomes a constant - return new ExternNode(t,ex).peephole(); + private ConstantNode externDecl( String ex, Type t ) { + if( t instanceof TypeFunPtr tfp ) { // Generic TFP from type parse + int fidx = _code.fidx(_ref._cname); // Get a FIDX + tfp = tfp.makeFrom(fidx); // Become a constant + _code.externFunc(fidx,ex); // Map fidx to extern name + t = tfp; + } + return (ExternNode)(new ExternNode(t,ex).peephole()); + } + + // 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) { + if( !(t instanceof TypeInteger) && !(t instanceof TypeFloat) ) + throw error("C data binding requires an integer or float type"); + String cname = ("C:"+name).intern(); + TypeStruct ts = TypeStruct.make(cname,false,Field.make(name,t,_code.externAlias(name),false,true)); + Type old = TYPES.putIfAbsent(cname,ts); + if( old!=null && old!=ts ) + throw error("Conflicting types for C data '"+name+"'"); + return con(TypeMemPtr.make((byte)2,ts,true)); } /** @@ -2212,13 +2712,36 @@ public class Parser { * * <pre> * integerLiteral: [1-9][0-9]* | [0] - * floatLiteral: [digits].[digits]?[e [digits]]? + * floatLiteral: [digits].[digits]?[e [digits]]?[f]? * </pre> */ private ConstantNode parseLiteral() { return con(_lexer.parseNumber()); } - public static Node con( long con ) { return con==0 ? ZERO : con(TypeInteger.constant(con)); } - public static ConstantNode con( Type t ) { return (ConstantNode)new ConstantNode(t).peephole(); } - public static ConFldOffNode off( TypeStruct base, String fname ) { return (ConFldOffNode)(new ConFldOffNode(base._name,fname).peephole()); } + ConstantNode con( long con ) { return _code.con(con); } + ConstantNode con( Type t ) { return _code.con(t ); } + private void classInit(Lexer site, Node ptr) { + // Ordinary load machinery resolves the flag alias and offset when + // the receiver is still a forward reference. + Field flag = ptr._type instanceof TypeMemPtr tmp ? tmp._obj.field("$init") : null; + int alias = flag==null ? 1 : flag._alias; + Node ready = new LoadNode(site,"$init",alias,null,mem(),ptr,fldoff(ptr,"$init")).peephole(); + CLInitNode init = new CLInitNode(site,ctrl(),mem(),ptr,ready).peephole().keep(); + ctrl(new CProjNode(init,0,ScopeNode.CTRL).peephole()); + Node bulk = new ProjNode(init,1,ScopeNode.MEM0).peephole().keep(); + flag = init.flag(); + mem(flag==null ? bulk : mergeAlias(bulk,flag._alias, + new ProjNode(init,2,memName(flag._alias)).peephole())); + bulk.unkeep(); + init.unkeep(); + } + + // Field offset for a known type (but unknown offset) + public static ConFldOffNode off( TypeStruct ts, String fname ) { + return (ConFldOffNode)(new ConFldOffNode(ts,fname).peephole()); + } + // Field offset for an unknown type + public Node fldoff( Node n, String name ) { + return new FldOffNode(n,name,loc()).peephole(); + } public Node peep( Node n ) { // Peephole, then improve with lexically scoped guards return _scope.upcastGuard(n.peephole()); @@ -2268,6 +2791,14 @@ public class Parser { return require(con(TypeInteger.constant(c)),"'"); } + private boolean noLowerCase(String s) { + for( int i=0; i<s.length(); i++ ) + if( Character.isLowerCase(s.charAt(i)) ) + return false; + return true; + } + + ////////////////////////////////// // Utilities for lexical analysis @@ -2332,18 +2863,23 @@ public class Parser { // Input buffer; an array of text bytes read from a file or a string private final byte[] _input; + public final CompUnit _cu; // Original source, retained across inlining. // Tracks current position in input buffer private int _position = 0; - public boolean before(Lexer that) { - return that!=null && _input==that._input && _position<that._position; - } - // private int _line_number = 1; // Start of current line private int _line_start = 0; + /** True when this location precedes {@code that} in the same source. */ + public boolean before(Lexer that) { + return that != null && (_cu!=null ? _cu==that._cu : _input==that._input) && _position < that._position; + } + + public String position() { return _line_number+":"+(_position-_line_start+1); } + public int pos() { return _position; } + /** * Record the source text for lexing */ @@ -2356,13 +2892,28 @@ public class Parser { */ public Lexer(byte[] buf) { _input = buf; + _cu = null; } + private Lexer(CompUnit cu) { + _input = cu._src.getBytes(); + _cu = cu; + } + + // Imported location: source text is only needed when reporting an error. + private Lexer(CompUnit cu, int pos) { + _input = null; + _cu = cu; + _position = pos; + } + + /** * Copy a lexer from a lexer */ private Lexer(Lexer l) { _input = l._input; + _cu = l._cu; _position = l._position; _line_number = l._line_number; _line_start = l._line_start; @@ -2371,7 +2922,7 @@ public class Parser { // Very handy in the debugger, shows the unparsed program @Override public String toString() { - return new String(_input, _position, _input.length - _position); + return _input==null ? _cu._cname+"@"+_position : new String(_input, _position, _input.length - _position); } // True if at EOF @@ -2490,7 +3041,7 @@ public class Parser { // Used for errors String getAnyNextToken() { - if (isEOF()) return ""; + if (isEOF()) return "EOF"; if (isIdStart(peek())) return parseId(); if (isNumber(peek())) return parseNumberString(); if (isPunctuation(peek())) return parsePunctuation(); @@ -2504,17 +3055,26 @@ public class Parser { private Type parseNumber() { 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 error("Syntax error: integer values cannot start with '0'",this); long i = Long.parseLong(new String(_input,old,len)); return TypeInteger.constant(i); } - return TypeFloat.constant(Double.parseDouble(new String(_input,old,-len))); + return TypeFloat.constant(Double.parseDouble(new String(_input,old,n))); } private String parseNumberString() { int old = _position; int len = Math.abs(isLongOrDouble()); + if( peek()=='f' ) { + nextChar(); + len++; + } return new String(_input,old,len); } @@ -2544,7 +3104,7 @@ public class Parser { private String parseId() { int start = _position; while (isIdLetter(nextChar())) ; - return new String(_input, start, --_position - start); + return new String(_input, start, --_position - start).intern(); } // @@ -2579,9 +3139,9 @@ public class Parser { } } - ParseException errorSyntax(String syntax) { return _errorSyntax("expected "+syntax); } + ParseException errorSyntax(String syntax) { return _errorSyntax("expected `"+syntax+"`"); } private ParseException _errorSyntax(String msg) { - return error("Syntax error, "+msg+": " + _lexer.getAnyNextToken()); + return error("Syntax error, "+msg+" but found `" + _lexer.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/src/main/java/com/seaofnodes/simple/Var.java b/src/main/java/com/seaofnodes/simple/Var.java index e13b85c5..af371d21 100644 --- a/src/main/java/com/seaofnodes/simple/Var.java +++ b/src/main/java/com/seaofnodes/simple/Var.java @@ -1,6 +1,7 @@ package com.seaofnodes.simple; import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; /** @@ -13,9 +14,12 @@ public class Var { // promotions. public int _idx; // index in containing scope private Type _type; // Declared type - public boolean _init; // Explicit declaration initializer public boolean _final; // Final field public boolean _fref; // Forward ref + // Parser-only definite-initialization marker. The current binding's + // nullable type carries the path-sensitive state through SSA merges. + public boolean _uninit; + public int _extern; // C storage alias; zero for ordinary variables public Parser.Lexer _loc; // Source location public Var(int idx, String name, Type type, boolean xfinal, Parser.Lexer loc ) { @@ -29,14 +33,8 @@ public class Var { _fref = fref; _loc = loc; } - // Raw declared type, without resolving a forward reference while printing. - public Type _type() { return _type; } - public Type type() { - if( !_type.isFRef() ) return _type; - // Update self to no longer use the forward ref type - Type def = Parser.TYPES.get(((TypeMemPtr)_type)._obj._name); - return (_type=_type.meet(def)); - } + public Type type() { return _type; } + public Type _type() { return _type; } // ScopeNode printer needs to have no side effects // Forward reference variables (not types) must be BOTTOM and // distinct from inferred variables @@ -51,6 +49,6 @@ public class Var { } @Override public String toString() { - return _type.toString()+(_final ? " ": " !")+_name; + return (_fref ? "FREF " : "")+(_uninit ? "UNINIT " : "")+_type.toString()+(_final ? " ": " !")+_name; } } diff --git a/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java b/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java index 2319714b..0b314978 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java +++ b/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java @@ -15,11 +15,16 @@ abstract public class BuildLRG { // since it will not color, just split the conflicted ranges now). Returns // true if no hard-conflicts, although we still might not color. public static boolean run(int round, RegAlloc alloc) { - for( Node bb : alloc._code._cfg ) + FunNode fun = null; + for( Node bb : alloc._code._cfg ) { + if( bb instanceof FunNode fun2 ) + fun = fun2; for( Node n : bb.outs() ) { // A synthetic never-exit value has no runtime definition. if( n._type==Type.TOP ) continue; if( n instanceof PhiNode phi && !(phi._type instanceof TypeMem) ) { + if( phi instanceof MachNode mach && mach.outregmap()==null ) + continue; // All real Phi inputs end up with the same LRG. // Pass 1: find any pre-existing LRG, to avoid make-then-Union a LRG LRG lrg = alloc.lrg(phi); @@ -28,8 +33,8 @@ abstract public class BuildLRG { if( (lrg = alloc.lrg(phi.in(i))) != null ) break; // If none, make one. - if( lrg==null ) lrg = alloc.newLRG(n); - if( phi instanceof MachNode ) defLRG(alloc,n); + if( lrg==null ) lrg = alloc.newLRG(n,fun); + if( phi instanceof MachNode ) defLRG(alloc,n,fun); // Pass 2: everybody uses the same LRG lrg=alloc.union(lrg,phi); for( int i=phi instanceof ParmNode ? 2 : 1; i<n.nIns(); i++ ) @@ -51,7 +56,7 @@ abstract public class BuildLRG { } // Define live range - defLRG(alloc,n); + defLRG(alloc,n,fun); // Now, look in the opposite direction. How are incoming // LRGs affected by this node: For all uses, make live lrgs @@ -71,8 +76,9 @@ abstract public class BuildLRG { if( n instanceof MultiNode && !(n instanceof CFGNode) ) for( Node proj : n.outs() ) if( proj instanceof MachNode ) - defLRG(alloc,proj); + defLRG(alloc,proj,fun); } + } // Collect live ranges alloc.unify(); @@ -80,12 +86,12 @@ abstract public class BuildLRG { return alloc.success(); } - private static void defLRG( RegAlloc alloc, Node n ) { + private static void defLRG( RegAlloc alloc, Node n, FunNode fun ) { MachNode mach = (MachNode)n; RegMask def_mask = mach.outregmap(); if( def_mask == null ) return; LRG lrg = mach.twoAddress() == 0 - ? alloc.newLRG(n) // Define a new LRG for N + ? alloc.newLRG(n,fun) // Define a new LRG for N : alloc.lrg2(n,mach.twoAddress()); // Use the matching 2-adr input // Record mask and mach if( !lrg.machDef(mach,def_mask.size1()).and(def_mask) ) diff --git a/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java b/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java new file mode 100644 index 00000000..6c70b71d --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java @@ -0,0 +1,214 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.util.*; + +// Search for Class Initialization Cycles. + +// Done after Opto, so dead code does not make a cycle. + +// Build the inits-graph, a call-graph plus CLInits plus class parents. + +// (1) Scan all functions f, building _edges[f][]: the set of <cinit> fcns from +// CompUnits from CLInitNodes, and the set of fcn call targets in f and the +// class parent. These are all reported as fidxs (ints). +// +// (2) Walk the _edges graph using Tarjans SCC. Reject cycles containing more +// than one <clinit>. +// +// (3) Report on <clinit> cycles +// +public class ClassInitDependencies { + private final CodeGen _code; + // Function FIDX -> callees, touched initializers, and parent initializer. + public final int[][] _edgess; + private final int[] _pres; + private final int[] _lows; + private final int[] _stk ; + private int _sp; + + + // Build once; tests can inspect the graph independently of cycle checking. + public ClassInitDependencies( CodeGen code ) { + _code = code; + assert code._phase.ordinal() >= CodeGen.Phase.IterPostOpto.ordinal(); + _pres = new int[code._linker._len]; + _lows = new int[code._linker._len]; + _stk = new int[code._linker._len]; + + // + BitSet visit = code.visit(); // Visit set per-fcn + Ary<CFGNode> work = new Ary<>(CFGNode.class); // Worklist per-fcn + IntHashMap iset = new IntHashMap(0); // dedup hash set per-fcn + _edgess = new int[code._linker._len][]; + + // For all functions, build the _edges array + for( FunNode fun : code._linker ) + if( fun!=null && !fun.isDead() ) + // Visit function and collect direct class touches and callees + _edgess[fun.sig().fidx()] = walk(code,fun,work,iset,visit,null); + visit.clear(); + } + + // Return the outgoing init edge set for this function + private static int[] walk( CodeGen code, FunNode fun, Ary<CFGNode> work, IntHashMap iset, BitSet visit, + Ary<Parser.Lexer> sites) { + // Walk function body, looking for outgoing init edges + assert iset .isEmpty(); // int hash set for de-dup + assert visit.isEmpty(); // needs a visit set, because loops + assert work .isEmpty(); // needs a worklist, because DAG, parallel explore paths + + // Children classes always dependent on parents + if( fun.isClz() && fun._compunit._par != null ) + iset.put(code.link(fun._compunit._par).sig().fidx(),1); + + work.push(fun); + while( !work.isEmpty() ) { + CFGNode cfg = work.pop(); + if( visit.get(cfg._nid) ) continue; // Been there, done that + visit.set(cfg._nid); + assert !cfg._type.isHigh(); + // Do not walk off end of function + if( cfg instanceof ReturnNode ) continue; + + // CLInit adds a class init requirement to this function + // (plus all parent classes) + if( cfg instanceof CLInitNode init ) { + FunNode clz = code.link(init.cu()); // <clinit> referred to + iset.put(clz.sig().fidx(),1); // Add to edge set + if( sites!=null && sites.atX(clz.sig().fidx())==null ) sites.setX(clz.sig().fidx(),init._loc); + } + + // Calls union all their fptr fidxs into the callee set + if( cfg instanceof CallNode call && call.fptr()._type instanceof TypeFunPtr fp ) { + assert !XInt.isHigh(fp.fidxs()) && fp.nfcns()>0; // Call is alive with valid targets + // Union call targets into the callee set + for( int dstx = XInt.next(fp.fidxs(),0); dstx>=0; dstx=XInt.next(fp.fidxs(),dstx) ) { + iset.put(dstx,1); + if( sites!=null && sites.atX(dstx)==null ) sites.setX(dstx,call._loc); + } + } + + // Forward recursion to walk the whole function + for( Node use : cfg._outputs ) { + // Do not walk into another function + if( !(use instanceof CFGNode cfg1) || use instanceof FunNode || use instanceof StopNode ) continue; + // Expect control flow edge is sensible + assert (use instanceof RegionNode r && r._inputs.find(cfg)>0) || use.in(0)==cfg; + if( !visit.get(cfg1._nid) ) // Small speed opt + work.add(cfg1); + } + } + + visit.clear(); + int[] edges = iset.isEmpty() ? null : iset.keySet(); + iset.clear(); + return edges; + } + + // Return an error string, or null if no cycle contains multiple clinits. + public String check() { + Arrays.fill(_pres,0); + _sp = 0; + _pres[0] = 1; // Preorders stored in pres[0] and start at 1 + + int idx = -1; + for( int fidx = 1; fidx < _edgess.length; fidx++ ) + if( _pres[fidx] == 0 && // Already visited + (idx = scc(fidx)) != -1 ) // And found a cycle + return report(idx); // Report cycle + return null; + } + + // Every internal edge of an SCC belongs to a cycle. Print all of them: + // stack order is not a path, and enumerating all cycle paths can explode. + // This bookkeeping and formatting are only needed on an error. + private String report(int idx) { + BitSet members = new BitSet(); + for( int i=idx; i<_sp; i++ ) members.set(_stk[i]); + SB sb = new SB().p("Cyclic class initialization:"); + // Recover one surviving source site per edge only after finding an + // error. A representative touch is enough; runtime first-touch order + // need not be statically known. Lexer snapshots retain the source CU + // even when the node has been inlined into a different function. + Ary<CFGNode> work = new Ary<>(CFGNode.class); + IntHashMap iset = new IntHashMap(0); + BitSet visit = new BitSet(); + Ary<Parser.Lexer> sites = new Ary<>(Parser.Lexer.class); + for( int i=idx; i<_sp; i++ ) { + int src = _stk[i]; + FunNode fun = _code.link(src); + Arrays.fill(sites._es,null); + sites.clear(); + walk(_code,fun,work,iset,visit,sites); + if( _edgess[src]!=null ) + for( int dst : _edgess[src] ) + if( members.get(dst) ) { + sb.p("\n ").p(name(src)).p(" -> ").p(name(dst)); + Parser.Lexer loc = sites.atX(dst); + if( loc!=null && loc._cu!=null ) + sb.p('\n').p(SourceError.format(loc._cu,loc.pos(),"cyclic class initialization")); + else if( fun.isClz() && fun._compunit._par==_code.link(dst)._compunit ) + sb.p("\n parent initialization requirement"); + } + } + return sb.toString(); + } + + private String name(int fidx) { + FunNode fun = _code.link(fidx); + String owner=fun._compunit._cname; + if( fun.isClz() ) return owner; + String label=fun.label(); + return (label.startsWith(owner+".") ? label : owner+"."+label)+" [fidx "+fidx+"]"; + } + + // Tarjan SCC + int scc( int fidx ) { + // Assign pre-order number + _lows[fidx] = _pres[fidx] = _pres[0]++; + _stk[_sp++] = fidx; + // walk outbound edges + if( _edgess[fidx]!=null ) { + for( int edge : _edgess[fidx] ) { + if( edge >= _pres.length ) continue; // Out of bounds happens when calling C fcns + if( _pres[edge] == 0 ) { // Not visited + int idx = scc(edge); + if( idx != -1 ) return idx; // Found a cycle + _lows[fidx] = Math.min(_lows[fidx],_lows[edge]); + } else if( _edgess[edge]!=null && onStk(edge) ) { + _lows[fidx] = Math.min(_lows[fidx],_pres[edge]); + } + } + } + // Found a cycle + if( _lows[fidx] == _pres[fidx] ) { + int idx = find(fidx); + if( hasClzCycle(idx) ) + return idx; // Found a class cycle + // Pop stack, including fidx + _sp = idx; + } + return -1; // No cycle + } + + private int find(int e) { + for( int i=0; i<_sp; i++ ) if( _stk[i] == e ) return i; + return -1; + } + private boolean onStk(int e) { return find(e) != -1; } + + private boolean hasClzCycle(int idx) { + int clinits = 0; + for( int i=idx; i<_sp; i++ ) { + FunNode fun = _code.link(_stk[i]); + if( fun!=null && fun.isClz() && ++clinits > 1 ) + return true; + } + return false; + } + +} diff --git a/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java index d3895b08..e6bace8c 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java +++ b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -1,120 +1,322 @@ package com.seaofnodes.simple.codegen; import com.seaofnodes.graph.GraphObserver; - import com.seaofnodes.simple.IterPeeps; import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.print.*; import com.seaofnodes.simple.type.*; -import com.seaofnodes.simple.util.Ary; -import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; -import java.io.IOException; +import com.seaofnodes.simple.util.*; +import java.io.File; import java.util.*; + +// Compile Driver for a Compilation Unit @SuppressWarnings("unchecked") public class CodeGen { - public GraphObserver<Node> _obs; - public static final String PORTS = "com.seaofnodes.simple.node.cpus"; - // Last created CodeGen as a global; used all over to avoid passing about a - // "context". + + // Last created CodeGen as a global; used all over to avoid passing about a "context". public static CodeGen CODE; + // Location of built-in CPU ports + public static final String PORTS = "com.seaofnodes.simple.node.cpus"; + + // Compile phases public enum Phase { - Parse, // Build ideal IR from source - Iter, // Pessimistic peepholes - Opto, // Optimistic interprocedural SCCP + Parse, // Parse ASCII text into Sea-of-Nodes IR + Iter, // Pessimistic peepholes after parse + Opto, // SCCP, numeric modes, seed the peephole worklist IterPostOpto, // Peepholes and inlining using SCCP results - FinalizeIR, // Finish function cleanup and layouts - TypeCheck, // Check the surviving ideal graph - LoopTree, // Discover loops and connect nonreturning paths - Select, // Select machine instructions - Schedule, // Global code motion - LocalSched, // Order instructions within blocks - RegAlloc, // Assign registers and stack slots - Encoding, // Encode instructions - Export; // Link in memory or write ELF - + FinalizeIR, // Field layout, offset cleanup, function/CU roots + TypeCheck, // Last check for bad programs + Lower, // Expand checked class initialization to ordinary IR + LoopTree, // Build a loop tree; break infinite loops + Select, // Convert to target hardware nodes + Schedule, // Global schedule (code motion) nodes + LocalSched, // Local schedule + RegAlloc, // Register allocation + Encoding, // Encoding + Export, // Export + LastPhase // After last phase } public Phase _phase; + // Null for normal compilation; owned by this compilation, not a global viewer. + public GraphObserver<Node> _obs; + + /** True when tests deliberately randomize Iter worklist order. */ + public static boolean iterSeedOverridden() { return System.getProperty("simple.iter.seed") != null; } + + // Expensive assert sampling: + // -Dsimple.assert.expensive=0 disables these checks + // -Dsimple.assert.expensive=1 checks every step + // -Dsimple.assert.expensive=N checks once every 1<<N steps + private static final int EXPENSIVE_ASSERT_LOG = Integer.getInteger("simple.assert.expensive",8); + public static boolean expensiveAssert() { return EXPENSIVE_ASSERT_LOG > 0; } + public static boolean expensiveAssert( int trip ) { + if( EXPENSIVE_ASSERT_LOG <= 0 ) return false; + if( EXPENSIVE_ASSERT_LOG == 1 ) return true; + int shift = Math.min(EXPENSIVE_ASSERT_LOG,30); + return (trip & ((1<<shift)-1)) == 0; + } + + // --------------------------- + // Module Source Root + public final String _modDir; + // Module Build Root + public final String _buildDir; + // Slashed path from source root to the entry file, without ".smp". + public final String _srcName; + + // Only available for tests, otherwise source comes from the CompUnit + private final String _src; + + // Current Working Directory; default module base + private final String _cwd; + + // Source files discovered in this invocation. All live initializers + // participate in cycle checking, including fully inlined bodies. + public HashMap<String,CompUnit> _compunits; + + // Test shortcut for only one compilation unit + public CompUnit compunit() { + assert _compunits.size()==1; + for( CompUnit cu : _compunits.values() ) + return cu; + assert false : "no compilation unit"; + return null; + } + + // True when fidx is published through a public class or instance field. + public boolean publicFIDX(int fidx) { + if( fidx < LocalIds.RESERVED ) return false; + // A public class initializer is itself an external root; unlike + // constructors and methods, it is not reached through a class field. + FunNode fun = _linker.atX(fidx); + if( fun != null && fun.isClz() && fun.isPublic() ) + return true; + // Include instance methods even while their constant function fields + // are stored in each instance rather than on the class object. + for( Type t : Parser.TYPES.values() ) + if( t instanceof TypeStruct clz && + Parser.startsClzPrefix(clz._name) && + Parser.isPublicClassName(clz._name) && + resolvedClass(clz) ) { + if( publicFieldFIDX(clz,fidx) ) return true; + Type self = Parser.TYPES.get(clz._name.substring(Parser.CLZ.length())); + if( self instanceof TypeStruct ts && publicFieldFIDX(ts,fidx) ) return true; + } + return false; + } + + private static boolean publicFieldFIDX(TypeStruct clz, int fidx) { + boolean hasCtor = clz.field("<ctor>") != null; + for( Field fld : clz._fields ) + if( fld._fname.charAt(0)!='_' && fld._t instanceof TypeFunPtr tfp && + // With a user constructor, class:N.N is the private <init>. + !(hasCtor && clz.hiddenInit(fld)) && + XInt.bit(tfp._fidxs,fidx) ) + return true; + return false; + } + + public static boolean hasUserConstructor(TypeStruct self) { + Type t = Parser.TYPES.get(Parser.addClzPrefix(self._name)); + return t instanceof TypeStruct clz && clz.field("<ctor>") != null; + } + + // A forward reference gets a class object and constructor signature so + // parsing can proceed, but it has no callable definition yet. + private static boolean resolvedClass(TypeStruct clz) { + Type t = Parser.TYPES.get(clz._name.substring(Parser.CLZ.length())); + return !(t instanceof TypeStruct ts) || !ts._fref; + } + + private int[] _publicFIDXs, _publicAliases; + + /** + * Freeze the externally visible portion of the program after ParseAll has + * loaded every compilation unit and closed/upgraded all types. Later + * optimization can remove linker entries and create inline-only fidxs; + * neither operation changes this interface. + */ + void freezePublicInterface() { + assert _publicFIDXs==null && _publicAliases==null; + int[] fidxs = XInt.EMPTY; + for( int fidx=LocalIds.RESERVED; fidx<_linker._len; fidx++ ) + if( publicFIDX(fidx) ) + fidxs = XInt.make(fidxs,fidx); + int[] aliases = XInt.EMPTY; + for( Type t : Parser.TYPES.values() ) + if( t instanceof TypeStruct clz && + Parser.startsClzPrefix(clz._name) && + Parser.isPublicClassName(clz._name) && + resolvedClass(clz) ) { + boolean hasCtor = clz.field("<ctor>") != null; + for( Field fld : clz._fields ) + if( fld._fname.charAt(0)!='_' && fld._t!=Type.TOP && + !(hasCtor && clz.hiddenInit(fld)) ) + aliases = XInt.make(aliases,fld._alias); + } + _publicFIDXs = fidxs; + _publicAliases = aliases; + } + + public int[] publicFIDXs() { assert _publicFIDXs !=null; return _publicFIDXs; } + public int[] publicAliases(){ assert _publicAliases!=null; return _publicAliases; } + public boolean publicInterfaceFrozen() { return _publicFIDXs != null; } - // Run through target, skipping phases already completed. - public CodeGen driver(Phase target) { return driver(target,null,null); } - public CodeGen driver(Phase target, String cpu, String callingConv) { return driver(target,cpu,callingConv,0); } - public CodeGen driver(Phase target, String cpu, String callingConv, int dump) { - if( target.ordinal()>=Phase.RegAlloc.ordinal() && cpu==null && _mach==null ) - throw new IllegalArgumentException("Machine code requires a CPU target"); - if( target==Phase.Export ) throw new IllegalArgumentException("Use exportELF with an output file or null for memory linking"); - step(target, Phase.Parse, this::parse, dump); - step(target, Phase.Iter, this::iter, dump); - step(target, Phase.Opto, this::opto, dump); + // --------------------------- + + // Very common nodes, cached here + public final ConstantNode ZERO; + public final XCtrlNode XCTRL; + + public Node constant( Type t ) { + assert _phase==null || _phase.ordinal() <= Phase.Lower.ordinal(); + return ConstantNode.make(t).peephole(); + } + public ConstantNode con( Type t ) { return (ConstantNode)constant(t); } + public ConstantNode con( long con ) { return con==0 ? ZERO : con(TypeInteger.constant(con)); } + + + // --------------------------- + // Test setup; no module nor file with specific argument + public CodeGen( String src ) { this(src, 126L, true ); } + + // Test setup; no module nor file; can alter seed & argument; can re-run same CodeGen + public CodeGen( String src, Type arg ) { + this(null,null,null, src, 126L, true, arg); + } + // Test setup; no module nor file; can alter seed & argument; can re-run same CodeGen + public CodeGen( String src, long workListSeed, boolean resetTypes ) { + this(null,null,null, src, workListSeed, resetTypes, TypeInteger.BOT); + } + + // Generic CodeGen, including full module setup + public CodeGen( String modDir, String buildDir, + String srcName, String src, long workListSeed, boolean resetTypes, Type arg ) { + // Public singleton to avoid passing about this state to a huge count + // of places. Probably becomes a TLS at some point. + CODE = this; + if( resetTypes ) Type.reset(); // Reset to recover from failed test + + _cwd = System.getProperty("user.dir")+"/"; + _modDir = modDir == null ? _cwd : modDir; + _buildDir = buildDir == null ? _cwd : buildDir; + _srcName = srcName; + _callingConv = null; // Calling convention + // Source code from test strings, not files + _src = src; + // All the compilation units + _compunits = new HashMap<>(); + _phase = null; + // Start GVN table + _gvn = new HashMap<>(); + // Allow whole-suite worklist-order sweeps without rewriting the many + // explicit seeds used by tests and command-line compilation. + String iterSeed = System.getProperty("simple.iter.seed"); + _iter = new IterPeeps(iterSeed == null ? workListSeed : Long.parseLong(iterSeed)); + // End points of graph + _stop = new StopNode().init(); + _start = new StartNode(null,_stop,arg).init(); + ZERO = con(TypeInteger.ZERO).keep(); + XCTRL = new XCtrlNode().peephole().keep(); + P = new Parser(this); + } + + + // Run the requested phases. ELF output contains machine code and data. + + // No code emission, just IR generation + public CodeGen driver( Phase phase ) { return driver(phase,null,null,false,false,0); } + // No object file writing, but code generation for a specific cpu/os pair (allows emulation) + public CodeGen driver( Phase phase, String cpu, String callingConv ) { + if( phase.ordinal() < Phase.Export.ordinal() ) + // No code outputted + return driver(phase,cpu,callingConv,false,false,0); + if( _srcName == null ) + throw new RuntimeException("No source filename provided, so do not know how to name the obj file"); + boolean emitEntrySymbol = !_srcName.contains("."); + return driver( phase, cpu, callingConv, false, emitEntrySymbol, 0 ); + } + // Write an object file for a specific cpu/os pair + + public CodeGen driver( String cpu, String callingConv, boolean inMemory, boolean emitEntrySymbol ) { return driver(Phase.Export,cpu,callingConv,inMemory,emitEntrySymbol,0); } + + // The Generic driver + public CodeGen driver( Phase target, String cpu, String callingConv, boolean inMemory, boolean emitEntrySymbol, int dump ) { + // Parse the entry source and discover all referenced source files. + step(target, Phase.Parse, this::parse, dump); + // Run peepholes to a fixed point + step(target, Phase.Iter, this::iter, dump); + // Run SCCP (and resolve int/flt expressions) + step(target, Phase.Opto, this::opto, dump); + // Run peepholes to a fixed point again, on the better types. step(target, Phase.IterPostOpto, this::iterPostOpto, dump); - step(target, Phase.FinalizeIR, this::finalizeIR, dump); - step(target, Phase.TypeCheck, this::typeCheck, dump); - step(target, Phase.LoopTree, this::loopTree, dump); + // Field layouts; field symbols become constant offsets + step(target, Phase.FinalizeIR, this::finalizeIR, dump); + // Final error check - bad programs fail here + step(target, Phase.TypeCheck, this::typeCheck, dump); + // Replace "fat" nodes with their exploded explicit IR; this can + // introduce CFG diamonds + step(target, Phase.Lower, this::lower, dump); + // Compute the loop-tree, used for approximating exec frequencies for + // scheduling and register allocation + step(target, Phase.LoopTree, this::loopTree, dump); + // Instruction selection step(target, Phase.Select, () -> instSelect(cpu,callingConv), dump); - step(target, Phase.Schedule, this::GCM, dump); - step(target, Phase.LocalSched, this::localSched, dump); - step(target, Phase.RegAlloc, this::regAlloc, dump); - step(target, Phase.Encoding, this::encode, dump); + // Global Code Motion + step(target, Phase.Schedule, this::GCM, dump); + // Local (basic block only) scheduling + step(target, Phase.LocalSched, this::localSched, dump); + // Register allocation + step(target, Phase.RegAlloc, this::regAlloc, dump); + // Encode machine instructions into bits + step(target, Phase.Encoding, this::encode, dump); + // Export machine code and data into one ELF file + step(target, Phase.Export, () -> exportELF(inMemory,emitEntrySymbol), dump); return this; } + // The driver names the order; optional phases decide locally whether to run. private void step(Phase target, Phase next, Runnable work, int dump) { - if( next.ordinal()>target.ordinal() || (_phase!=null && _phase.ordinal()>=next.ordinal()) ) return; + if( next.ordinal() > target.ordinal() || (_phase!=null && _phase.ordinal()>=next.ordinal()) ) return; Phase prior = _phase; work.run(); - if( _phase!=prior && (dump & (1<<_phase.ordinal()))!=0 ) { - if( (dump & (1<<30))!=0 ) System.err.println("After "+_phase+":"); - System.err.println(this); - } + if( _phase!=prior ) dump(dump); } - public final long[] _times = new long[Phase.values().length]; - private CodeGen phase(Phase next, boolean ordered, Runnable work) { + // All phase entry points share ordering checks, timing, and viewer notification. + // Record elapsed time even on failure, but report completion only on success. + CodeGen phase(Phase next, boolean ordered, Runnable work) { assert ordered : "Cannot run "+next+" after "+_phase; _phase = next; long t0 = System.currentTimeMillis(); - try { work.run(); } - finally { _times[next.ordinal()] = System.currentTimeMillis()-t0; } - if( _obs!=null ) _obs.phase(next.name()); + work.run(); // Do The Phase + _times[next.ordinal()] = System.currentTimeMillis() - t0; + if( _obs!=null ) _obs.phase(next.name()); // Record for viewer return this; } - - // --------------------------- - // Compilation source code - public final String _src; - // Compile-time known initial argument type - public final TypeInteger _arg; - - // --------------------------- - public CodeGen( String src ) { this(src, TypeInteger.BOT, 123L, true ); } - public CodeGen( String src, TypeInteger arg) { this(src, arg==null ? TypeInteger.BOT : arg, 123L, true ); } - - public CodeGen( String src, TypeInteger arg, long workListSeed, boolean reset ) { - CODE = this; - if( reset ) Type.reset(); - _main = makeFun(TypeFunPtr.MAIN); - _phase = null; - _callingConv = null; - _start = new StartNode(arg); - _stop = new StopNode(src); - _start.setDef(1,_stop); - _src = src; - _arg = arg; - _iter = new IterPeeps(workListSeed); - P = new Parser(this,arg); + public String entryClinitName() { + return Parser.addClzPrefix(_srcName==null ? "Test" : _srcName.replace('/','.'))+".<clinit>"; } - - // Run all the phases through final ELF emission - public CodeGen driver( String cpu, String callingConv, String obj ) throws IOException { - return driver(Phase.Encoding,cpu,callingConv).exportELF(obj); + // Verbose printing during compilation + private void dump(int dump) { + int p2 = _phase.ordinal(); + if( (dump & (1<<p2)) == 0 ) return; + if( (dump & (1<<30)) != 0 ) + System.err.println("After "+_phase+":"); + System.err.println(_phase.ordinal() >= Phase.LocalSched.ordinal() + ? asm(new SB()).toString() + : IRPrinter.prettyPrint(this)); } + // Record times by phase + public final long[] _times = new long[Phase.LastPhase.ordinal()]; + // --------------------------- /** * A counter, for unique node id generation. Starting with value 1, to @@ -126,9 +328,30 @@ public class CodeGen { return _uid++; } - // Next available memory alias number - private int _alias = 2; // 0 is for control, 1 for memory - public int getALIAS() { return _alias++; } + + // Allocate dense alias/function/call IDs within this invocation. + // Class slots let a forward reference reserve an initializer's identity. + public final LocalIds _aliases = new LocalIds(); + public int alias(String clz) { return _aliases.next(clz); } + + // C storage has one global alias per symbol, including across object files. + public int externAlias(String name) { return _aliases.next("C:"+name,LocalIds.RESERVED); } + + // External data addresses supplied by an in-memory linker (image offsets). + public final HashMap<String,Integer> _externDataAddresses = new HashMap<>(); + + // Compute local function index (FIDX) from global function info. This is + // called *in order* during parsing, and that order is part of the global + // unique mapping + public final LocalIds _fidxs = new LocalIds(); + // Return local index for specific global file & order. Used to find fidxs for e.g. FREF class <init> fcns + public int fidx( String clz, int order ) { return _fidxs.next(clz, order); } + // New fidx in the current file + public int fidx( String clz ) { return fidx(clz, -1); } + + // Compute local RPC index from global RPC info, one per call + public final LocalIds _rpcs = new LocalIds(); + public int rpc( String clz ) { return _rpcs.next(clz); } // Inlining can insert CFG edges and invalidate cached depth ordering. @@ -143,41 +366,35 @@ public class CodeGen { public final BitSet _visit = new BitSet(); public BitSet visit() { assert _visit.isEmpty(); return _visit; } - // Start and stop; end points of the generated IR + // Start and Stop; end points of the generated IR public StartNode _start; public StopNode _stop; - // Native calls are exposure roots even on paths with no live Return. - public final Ary<CallNode> _calls = new Ary<>(CallNode.class); // Global Value Numbering. Hash over opcode and inputs; hits in this table // are structurally equal. - public final HashMap<Node,Node> _gvn = new HashMap<>(); + public final HashMap<Node,Node> _gvn; - // Source of unique function indices - private int _fidx =0; - public TypeFunPtr makeFun( TypeFunPtr fun ) { - int fidx = _fidx++; - return fun.makeFrom(fidx); - } - // Signature for MAIN - public TypeFunPtr _main; - public void setMain(FunNode main) { - _main = main.sig(); - link(main); - } // Reverse from a constant function pointer to the IR function being called. // Error to call with a non-constant TFP public FunNode link( TypeFunPtr tfp ) { return link(tfp.fidx()); } - // Read-only diagnostic lookup; preserve stale entries. - public FunNode _link(TypeFunPtr tfp) { return _link(tfp.fidx()); } + // Read-only linker lookup, suitable for printers and debugger display. + // In particular, do not lazily remove a partially dead function. public FunNode _link(int fidx) { return _linker.atX(fidx); } - // Return the FunNode from a fidx, cleaning up dead entries. + public FunNode _link(TypeFunPtr tfp) { return _link(tfp.fidx()); } + // Return the FunNode from a fidx public FunNode link( int fidx ) { FunNode fun =_linker.atX(fidx); if( fun!=null && fun.isDead() ) { _linker.setX(fidx,null); fun = null; } return fun; } + // Return the <clinit> for a CompUnit + public FunNode link( CompUnit cu ) { + for( FunNode fun : _linker ) + if( fun!=null && fun._compunit == cu && fun.isClz() ) + return fun; + return null; + } // Insert linker mapping from constant function signature to the function // being called. @@ -189,32 +406,34 @@ public class CodeGen { // "Linker" mapping from constant TypeFunPtrs to heads of function. These // TFPs all have exact single fidxs and their return is wiped to BOTTOM (so // the return is not part of the match). - final Ary<FunNode> _linker = new Ary<>(FunNode.class); + public final Ary<FunNode> _linker = new Ary<>(FunNode.class); + public final Ary<CallNode> _calls = new Ary<>(CallNode.class); - // Native symbols belong to function identities, not to one ExternNode. - // Selection and spill copies can turn that node into an ordinary constant. - private final HashMap<TypeFunPtr,String> _externs = new HashMap<>(); - public void externFunc(TypeFunPtr tfp, String name) { _externs.put(tfp.makeFrom(Type.BOTTOM),name); } - public String externFunc(TypeFunPtr tfp) { return _externs.get(tfp.makeFrom(Type.BOTTOM)); } + // Extern function declarations; input is the FIDX assigned to the name. + final HashMap<Integer,String> _externFunc = new HashMap<>(); + public void externFunc(int fidx, String ex) { + assert !_externFunc.containsKey(fidx); + _externFunc.put(fidx,ex); + } + public String externFunc(int fidx) { return _externFunc.get(fidx); } + + public String funcName(int fidx ) { + FunNode fun = _link(fidx); + if( fun!=null ) return fun._name; + return externFunc(fidx); + } + public boolean owns(FunNode fun) { + return fun._compunit != null && fun._compunit._src != null; + } + + + // --------------------------- // Parser object public final Parser P; // Parse ASCII text into Sea-of-Nodes IR - public CodeGen parse() { - return phase(Phase.Parse, _phase==null, () -> { - P.parse(); - // Close the declarations as a group before replacing old forward - // shapes in the graph. A can mention B before B mentions A; both - // must use the same complete cycle, including through functions. - HashMap<String,Type> closed = new HashMap<>(); - for( var entry : Parser.TYPES.entrySet() ) - if( entry.getValue() instanceof TypeMemPtr ptr && !ptr._obj._open ) - closed.put(entry.getKey(),ptr.makeFrom(ptr._obj.close(Parser.TYPES))); - Parser.TYPES.putAll(closed); - _start.walk(n -> { n.upgradeType(Parser.TYPES); return null; }); - }); - } + public CodeGen parse() { return phase(Phase.Parse, _phase==null, () -> ParseAll.parse(this,_src)); } // --------------------------- @@ -228,37 +447,63 @@ public class CodeGen { public void iterCnt() { if( !_midAssert ) _iter_cnt++; } public void iterNop() { if( !_midAssert ) _iter_nop_cnt++; } - // Pessimistic peepholes reach a fixed point after parsing. - public CodeGen iter() { return phase(Phase.Iter, _phase==Phase.Parse, () -> _iter.iterate(this)); } + // Pessimistic peepholes after parsing. This lifts loaded and parsed + // types before the optimistic Opto pass resets types and lets them fall. + public CodeGen iter() { return phase(Phase.Iter, _phase==Phase.Parse, () -> Iter.run(this)); } - // SCCP computes types and queues changed nodes; Iter applies their rewrites. + // SCCP and numeric-mode resolution; leave cleanup work queued for Iter. public CodeGen opto() { return phase(Phase.Opto, _phase==Phase.Iter, () -> Opto.opto(this)); } + public CodeGen iterPostOpto() { return phase(Phase.IterPostOpto, _phase==Phase.Opto, () -> _iter.iterate(this)); } public CodeGen finalizeIR() { return phase(Phase.FinalizeIR, _phase==Phase.IterPostOpto, () -> FinalizeIR.run(this)); } + + // Root a surviving function without claiming that it has unknown callers. + void keepInCompUnit(FunNode fun) { + StopCUNode stop = fun._compunit._stop; + if( stop._inputs.find(fun.ret()) == -1 ) + add(stop).addDef(fun.ret()); + } public <N extends Node> N add( N n ) { return _iter.add(n); } public void addAll( Ary<Node> ary ) { _iter.addAll(ary); } + public void addAll( Node n ) { _iter.add(n); _iter.addAll(n._inputs); } // --------------------------- // Last check for bad programs public CodeGen typeCheck() { return phase(Phase.TypeCheck, _phase==Phase.FinalizeIR, () -> TypeCheck.run(this)); } + public static TypeStruct requiredStruct(Node n) { + // CNC: remove? cleanup? + if( n instanceof NewNode alloc ) return alloc._ts; + if( n instanceof ConFldOffNode off ) return off._ts; + if( n instanceof FldOffNode off && off.in(0)._type instanceof TypeMemPtr ptr ) return ptr._obj; + return null; + } + + // --------------------------- + // TypeCheck queued surviving CLInits during its existing error walk. + public CodeGen lower() { + return phase(Phase.Lower, _phase==Phase.TypeCheck, () -> _iter.iterate(this)); + } + // --------------------------- // Build the loop tree; break never-exit loops public CodeGen loopTree() { - return phase(Phase.LoopTree, _phase==Phase.TypeCheck, () -> { - // Build the loop tree, fix never-exit loops - _start.buildLoopTree(_start,_stop); - }); + return phase(Phase.LoopTree, _phase==Phase.Lower, () -> { _start.buildLoopTree(_linker,_stop); FinishCalls.run(this); }); } // --------------------------- + + + // --------------------------- // Code generation CPU target public Machine _mach; // Chosen calling convention (usually either Win64 or SystemV) public String _callingConv; + public String _cCallingConv; // Callee save registers public RegMask _callerSave; + public RegMask _cCallerSave; // All returns have the following inputs: // 0 - ctrl @@ -273,11 +518,11 @@ public class CodeGen { // Convert to target hardware nodes public CodeGen instSelect( String cpu, String callingConv ) { return instSelect(cpu,callingConv,PORTS); } public CodeGen instSelect( String cpu, String callingConv, String base ) { - if( cpu==null ) return this; - return phase(Phase.Select, _phase==Phase.LoopTree, - () -> new InstSelect(this).run(cpu,callingConv,base)); + return InstSelect.select(this,cpu,callingConv,base); } + // --------------------------- + // --------------------------- // Control Flow Graph in Reverse Post Order. @@ -285,17 +530,12 @@ public class CodeGen { // Global schedule (code motion) nodes public CodeGen GCM() { - return phase(Phase.Schedule, (_phase==Phase.LoopTree || _phase==Phase.Select), () -> { - GlobalCodeMotion.buildCFG(this); - - }); + return phase(Phase.Schedule, (_phase==Phase.LoopTree || _phase==Phase.Select), () -> GlobalCodeMotion.buildCFG(this)); } // --------------------------- // Local (basic block) scheduler phase, a classic list scheduler - public CodeGen localSched() { - return phase(Phase.LocalSched, _phase==Phase.Schedule, () -> ListScheduler.sched(this)); - } + public CodeGen localSched() { return phase(Phase.LocalSched, _phase==Phase.Schedule, () -> ListScheduler.sched(this)); } // --------------------------- @@ -308,11 +548,10 @@ public class CodeGen { }); } - // Human readable register name - public String reg(Node n) { return reg(n,null); } - public String reg(Node n, FunNode fun) { + // Human-readable register name + public String reg(Node n) { if( _phase.ordinal() >= Phase.RegAlloc.ordinal() ) { - String s = _regAlloc.reg(n,fun); + String s = _regAlloc.reg(n); if( s!=null ) return s; } return "N"+ n._nid; @@ -321,50 +560,50 @@ public class CodeGen { // --------------------------- // Encoding - public Encoding _encoding; // Encoding object - public void preEncode() { } // overridden by alternative ports - public CodeGen encode() { - return phase(Phase.Encoding, _phase==Phase.RegAlloc, () -> { - _encoding = new Encoding(this); - preEncode(); - _encoding.encode(); - }); - } + public Encoding _encoding; + public CodeGen encode() { return phase(Phase.Encoding, _phase==Phase.RegAlloc, () -> _encoding = new Encoding(this).encode()); } // --------------------------- // Exporting to external formats - public CodeGen exportELF(String fname) throws IOException { - assert _phase==Phase.Encoding; - _phase=Phase.Export; - long t0=System.currentTimeMillis(); - try { - if( fname == null ) new LinkMem(this).link(); // In memory patching - else new ElfFile(this).export(fname); // External ELF file - } finally { _times[Phase.Export.ordinal()]=System.currentTimeMillis()-t0; } - if( _obs!=null ) _obs.phase(Phase.Export.name()); - return this; + ElfWriter _elf; + public CodeGen exportELF( boolean inMemory, boolean emitEntrySymbol ) { + return phase(Phase.Export, _phase==Phase.Encoding, () -> { + if( inMemory ) new LinkMem(this).link(_encoding); + else _elf = new ElfWriter(this).export(emitEntrySymbol); + }); } + // --------------------------- + public boolean _asmLittle=true; - SB asm(SB sb) { return ASMPrinter.print(sb,this); } public String asm() { return asm(new SB()).toString(); } + SB asm(SB sb) { + ASMPrinter.print(sb,this); + return sb; + } // Testing shortcuts - public Node ctrl() { return _stop.ret().ctrl(); } - public Node expr() { return _stop.ret().expr(); } - public String print() { return _stop.print(); } + public Node ctrl() { return compunit()._stop.ret().ctrl(); } + public Node expr() { return compunit()._stop.ret().expr(); } + public String print() { return compunit()._stop.print(); } // Debugging helper @Override public String toString() { - return _phase.ordinal() > Phase.Schedule.ordinal() - ? IRPrinter._prettyPrint( this ) - : IRPrinter.prettyPrint(this); + if( _stop == null ) + return "No StopNode"; + return IRPrinter.prettyPrint(this); } // Debugging helper - public Node f(int idx) { return _stop.find(idx); } + public Node f(int idx) { + for( CompUnit cu : _compunits.values() ) { + Node n = (cu._stop == null ? _stop : cu._stop).find(idx); + if( n != null ) return n; + } + return null; + } String printCFG() { @@ -383,28 +622,4 @@ public class CodeGen { } System.out.println(); } - - public void print_as_hex() { - for (byte b : _encoding._bits.toByteArray()) { - System.out.print(String.format("%02X", b)); - } - System.out.println(); - } - - //// Debug purposes for now -// public CodeGen printENCODING() { -// ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); -// -// for(Node bb : CodeGen.CODE._cfg) { -// for(Node n: bb.outs()) { -// if(n instanceof MachNode) { -// ((MachNode) n).encoding(E); -// } -// } -// } -// -// print_as_hex(outputStream); -// // Get the raw bytes from the output stream -// return this; -// } } diff --git a/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java b/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java new file mode 100644 index 00000000..c8b0a82c --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java @@ -0,0 +1,98 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.util.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import static com.seaofnodes.simple.codegen.CodeGen.CODE; + +// A source file and its class. Parent links follow the source directory tree. +public class CompUnit { + public final CompUnit _par; // Parent, e.g. A/B + final String _fname; // Full slashed file name starting from module root, e.g. A/B/C + final public String _cname; // Full dotted class name starting from module root, e.g. A.B.C + final String _name; // Base name, e.g. C + final File _smp; // Simple source file, e.g. module_root/A/B/C.smp + final public String _src; // Source code, from file or test case + + public String sourceFile() { + return _smp!=null ? _smp.getAbsolutePath() : null; + } + + public TypeStruct _clz; // The one TypeStruct being published + + // _deps records units required for types, code, or class references, + // without requiring their initializers to run. These source + // dependencies may form a cycle. + Ary<CompUnit> _deps; + + // Per-Compilation-Unit Start/StopNodes, keeping alive all exported Nodes. + // Null means no code loaded (yet). + public StartCUNode _start; + public StopCUNode _stop ; + + + // Discovered source file. Source is loaded if required to parse, and null + // otherwise. + CompUnit( CompUnit par, String fname, String cname, String name ) throws IOException { + _par = par; + _fname= fname; + _cname= cname; + _name = name; + _smp = source(fname); + + _src = _smp.exists() ? Files.readString(_smp.toPath()) : null; + } + + // The bundled library is ordinary source. A local sys tree can replace it; + // this chapter never loads Simple code from an object file. + static File source(String fname) { + File file=new File(CODE._modDir,fname+".smp"); + if( file.exists() || !(fname.equals("sys") || fname.startsWith("sys/")) ) return file; + return new File("src/main/smp",fname+".smp"); + } + + // Test case. No source file. Source code passed in as a String. Fake + // class name. + CompUnit( String fname, String src ) { + _par = null; + _name = fname; + _fname= fname; + _cname= fname; + _smp = null; + _src = src; + } + + // Record source discovery without adding an initialization edge. + void addDep(CompUnit dep) { + if( _deps == null ) _deps = new Ary<>(CompUnit.class); + _deps.add(dep); + } + + public void addFun(CodeGen code, FunNode fun) { + assert fun.in(1)==_start; + _stop.addDef(fun.ret()); + code.add(_stop); + _stop.addDep(fun); + } + + void setStart(CodeGen code) { + _stop = new StopCUNode().init(); + _start = new StartCUNode(code._start,_stop, Type.BOTTOM, _fname).init(); + code._stop.addDef(_stop); + } + + @Override public String toString() { + String x = "{"+_cname; + if( _smp!=null && _smp.exists()) x += ",hasSMP"; + if( _src!=null ) x += ",hasSrc"; + x += "}"; + return x; + } +} diff --git a/src/main/java/com/seaofnodes/simple/codegen/ElfExpr.java b/src/main/java/com/seaofnodes/simple/codegen/ElfExpr.java deleted file mode 100644 index 9d9b8a1c..00000000 --- a/src/main/java/com/seaofnodes/simple/codegen/ElfExpr.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.seaofnodes.simple.codegen; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -public class ElfExpr { - public static void main(String[] args) { - try (FileOutputStream fos = new FileOutputStream("hello.o")) { - ByteBuffer buffer = ByteBuffer.allocate(512); - buffer.order(ByteOrder.LITTLE_ENDIAN); - - // === ELF HEADER (64 bytes) === - buffer.put(new byte[]{ - 0x7F, 'E', 'L', 'F', // ELF Magic - 2, 1, 1, 0, // 64-bit, little-endian, version - 0, 0, 0, 0, 0, 0, 0, // ABI padding - 2, 0, // Type: Executable (ET_EXEC) - 0x3E, 0x00, // Machine: x86-64 - 1, 0, 0, 0, // ELF version - 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Entry point - 0, 0, 0, 0, 0, 0, 0, 0, // Program header offset - 64, 0, 0, 0, 0, 0, 0, 0, // Section header offset - 0, 0, 0, 0, // Flags - 64, 0, // ELF header size - 0, 0, // Program header entry size - 0, 0, // Number of program header entries - 40, 0, // Section header entry size - 2, 0, // Number of section headers - 1, 0 // Section header string table index - }); - - // === .TEXT SECTION (Executable Code) === - byte[] textSection = { - // sys_write (write(1, message, 13)) - (byte) 0x48, (byte) 0xC7, (byte) 0xC0, 0x01, 0x00, 0x00, 0x00, // mov rax, 1 - (byte) 0x48, (byte) 0xC7, (byte) 0xC7, 0x01, 0x00, 0x00, 0x00, // mov rdi, 1 - (byte) 0x48, (byte) 0xBE, (byte)0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov rsi, message - (byte) 0x48, (byte) 0xC7, (byte) 0xC2, 0x0D, 0x00, 0x00, 0x00, // mov rdx, 13 - (byte) 0x0F, (byte) 0x05, // syscall - - // sys_exit (exit(0)) - (byte) 0x48, (byte) 0xC7, (byte) 0xC0, 0x3C, 0x00, 0x00, 0x00, // mov rax, 60 - (byte) 0x48, (byte) 0x31, (byte) 0xFF, // xor rdi, rdi - (byte) 0x0F, (byte) 0x05 // syscall - }; - - // Append section data - buffer.position(256); - buffer.put(textSection); - - // === STRING MESSAGE (.data Section) === - byte[] message = "Hello, World!\n".getBytes(); - buffer.position(400); - buffer.put(message); - - // === SECTION HEADERS === - buffer.position(64); - - // Null Section (required as first entry) - buffer.put(new byte[40]); - - // .text Section - buffer.putInt(1); // Offset in section string table (".text") - buffer.putInt(1); // Type: SHT_PROGBITS - buffer.putLong(6); // Flags: Executable | Allocatable - buffer.putLong(0); // Virtual address - buffer.putLong(256); // Offset to section data - buffer.putLong(textSection.length); // Section size - buffer.putInt(0); // Link - buffer.putInt(0); // Info - buffer.putLong(4); // Address alignment - buffer.putLong(0); // No extra entries - - // === SECTION HEADER STRING TABLE === - buffer.position(500); - buffer.put(".text".getBytes()); - buffer.put((byte) 0x00); // Null terminator - - fos.write(buffer.array()); - System.out.println("ELF object file 'hello.o' created."); - } catch (IOException e) { - e.printStackTrace(); - } - } -} diff --git a/src/main/java/com/seaofnodes/simple/codegen/ElfFile.java b/src/main/java/com/seaofnodes/simple/codegen/ElfFile.java deleted file mode 100644 index b6267600..00000000 --- a/src/main/java/com/seaofnodes/simple/codegen/ElfFile.java +++ /dev/null @@ -1,382 +0,0 @@ -package com.seaofnodes.simple.codegen; - -import com.seaofnodes.simple.*; -import com.seaofnodes.simple.codegen.Encoding.BAOS; -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.*; -import com.seaofnodes.simple.util.Ary; -import java.io.*; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.HashMap; -import java.util.Map; - -public class ElfFile { - - final CodeGen _code; - Ary<Section> _sections = new Ary<>(Section.class); - - ElfFile( CodeGen code ) { _code = code; } - - public static int writeCString(DataSection strtab, String name) { - // place name in the string table - int pos = strtab._contents.size(); - byte[] name_bytes = name.getBytes(); - strtab._contents.write(name_bytes, 0, name_bytes.length); - // the strings are null terminated - strtab._contents.write(0); - return pos; - } - - public static void write4( ByteArrayOutputStream bits, int op ) { - bits.write(op ); - bits.write(op>> 8); - bits.write(op>>16); - bits.write(op>>24); - } - public static void write8( ByteArrayOutputStream bits, long i64 ) { - write4(bits, (int) i64 ); - write4(bits, (int)(i64>>32)); - } - - public static final int SYMBOL_SIZE = 4+1+1+2+8+8; - - public static final int SYM_BIND_LOCAL = 0; - public static final int SYM_BIND_GLOBAL = 1; - public static final int SYM_BIND_WEAK = 2; - - public static final int SYM_TYPE_NOTYPE = 0; - public static final int SYM_TYPE_OBJECT = 1; - public static final int SYM_TYPE_FUNC = 2; - public static final int SYM_TYPE_SECTION = 3; - - public static class Symbol { - String _name; - int _name_pos; - - // index in the symbol table - int _index; - - // which section is it in - int _parent; - - // depends on the symbol type - // TODO - int _value, _size; - - // top 4bits are the "bind", bottom 4 are "type" - int _info; - - // bind 1011 - // 00001111 - // 10110000 - - Symbol(String name, int parent, int bind, int type) { - _name = name; - _parent = parent; - _info = (bind << 4) | (type & 0xF); - } - - void writeHeader(ByteBuffer out) { - out.putInt(_name_pos); // name - out.put((byte) _info); // info - // default visibility - out.put((byte) 0); // other - out.putShort((short) _parent); // shndx - out.putLong(_value); // value - out.putLong(_size); // size - } - } - - public static final int SHF_WRITE = 1; - public static final int SHF_ALLOC = 2; - public static final int SHF_EXECINSTR = 4; - public static final int SHF_STRINGS = 32; - public static final int SHF_INFO_LINK = 64; - - public static final int SECTION_HDR_SIZE = 64; - abstract public static class Section { - String _name; - int _index; - - int _type, _flags; - int _link, _info; - - // where's the name in the string table - int _name_pos; - int _file_offset; - - Section(String name, int type) { - _name = name; - _type = type; - } - - void writeHeader(ByteBuffer out) { - out.putInt(_name_pos); // name - out.putInt(_type); // type - out.putLong(_flags); // flags - out.putLong(0); // addr - out.putLong(_file_offset); // offset - out.putLong(size()); // size - out.putInt(_link); // link - out.putInt(_info); // info - out.putLong(16); // addralign - if (_type == 2) { - out.putLong(SYMBOL_SIZE);// entsize - } else if (_type == 4) { - out.putLong(24); // entsize - } else { - out.putLong(0); // entsize - } - } - - abstract void write(ByteBuffer out); - abstract int size(); - } - - public static class SymbolSection extends Section { - Ary<Symbol> _symbols = new Ary<>(Symbol.class); - Ary<Symbol> _loc = new Ary<>(Symbol.class); - - SymbolSection(String name, int type) { - super(name, type); - } - - @Override - void writeHeader(ByteBuffer out) { - // points to string table section - _link = 1; - - // number of non-local symbols - _info = _loc.len() + 1; - - super.writeHeader(out); - } - - void push(Symbol s) { - if ((s._info >> 4) != SYM_BIND_LOCAL) { - _symbols.push(s); - } else { - _loc.push(s); - } - } - - @Override - void write(ByteBuffer out) { - // Index 0 both designates the first entry in the table and serves as the undefined symbol index - for( int i = 0; i < SYMBOL_SIZE/4; i++ ) { - out.putInt(0); - } - // index is already set - for( Symbol s : _loc ) { - s.writeHeader(out); - } - for( Symbol s : _symbols ) { - s.writeHeader(out); - } - } - - @Override - int size() { - return (1 + _symbols.len() + _loc.len()) * SYMBOL_SIZE; - } - } - - public static class DataSection extends Section { - ByteArrayOutputStream _contents; - - DataSection(String name, int type) { - super(name, type); - _contents = new ByteArrayOutputStream(); - } - - DataSection(String name, int type, ByteArrayOutputStream contents) { - super(name, type); - _contents = contents; - } - - @Override - void write(ByteBuffer out) { - out.put(_contents.toByteArray()); - } - - @Override - int size() { - return _contents.size(); - } - } - - private void pushSection(Section s) { - _sections.push(s); - // ELF is effectively "1-based", due to the NULL section - s._index = _sections._len; - } - - /* creates function and stores where it starts*/ - private final HashMap<TypeFunPtr,Symbol> _funcs = new HashMap<>(); - private void encodeFunctions(SymbolSection symbols, DataSection text) { - for( int i=0; i<_code._cfg._len; i++ ) { - if( !(_code._cfg.at(i) instanceof FunNode fun) ) continue; - // skip until the function ends - while( !(_code._cfg.at(i) instanceof ReturnNode ret) ) - i++; - int end = _code._encoding._opStart[ret._nid] + _code._encoding._opLen[ret._nid]; - - if( fun._name==null ) continue; // Anonymous addresses are relocated within .text. - Symbol func = new Symbol(fun._name, text._index, SYM_BIND_GLOBAL, SYM_TYPE_FUNC); - func._value = _code._encoding._opStart[fun._nid]; - func._size = end - func._value; - symbols.push(func); - _funcs.put(fun.sig(), func); - } - } - - public void export(String fname) throws IOException { - DataSection strtab = new DataSection(".strtab", 3 /* SHT_SYMTAB */); - // first byte is reserved for an empty string - strtab._contents.write(0); - pushSection(strtab); - - // place all the symbols - SymbolSection symbols = new SymbolSection(".symtab", 2 /* SHT_STRTAB */); - pushSection(symbols); - - // zero flag by default - // we've already constructed this entire section in the encoding phase - DataSection text = new DataSection(".text", 1 /* SHT_PROGBITS */, _code._encoding._bits); - text._flags = SHF_ALLOC | SHF_WRITE | SHF_EXECINSTR; - pushSection(text); - - // Build and write constant pool - BAOS cpool = new BAOS(); - Encoding enc = _code._encoding; - enc.writeConstantPool(cpool,false); - DataSection rdata = new DataSection(".rodata", 1 /* SHT_PROGBITS */, cpool); - rdata._flags = SHF_ALLOC; - pushSection(rdata); - - // populate function symbols - encodeFunctions(symbols, text); - - int idx = 1; - for( Section s : _sections ) { - Symbol sym = new Symbol(s._name, idx++, SYM_BIND_LOCAL, SYM_TYPE_SECTION); - // we can reuse the same name pos from the actual section - sym._name_pos = s._name_pos; - sym._size = s.size(); - symbols.push(sym); - } - - // calculate local index - int num = 1; - for( Symbol s : symbols._loc ) - s._index = num++; - // extra space for .rela.text - int start_global = num+1; // Add one to skip the final .rela.text local symbol - for( Symbol a: symbols._symbols ) - a._index = start_global++; - - // create .text relocations - DataSection text_rela = new DataSection(".rela.text", 4 /* SHT_RELA */); - for( Node n : enc._externals.keySet()) { - int nid = n._nid; - String extern = enc._externals.get(n); - - Symbol sym = new Symbol(extern, 0, SYM_BIND_GLOBAL, SYM_TYPE_NOTYPE); - sym._index = start_global++; - symbols.push(sym); - - int offset = enc._opStart[nid] + enc._opLen[nid] - 4; - - // u64 offset - write8(text_rela._contents, offset); - // u64 info - write8(text_rela._contents, ((long)sym._index << 32L) | 4L /* PLT32 */); - // i64 addend - write8(text_rela._contents, -4); - } - - // Write relocations for the constant pool - for( Encoding.Relo relo : enc._bigCons.values() ) { - Symbol glob = new Symbol("CPOOL$"+start_global, rdata._index, SYM_BIND_GLOBAL, SYM_TYPE_FUNC); - glob._value = relo._target; - glob._size = relo._t.size(); - glob._index = start_global++; - symbols.push(glob); - write8(text_rela._contents, relo._opStart+relo._off); - write8(text_rela._contents, ((long)glob._index << 32L) | relo._elf ); - write8(text_rela._contents, -4); - } - - text_rela._flags = SHF_INFO_LINK; - text_rela._link = 2; - text_rela._info = text._index; - pushSection(text_rela); - - // Final .rela.text symbol - Symbol sym = new Symbol(text_rela._name, num++, SYM_BIND_LOCAL, SYM_TYPE_SECTION); - sym._name_pos = text_rela._name_pos; - sym._size = text_rela.size(); - symbols.push(sym); - - // populate string table - for( Section s : _sections ) { s._name_pos = writeCString(strtab, s._name); } - for( Symbol s : symbols._symbols ) { s._name_pos = writeCString(strtab, s._name); } - for( Symbol s : symbols._loc ) { s._name_pos = writeCString(strtab, s._name); } - - int size = 64; // ELF header - // size of all section data - for( Section s : _sections ) { - s._file_offset = size; - size += s.size(); - } - // headers go at the very end - int shdr_offset = size; - // there's a "null" section at the start of the section headers - size += SECTION_HDR_SIZE*(1 + _sections.len()); - - // System.out.printf("Hello %d, %d, %s\n", size, shdr_offset); - ByteBuffer out = ByteBuffer.allocate(size); - out.order(ByteOrder.LITTLE_ENDIAN); - - // ELF header - out.put(new byte[]{ 0x7F, 'E', 'L', 'F', 2, 1, 1, 0 }); - out.put(new byte[8]); - out.putShort((short)1 /* ET_REL */); // type - out.putShort((short)62 /* AMD64 */); // machine - out.putInt(1); // version - out.putLong(0); // entry - out.putLong(0); // phoff - out.putLong(shdr_offset); // shoff - out.putInt(0); // flags - out.putShort((short)64); // ehsize - out.putShort((short)0); // phentsize - out.putShort((short)0); // phnum - out.putShort((short)64); // shentsize - out.putShort((short) (1 + _sections.len())); // shnum - out.putShort((short)1); // shstrndx - - // raw section data - for( Section s : _sections ) { - s.write(out); - } - - // "null" section - for( int i = 0; i < 16; i++ ) { - out.putInt(0); - } - - for( Section s : _sections ) { - s.writeHeader(out); - } - assert out.position() == size; - - File file = new File(fname); - if( file.getParentFile()!=null ) - file.getParentFile().mkdirs(); - BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); - bos.write(out.array()); - bos.close(); - } - -} diff --git a/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java b/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java new file mode 100644 index 00000000..c52209dd --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java @@ -0,0 +1,460 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.BAOS; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; + +public class ElfWriter { + + final CodeGen _code; + Ary<Section> _sections; + StringSection _strtab; + + ElfWriter( CodeGen code ) { + _code = code; + } + + // ------------------------------------------------------------------------ + abstract public class Section { + static final int SHT_NULL = 0; + static final int SHT_PROGBITS= 1; + static final int SHT_SYMTAB = 2; + static final int SHT_STRTAB = 3; + static final int SHT_RELA = 4; + + final String _name; + final int _type, _entsize, _index, _name_pos, _flags; + int _link, _info; + int _file_offset; + + Section(String name, int type, int entsize, int flags) { + _name = name; + _type = type; + _entsize = entsize; + _flags = flags; + _index = _sections._len; + _sections.push(this); + _name_pos = name.isEmpty() + ? 0 // Null section + : _strtab == null + ? 1 // StringSection + // All the other sections + : _strtab.writeCString(name); + } + + void writeHeader(ByteBuffer out) { + out.putInt(_name_pos); // name + out.putInt(_type); // type + out.putLong(_flags); // flags + out.putLong(0); // addr + out.putLong(_file_offset); // offset + out.putLong(size()); // size + out.putInt(_link); // link + out.putInt(_info); // info + out.putLong(16); // addralign + out.putLong(_entsize); // Fixed-size element size + } + + abstract void write(ByteBuffer out); + abstract int size(); + } + + // ------------------------------------------------------------------------ + // Generic data sections; carries about a BAOS for bits + public class DataSection extends Section { + static final int SHF_WRITE = 1; + static final int SHF_ALLOC = 2; + static final int SHF_EXECINSTR = 4; + static final int SHF_INFO_LINK = 64; + + final BAOS _contents; + + DataSection( String name, int type, int flags, int entsize ) { this(name, type, flags, entsize, new BAOS() ); } + DataSection( String name, int type, BAOS contents, int flags ) { this(name, type, flags, 0, contents ); } + private DataSection( String name, int type, int flags, int entsize, BAOS contents ) { + super(name, type, entsize, flags); + _contents = contents; + } + + @Override void write(ByteBuffer out) { out.put(_contents.toByteArray()); } + @Override int size() { return _contents.size(); } + public void write2( int op ) { + _contents.write(op ); + _contents.write(op>> 8); + } + public void write4( int op ) { + _contents.write(op ); + _contents.write(op>> 8); + _contents.write(op>>16); + _contents.write(op>>24); + } + public void write8( long i64 ) { + write4( (int) i64 ); + write4( (int)(i64>>32)); + } + + } + + // ------------------------------------------------------------------------ + // Section to collect and de-dup strings + public class StringSection extends DataSection { + final HashMap<String,Integer> _dedup = new HashMap<>(); + + StringSection() { + super(".strtab",SHT_STRTAB, 0, 0 ); + int emptyoff = writeCString(""); // Always empty string at offset 0 + assert emptyoff == 0; + int namepos = writeCString(".strtab"); + assert namepos == _name_pos; + } + + public int writeCString(String name) { + if( name==null ) return 0; // Anonymous functions have no name + Integer ii = _dedup.get(name); + if( ii != null ) return ii; + // place name in the string table + int pos = _contents.size(); + _dedup.put(name,pos); + byte[] name_bytes = name.getBytes(); + _contents.write(name_bytes); + // the strings are null terminated + _contents.write(0); + return pos; + } + } + + // ------------------------------------------------------------------------ + // The relocations; 3 words per relocation + public class ReloSection extends DataSection { + ReloSection( int info ) { + this(".rela.text",info); + } + ReloSection(String name,int info) { + super(name, SHT_RELA, SHF_INFO_LINK, 24/*entsize*/); + _link = 2; + _info = info; + } + + void writeReloOffPlus(int offset, int symidx, byte relo) { + writeRelo(offset,symidx,relo,-4); + } + void writeRelo(int offset,int symidx,byte relo,long addend) { + assert symidx > 0; + write8(offset); + write8(((long)symidx << 32L) | relo); + write8(addend); + } + } + + + // ------------------------------------------------------------------------ + // Symbols section; emit the symbols into the data section directly + public class SymbolSection extends DataSection { + public static final int SYMBOL_SIZE = 4+1+1+2+8+8; + final BAOS _globals; + final int _nlocals; + + SymbolSection(int nlocals) { + super(".symtab", SHT_SYMTAB, 0/*flags*/, SYMBOL_SIZE); + _globals = new BAOS(); + _nlocals = nlocals; + } + + // Global symbol index + int gidx() { return _nlocals+(_globals.size()/SYMBOL_SIZE); } + + public int symbol( String name, int shndx, int bind, int type, long value, long size ) { + int gidx = gidx(); // Global index before I write to _globals + BAOS baos = bind==SYM_BIND_LOCAL ? _contents : _globals; + write4(baos,_strtab.writeCString(name)); + int info = (bind << 4) | (type & 0xF); + baos.write(info); + baos.write(0); // other/visibility + write2(baos,shndx); // section index + write8(baos,value); + write8(baos,size); + return bind==SYM_BIND_LOCAL ? -99 : gidx; + } + + public int symbol( String name, int shndx, int bind, int type ) { + return symbol(name,shndx,bind,type,0,0); + } + + // creates function and stores where it starts. + // Filters to just this class.obj file. + void encodeFunctions(StopNode stop, Encoding enc, int text_idx, boolean emitEntrySymbol) { + for( Node stopcu : stop._inputs ) { + for( Node ret : stopcu._inputs ) { + FunNode fun = ((ReturnNode)ret).fun(); + if( !_code.owns(fun) ) + continue; + int end = enc.opStart(fun.ret()) + enc.opLen(fun.ret()); + long value = enc.opStart(fun); + long size = end - value; + if( fun._name != null ) { // Anonymous functions have no name + // The C runtime owns `main` and adapts argc/argv into + // a normal Simple array. Keep the Simple function on + // its native ABI under a private entry symbol. + String name = emitEntrySymbol && "main".equals(fun._name) + ? "simple_main" + : fun._name; + symbol(name, text_idx, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, value, size); + } + } + } + } + + // Insert a default-main for top-level compilations, if the top-level + // does not declare a main. + void symbolEntryClinit(int text_idx, Encoding enc) { + FunNode clinit = null; + String entryName = _code.entryClinitName(); + for( FunNode fun : _code._linker ) + if( fun != null && _code.owns(fun) ) { + if( "main".equals(fun._name) ) + return; // encodeFunctions exported this as simple_main + if( entryName.equals(fun._name) ) + clinit = fun; // Keep top-level entry, if we don't find an explicit main + } + // No top-level main, so the top-level clinit becomes a default main + if( clinit != null ) { + assert enc.opStart(clinit) == 0; + symbol("simple_main",text_idx, SYM_BIND_GLOBAL, SYM_TYPE_FUNC, enc.opStart(clinit), 0); + return; + } + throw new IllegalStateException("Missing entry <clinit> "+entryName); + } + + static void write2( BAOS bits, int op ) { + bits.write(op ); + bits.write(op>> 8); + } + static void write4( BAOS bits, int op ) { + bits.write(op ); + bits.write(op>> 8); + bits.write(op>>16); + bits.write(op>>24); + } + static void write8( BAOS bits, long i64 ) { + write4(bits, (int) i64 ); + write4(bits, (int)(i64>>32)); + } + + @Override + void writeHeader(ByteBuffer out) { + // points to string table section + _link = _strtab._index; + _info = _nlocals; + super.writeHeader(out); + } + + @Override void write(ByteBuffer out) { + out.put(_contents.toByteArray()); + out.put(_globals .toByteArray()); + } + @Override int size() { + return _contents.size() + _globals.size(); + } + } + + public static final int SYM_BIND_LOCAL = 0; + public static final int SYM_BIND_GLOBAL = 1; + public static final int SYM_BIND_WEAK = 2; + + public static final int SYM_TYPE_NOTYPE = 0; + public static final int SYM_TYPE_OBJECT = 1; + public static final int SYM_TYPE_FUNC = 2; + public static final int SYM_TYPE_SECTION = 3; + + public static final int SECTION_HDR_SIZE = 64; + + // ------------------------------------------------------------------------ + + + // ------------------------------------------------------------------------ + + // Native code can live above 4G, outside our four-byte function pointers. + // Give address-taken natives a low entry which tail-jumps through the PLT. + // A weak, stable name makes references from separate objects agree on one + // entry. Do not patch references straight to the local copy: that would + // bypass the linker's choice and break pointer equality across objects. + private int nativeEntry(String name, HashMap<String,Integer> entries, + DataSection text, SymbolSection symbols, ReloSection relos) { + Integer old = entries.get(name); + if( old!=null ) return old; + int offset = text.size(); + text._contents.write(0xE9); // jmp rel32; preserve arguments and return address + text.write4(0); + int target = symbols.symbol(name,0,SYM_BIND_GLOBAL,SYM_TYPE_FUNC); + relos.writeReloOffPlus(offset+1,target,(byte)4/*PLT32*/); + int entry = symbols.symbol("__simple_native$"+name,text._index, + SYM_BIND_WEAK,SYM_TYPE_FUNC,offset,5); + entries.put(name,entry); + return entry; + } + + public ElfWriter export(boolean emitEntrySymbol) { + // Sections are created in the order they are emitted. + _sections = new Ary<>(Section.class); + + // Null section + Section nullSection = new DataSection("",Section.SHT_NULL,0,0); + + // String section next; written out early and available to record/dedup + // strings for all remaining sections + _strtab = new StringSection(); + + // place all the symbols + int nlocals = 1/*null*/+9/*number of sections*/; + SymbolSection symbols = new SymbolSection(nlocals); + + // we've already constructed these entire sections in the encoding phase + Encoding enc = _code._encoding; + // Append ELF-only thunks to a copy. Export must not change instruction + // offsets or the image used by the in-memory linker and emulators. + BAOS textBits = new BAOS(enc._bits.toByteArray(),enc._bits.size()); + DataSection text = new DataSection(".text" , Section.SHT_PROGBITS, textBits, DataSection.SHF_ALLOC | DataSection.SHF_WRITE | DataSection.SHF_EXECINSTR ); + // Constant pool + DataSection rodata = new DataSection(".rodata", Section.SHT_PROGBITS, enc._cpool, DataSection.SHF_ALLOC ); + // Static/global pool + DataSection rwdata = new DataSection(".data" , Section.SHT_PROGBITS, enc._sdata, DataSection.SHF_ALLOC | DataSection.SHF_WRITE ); + + + // populate function symbols + symbols.encodeFunctions(_code._stop, _code._encoding, text._index, emitEntrySymbol); + // Export the source compilation unit <clinit> as the C runtime "main". + // Encoding keeps this function at text offset zero. + if( emitEntrySymbol ) + symbols.symbolEntryClinit(text._index,enc); + + // create external .text relocations + ReloSection relocations = new ReloSection(text._index); + HashMap<String,Integer> nativeEntries = new HashMap<>(); + + for( Node n : enc._externals.keySet()) { + String extern = enc._externals.get(n); + boolean nativeAddress = n instanceof FunPtrNode && + _code.externFunc(((TypeFunPtr)n._type).fidx())!=null; + int symidx = nativeAddress + ? nativeEntry(extern,nativeEntries,text,symbols,relocations) + : symbols.symbol(extern,0,SYM_BIND_GLOBAL,SYM_TYPE_FUNC); + int offset = enc.opStart(n) + enc.opLen(n) - 4; + // Address materialization and static fields name the same thunk; + // direct calls still go straight to the native symbol's PLT entry. + relocations.writeReloOffPlus(offset,symidx,(byte)(nativeAddress ? 2/*PC32*/ : 4/*PLT32*/)); + } + + // Native ELF output is AMD64. Data addresses use PC32, not PLT calls. + for( Node n : enc._externData.keySet() ) { + String extern = enc._externData.get(n); + int symidx = symbols.symbol(extern, 0, SYM_BIND_GLOBAL, SYM_TYPE_OBJECT); + int offset = enc.opStart(n) + enc.opLen(n) - 4; + relocations.writeReloOffPlus(offset, symidx, (byte)2/*PC32*/); + } + + // Stable function identities are also addressable from data, including + // anonymous functions which have no source-level exported name. + HashMap<String,Integer> dataSymbols=new HashMap<>(); + for( FunNode fun : _code._linker ) + if( fun!=null && !fun.isDead() && _code.owns(fun) && enc.opStart(fun)>=0 ) { + String name=_code._fidxs.symbol(fun.sig().fidx()); + dataSymbols.put(name,symbols.symbol(name,text._index,SYM_BIND_GLOBAL,SYM_TYPE_FUNC,enc.opStart(fun),0)); + } + for( StaticData.ObjectData obj : enc._data.objects.values() ) { + int section=obj.external ? 0 : obj.readOnly ? rodata._index : rwdata._index; + dataSymbols.put(obj.symbol,symbols.symbol(obj.symbol,section,SYM_BIND_GLOBAL,SYM_TYPE_OBJECT, + obj.external ? 0 : obj.offset,obj.external ? 0 : obj.size)); + } + for( Encoding.Relo relo : enc._bigCons.values() ) { + StaticData.ObjectData obj=enc._data.object(relo._t); + relocations.writeReloOffPlus(relo._opStart+relo._off,dataSymbols.get(obj.symbol),relo._elf); + } + ReloSection roRelos=new ReloSection(".rela.rodata",rodata._index); + ReloSection rwRelos=new ReloSection(".rela.data",rwdata._index); + for( StaticData.Relocation relo : enc._data.relocations ) { + String name=enc._data.targetSymbol(relo.value()); + String nativeName=relo.value() instanceof TypeFunPtr fp ? _code.externFunc(fp.fidx()) : null; + Integer sym=nativeName==null ? dataSymbols.get(name) + : nativeEntry(nativeName,nativeEntries,text,symbols,relocations); + if( sym==null ) { + sym=symbols.symbol(name,0,SYM_BIND_GLOBAL,SYM_TYPE_NOTYPE); + dataSymbols.put(name,sym); + } + // AMD64 absolute 32/64-bit addresses; linker rejects overflow. + byte kind=(byte)(relo.logSize()==2 ? 10 : 1); + (relo.object().readOnly ? roRelos : rwRelos).writeRelo(relo.object().offset+relo.offset(),sym,kind,0); + } + + // Create a "Simple" section for Simple types and ir + + // Write the local section header symbols + assert nlocals == 1+_sections._len; + symbols.symbol("",0,0,0); // Null symbol at index 0 + for( Section s : _sections ) + symbols.symbol(s._name, s._index, SYM_BIND_LOCAL, SYM_TYPE_SECTION, 0, s.size()); + + int size = 64; // ELF header size + // size of all section data + for( Section s : _sections ) { + s._file_offset = size; + size += s.size(); + } + // headers go at the very end + int shdr_offset = size; + size += SECTION_HDR_SIZE*_sections.len(); + + ByteBuffer out = ByteBuffer.allocate(size); + out.order(ByteOrder.LITTLE_ENDIAN); + + // ELF header + out.put(new byte[]{ 0x7F, 'E', 'L', 'F', 2, 1, 1, 0 }); + out.put(new byte[8]); + out.putShort((short)1 /* ET_REL */); // type + out.putShort((short)62 /* AMD64 */); // machine + out.putInt(1); // version + out.putLong(0); // entry + out.putLong(0); // phoff + out.putLong(shdr_offset); // shoff + out.putInt(0); // flags + out.putShort((short)64); // ehsize + out.putShort((short)0); // phentsize + out.putShort((short)0); // phnum + out.putShort((short)64); // shentsize + out.putShort((short) _sections.len()); // shnum + out.putShort((short) _strtab._index); // shstrndx + + // raw section data + for( Section s : _sections ) + s.write(out); + + // Section headers + for( Section s : _sections ) + s.writeHeader(out); + + assert out.position() == size; + + String objName = _code._buildDir + "/" + _code._srcName + ".o"; + File file = new File(objName); + if( file.getParentFile()!=null ) + file.getParentFile().mkdirs(); + try { + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); + bos.write(out.array()); + bos.close(); + } catch( IOException ioe ) { + // Caller does not want to deal, but it is a failed compilation in any case + throw new RuntimeException(ioe); + } + // Reset for next ELF + _sections = null; + _strtab = null; + return this; + } + +} diff --git a/src/main/java/com/seaofnodes/simple/codegen/Encoding.java b/src/main/java/com/seaofnodes/simple/codegen/Encoding.java index 019e3892..06bf14f5 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/Encoding.java +++ b/src/main/java/com/seaofnodes/simple/codegen/Encoding.java @@ -1,12 +1,10 @@ package com.seaofnodes.simple.codegen; +import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.print.IRPrinter; import com.seaofnodes.simple.type.*; -import com.seaofnodes.simple.util.Ary; -import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; -import java.io.ByteArrayOutputStream; +import com.seaofnodes.simple.util.*; import java.util.*; @@ -24,11 +22,14 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { // Top-level program graph structure public final CodeGen _code; + // CFG Nodes in RPO order, per-encoding. + public Ary<CFGNode> _cfg; + // Instruction bytes. The registers are encoded already. Relocatable data // is in a fixed format depending on the kind of relocation. // - RIP-relative offsets into the same chunk are just encoded with their - // check relative offsets; no relocation info is required. + // chunk relative offsets; no relocation info is required. // - Fixed address targets in the same encoding will be correct for a zero // base; moving the entire encoding requires adding the new base address. @@ -36,17 +37,12 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { // - RIP-relative to external chunks have a zero offset; the matching // relocation info will be used to patch the correct value. - public static class BAOS extends ByteArrayOutputStream { - public byte[] buf() { return buf; } - void set( byte[] buf0, int count0 ) { buf=buf0; count=count0; } - }; - final public BAOS _bits = new BAOS(); - - public int [] _opStart; // Start of opcodes, by _nid - public byte[] _opLen; // Length of opcodes, by _nid - - // Function headers now padded when printing - public boolean _padFunHeads; + public final BAOS _bits = new BAOS(); // Instructions / encodings + public final BAOS _cpool = new BAOS(); // Constant r/o pool + public final BAOS _sdata = new BAOS(); // Static r/w pool + public final HashMap<Node,Relo> _bigCons = new HashMap<>(); + public final StaticData _data; + public final HashMap<String,Integer> _externalAddresses = new HashMap<>(); // Big Constant relocation info. public static class Relo { @@ -56,17 +52,28 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { public final int _structSize; // Capture layout during encoding, not printing public final byte _off; // Offset from start of opcode public final byte _elf; // ELF relocation type, e.g. 2/PC32 - public int _target; // Where constant is finally placed - public int _opStart; // Opcode start + public int _target; // Where constant is finally placed + public int _opStart; // Opcode start Relo( Node op, Type t, byte off, byte elf ) { _op=op; _t=t; _off=off; _elf=elf; _align = t.alignment(); _structSize = t instanceof TypeStruct ts ? ts.size() : 0; + _readOnly = readOnly(t); + } + public boolean readOnly() { return _readOnly; } + private final boolean _readOnly; + private static boolean readOnly(Type t) { + // Class initializers still write their fields, even when every + // final value is constant. Their storage must remain writable. + return !(t instanceof TypeStruct ts) || + (!Parser.startsClzPrefix(ts._name) && ts.isConstant()); } } - public final HashMap<Node,Relo> _bigCons = new HashMap<>(); - Encoding( CodeGen code ) { _code = code; } + Encoding( CodeGen code ) { + _code = code; + _data = new StaticData(this); + } // Shortcut to the defining register public short reg(Node n) { @@ -107,14 +114,6 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { } // Convenience for writing log-N - static void addN( int log, Type t, BAOS bits ) { - long x = t instanceof TypeInteger ti - ? ti.value() - : log==3 - ? Double.doubleToRawLongBits( ((TypeFloat)t).value()) - : Float.floatToRawIntBits((float)((TypeFloat)t).value()); - addN(log,x,bits); - } static void addN( int log, long x, BAOS bits ) { for( int i=0; i < 1<<log; i++ ) { bits.write((int)x); @@ -127,52 +126,95 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { public Encoding add4( int op ) { addN(2,op,_bits); return this; } public Encoding add8(long op ) { addN(3,op,_bits); return this; } + private final static int SENTINEL = -1; + private final IntHashMap _opStart = new IntHashMap(SENTINEL); + private final IntHashMap _opLen = new IntHashMap(SENTINEL); + + + // Read op start for n + public int opStart(Node n) { return _opStart.get(n._nid); } + // Read op length for n + public byte opLen (Node n) { return (byte)_opLen.get(n._nid); } + + // Set op start for n + public void opStart(Node n, int off) { + assert off != SENTINEL; + _opStart.put(n._nid,off); + } + + // Set op start for n + public void opStartAdd(Node n, int off) { + if( off==0 ) return; + int old = _opStart.get(n._nid); + assert old != SENTINEL && old+off != SENTINEL; + _opStart.put(n._nid,old+off); + } + + // Set op length for n + public void opLen(Node n, byte len) { + assert len != SENTINEL; + _opLen.put(n._nid,len); + } // Nodes need "relocation" patching; things done after code is placed. // Record src and dst Nodes. private final HashMap<Node,CFGNode> _internals = new HashMap<>(); // Source is a Call, destination in the Fun. public Encoding relo( CallNode call ) { - _internals.put(call,_code.link(call.tfp())); + FunNode fun = _code.link(call.tfp()); + if( fun != null && !_code.owns(fun) ) + _externals.put(call,fun._name); + else + _internals.put(call,fun); return this; } - public Encoding relo( ConstantNode con ) { - TypeFunPtr tfp = (TypeFunPtr)con._con; - // A native address has no local FunNode. Keep its symbol even after - // selection or a spill copy has replaced the original ExternNode. - String name = _code.externFunc(tfp); - if( name!=null ) return external(con,name); - _internals.put(con,_code.link(tfp)); + // Code address as a constant + public Encoding relo( FunPtrNode fptr ) { + TypeFunPtr tfp = (TypeFunPtr)fptr._type; + // Native functions have no local FunNode, but are still callable. + // Do not treat their addresses as identities of deleted functions. + String nativeName = _code.externFunc(tfp.fidx()); + if( nativeName!=null ) return external(fptr,nativeName); + FunNode fun = _code.link(tfp); + if( fun != null && !_code.owns(fun) ) + _externals.put(fptr,fun._name); + else + _internals.put(fptr,fun); return this; } + // Local jump public void jump( CFGNode jmp, CFGNode dst ) { _internals.put(jmp,dst.uctrlSkipEmpty()); } - - final HashMap<Node,String> _externals = new HashMap<>(); + // External references only located by Strings a link-time + public final HashMap<Node,String> _externals = new HashMap<>(); public Encoding external( Node n, String extern ) { _externals.put(n,extern); return this; } + public final HashMap<Node,String> _externData = new HashMap<>(); + public void externalData(Node n, String name) { _externData.put(n,name); } + // Store t as a 32/64 bit constant in the code space; generate RIP-relative - // addressing to load it + // addressing to load it. Type is stored in either the .rodata or .data. public void largeConstant( Node relo, Type t, int off, int elf ) { - assert t.isConstant(); + // TODO: Any-old struct in the cpool + assert t.isConstant() || (t instanceof TypeStruct ts && Parser.startsClzPrefix(ts._name)); assert (byte)off == off; assert (byte)elf == elf; _bigCons.put(relo,new Relo(relo,t,(byte)off,(byte)elf)); } // -------------------------------------------------- - void encode() { + Encoding encode( ) { // Basic block layout: negate branches to keep blocks in-order; insert // unconditional jumps. Attempt to keep backwards branches taken, // forwards not-taken (this is the default prediction on most // hardware). Layout is still Reverse Post Order but with more // restrictions. - basicBlockLayout(); + basicBlockLayout( ); // Write encoding bits in order into a big byte array. // Record opcode start and length. @@ -182,8 +224,13 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { // encodings and compact the code, changing all the offsets. compactShortForm(); + // Place pool objects and record relocations within their contents. + _data.write(); + // Patch RIP-relative and local encodings now. patchLocalRelocations(); + + return this; } // -------------------------------------------------- @@ -191,29 +238,48 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { // unconditional jumps. Attempt to keep backwards branches taken, forwards // not-taken (this is the default prediction on most hardware). Layout is // still Reverse Post Order but with more restrictions. - private void basicBlockLayout() { + private void basicBlockLayout( ) { IdentityHashMap<LoopNode,Ary<CFGNode>> rpos = new IdentityHashMap<>(); - Ary<CFGNode> rpo = new Ary<>(CFGNode.class); - rpos.put(_code._start.loop(),rpo); + _cfg = new Ary<>(CFGNode.class); + rpos.put(_code._start.loop(),_cfg); BitSet visit = _code.visit(); - rpo.add(_code._stop); - FunNode main = _code.link(_code._main); - for( Node n : _code._start._outputs ) - if( n instanceof FunNode fun && fun != main ) { - int x = rpo._len; - _rpo_cfg(fun, visit, rpos ); - assert rpo.at(x) instanceof ReturnNode; + + // Do them all except the <clinit>s + Ary<FunNode> clinits = new Ary<>(FunNode.class); + FunNode entry = null; + String entryName = _code.entryClinitName(); + for( FunNode fun : _code._linker ) { + if( fun != null && !fun.isDead() && _code.owns(fun) ) { + if( fun.isClz() ) { + if( fun._name.equals(entryName) ) + entry = fun; + else + clinits.add(fun); + } else { + int x = _cfg._len; + _rpo_cfg(fun,visit,rpos); + assert _cfg.at(x) instanceof ReturnNode; + } } - // Do any main function last... so it lands at offset 0 in the RPO - if( main!=null ) - _rpo_cfg(main, visit, rpos ); - rpo.add(_code._start); + } + // Now the <clinit>s last, so when reversed they become first. The + // program entry <clinit> is last of all, so it lands at offset zero. + for( FunNode clinit : clinits ) { + int x = _cfg._len; + _rpo_cfg(clinit,visit,rpos); + assert _cfg.at(x) instanceof ReturnNode; + } + if( entry != null ) { + int x = _cfg._len; + _rpo_cfg(entry,visit,rpos); + assert _cfg.at(x) instanceof ReturnNode; + } + // Reverse in-place - for( int i=0; i< rpo.size()>>1; i++ ) - rpo.swap(i,rpo.size()-1-i); + for( int i=0; i< _cfg.size()>>1; i++ ) + _cfg.swap(i,_cfg.size()-1-i); visit.clear(); - _code._cfg = rpo; // Save the new ordering } @@ -287,7 +353,7 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { private static boolean shouldInvert(CFGNode t, CFGNode f, int bld) { int tld = t.loopDepth(), fld = f.loopDepth(); // These next two are symmetric and can happen in any order; if `tld < - // bld` is true, the `fld < bld` must be false, or else both directions + // bld` is true, then `fld < bld` must be false, or else both directions // exit the loop... and the IF test would not be in the loop. // true to exit a loop usually (and false falls into Yet Another Loop @@ -332,24 +398,26 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { // -------------------------------------------------- // Write encoding bits in order into a big byte array. // Record opcode start and length. - public FunNode _fun; // Currently encoding function + + // Current function is used by the spill-op encodings to query the stack + // frame layout and get the spill offsets. private void writeEncodings() { - _opStart= new int [_code.UID()]; - _opLen = new byte[_code.UID()]; - for( CFGNode bb : _code._cfg ) { + for( CFGNode bb : _cfg ) { if( !(bb instanceof MachNode mach0) ) - _opStart[bb._nid] = _bits.size(); - else if( bb instanceof FunNode fun ) { - _fun = fun; // Currently encoding function - _opStart[bb._nid] = _bits.size(); + opStart(bb, _bits.size()); + else if( bb instanceof FunNode ) { + opStart(bb, _bits.size()); mach0.encoding( this ); - _opLen[bb._nid] = (byte) (_bits.size() - _opStart[bb._nid]); + opLen(bb, (byte) (_bits.size() - opStart(bb))); } for( Node n : bb._outputs ) { - if( n instanceof MachNode mach && !(n instanceof FunNode) ) { - _opStart[n._nid] = _bits.size(); + if( n instanceof MachNode mach && !(n instanceof FunNode) && + // FunPtrs are encoding in the block of their control, not + // the block of the ReturnNode input - which is also a CFG. + !(n instanceof FunPtrNode && bb instanceof ReturnNode) ) { + opStart(n, _bits.size()); mach.encoding( this ); - _opLen[n._nid] = (byte) (_bits.size() - _opStart[n._nid]); + opLen(n, (byte) (_bits.size() - opStart(n))); } } } @@ -359,10 +427,10 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { // Short-form RIP-relative support: replace short encodings with long // encodings and expand the code, changing all the offsets. private void compactShortForm() { - int len = _code._cfg._len; + int len = _cfg._len; int[] oldStarts = new int[len]; for( int i=0; i<len; i++ ) - oldStarts[i] = _opStart[_code._cfg.at(i)._nid]; + oldStarts[i] = opStart(_cfg.at(i)); // TODO: Rewrite this algo to use the small "_jmps" list of just the // jumps instead of walking all blocks. @@ -376,24 +444,27 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { while( slide != 0) { // While no fails slide = 0; for( int i=0; i<len; i++ ) { - CFGNode bb = _code._cfg.at(i); - _opStart[bb._nid] += slide; + CFGNode bb = _cfg.at(i); + opStartAdd(bb, slide); // Slide down all other (non-CFG) ops in the block for( Node n : bb._outputs ) - if( n instanceof MachNode && !(n instanceof CFGNode) ) - _opStart[n._nid] += slide; + if( n instanceof MachNode && !(n instanceof CFGNode) && + // Match writeEncodings(): FunPtrs are encoded in the + // block of their control, not again at the Return. + !(n instanceof FunPtrNode && bb instanceof ReturnNode) ) + opStartAdd(n, slide); if( bb instanceof RIPRelSize riprel ) { CFGNode target = (bb instanceof IfNode iff ? iff.cproj(0) : (CFGNode)bb.out(0)).uctrlSkipEmpty(); // Delta is from opStart to opStart. X86 at least counts // the delta from the opEnd, but we don't have the end until // we decide the size - so the encSize has to deal - int delta = _opStart[target._nid] - _opStart[bb._nid]; + int delta = opStart(target) - opStart(bb); byte opLen = riprel.encSize(delta); // Recorded size is smaller than the current size? - if( _opLen[bb._nid] < opLen ) { + if( opLen(bb) < opLen ) { // Start sliding the code down; record slide amount and new size - slide += opLen - _opLen[bb._nid]; - _opLen[bb._nid] = opLen; + slide += opLen - opLen(bb); + opLen(bb, opLen); } } } @@ -404,21 +475,21 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { // short-jumps span function headers, the padding will not make any // short jumps fail. for( int i=0; i<len; i++ ) { - CFGNode bb = _code._cfg.at(i); + CFGNode bb = _cfg.at(i); // Functions pad to align 16 if( bb instanceof FunNode ) { - int newStart = _opStart[bb._nid]+slide; + int newStart = opStart(bb)+slide; slide += (newStart+15 & -16)-newStart; } - _opStart[bb._nid] += slide; + opStartAdd(bb, slide); for( Node n : bb._outputs ) - if( n instanceof MachNode && !(n instanceof CFGNode) ) - _opStart[n._nid] += slide; + if( n instanceof MachNode && !(n instanceof CFGNode) && + !(n instanceof FunPtrNode && bb instanceof ReturnNode) ) + opStartAdd(n, slide); } - _padFunHeads = true; // Copy/slide the bits to make space for all the longer branches - int grow = _opStart[_code._cfg.at(len-1)._nid] - oldStarts[len-1]; + int grow = opStart(_cfg.at(len-1)) - oldStarts[len-1]; if( grow > 0 ) { // If no short-form ops, nothing to do here int end = _bits.size(); byte[] bits = new byte[end+grow]; @@ -426,7 +497,7 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { int start = oldStarts[i]; if( start==0 && i>1 ) continue; int oldStart = oldStarts[i]; - int newStart = _opStart[_code._cfg.at(i)._nid]; + int newStart = opStart(_cfg.at(i)); System.arraycopy(_bits.buf(),oldStart,bits,newStart,end-start); end = start; } @@ -441,103 +512,59 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { void patchLocalRelocations() { // Walk the local code-address relocations for( Node src : _internals.keySet() ) { - int start = _opStart[src._nid]; + int start = opStart(src); Node dst = _internals.get(src); - int target = _opStart[dst._nid]; - ((RIPRelSize)src).patch(this, start, _opLen[src._nid], target - start); + // If function is entirely dead, only the function pointer remains + // and, it can only be used to test against zero or equals to + // another function pointer... i.e., there Is No Code Here. + int target = dst == null ? start : opStart(dst); + ((RIPRelSize)src).patch(this, start, opLen(src), target - start); } } - // -------------------------------------------------- - // Write the constant pool into the BAOS and optionally patch locally - void writeConstantPool( BAOS bits, boolean patch ) { - padN(16,bits); - - // radix sort the big constants by alignment - Ary<Relo>[] raligns = new Ary[5]; - for( Node op : _bigCons.keySet() ) { - Relo relo = _bigCons.get(op); - int align = relo._t.alignment(); - Ary<Relo> relos = raligns[align]==null ? (raligns[align]=new Ary<>(Relo.class)) : raligns[align]; - relos.add(relo); - } - - - // Types can be used more than once; collapse the dups - HashMap<Type,Integer> targets = new HashMap<>(); - - // By alignment - for( int align = 4; align >= 0; align-- ) { - Ary<Relo> relos = raligns[align]; - if( relos == null ) continue; - for( Relo relo : relos ) { - // Map from relo to constant start and patch - Integer target = targets.get(relo._t); - if( target==null ) { - targets.put(relo._t,target = bits.size()); - // Write constant into constant pool - switch( relo._t ) { - case TypeTuple tt -> cpool(align,bits,tt); - case TypeStruct ts -> cpool(bits,ts); - // Simple primitive (e.g. larger int, float) - default -> addN(align,relo._t,bits); - } - } - // Record target address and opcode start - relo._target = target; - relo._opStart= _opStart[relo._op._nid]; - // Go ahead and locally patch in-memory - if( patch ) - ((RIPRelSize)relo._op).patch(this, relo._opStart, _opLen[relo._op._nid], relo._target - relo._opStart); - } - } - } - - // Constant tuples put all entries at same alignment - private void cpool(int align, BAOS bits, TypeTuple tt) { - for( Type tx : tt._types ) - addN(align,tx,bits); - } - - // Structs use internal field layout - private void cpool( BAOS bits, TypeStruct ts ) { - // Field order by offset - int[] layout = ts.layout(); - int off=0; // offset in the struct - for( int fn=0; fn<ts._fields.length; fn++ ) { - Field f = ts._fields[layout[fn]]; - int foff = ts. offset(layout[fn]); - // Pad up to field - while( off < foff ) { bits.write(0); off++; }; - // Constant array fields are special - if( f._fname=="[]" ) { // Must be a constant array - ((TypeConAry)f._t).write(bits); - off += ((TypeConAry)f._t).len(); - } else { - int log = f._t.log_size(); - addN(log,f._t,bits); - off += 1<<log; - } - } + // Emit a single scalar as bits + static void addN( int log, Type t, BAOS bits ) { + long x = switch( t ) { + case TypeInteger ti -> ti.value(); + case TypeFloat tf -> log==3 + ? Double.doubleToRawLongBits( tf.value()) + : Float.floatToRawIntBits((float)tf.value()); + case TypeFunPtr tfp -> 0; // These need to be relocated + case TypeMemPtr tmp -> 0; // These need to be relocated + default -> { if( t==Type.NIL ) yield 0; else throw Utils.TODO(); } + }; + addN(log,x,bits); } - // A series of libc/external calls that Simple can link against in a JIT. // Since no runtime in the JVM process, using magic numbers for the CPU // emulators to pick up on. public static int SENTINEL_CALLOC = -4; public static int SENTINEL_WRITE = -8; + int externalAddress(String name) { + Integer address=_externalAddresses.get(name); + if( address!=null ) return address; + return switch(name) { + case "calloc" -> SENTINEL_CALLOC; + case "write" -> SENTINEL_WRITE; + default -> throw new IllegalArgumentException("Unresolved external function '"+name+"'"); + }; + } + void patchGlobalRelocations() { + for( Node src : _externData.keySet() ) { + String name = _externData.get(src); + Integer target = _code._externDataAddresses.get(name); + if( target==null ) throw new IllegalArgumentException("Unresolved C data symbol '"+name+"'"); + int start = opStart(src); + ((RIPRelSize)src).patch(this,start,opLen(src),target-start); + } for( Node src : _externals.keySet() ) { - int start = _opStart[src._nid]; + int start = opStart(src); String dst = _externals.get(src); - int target = switch( dst ) { - case "calloc" -> SENTINEL_CALLOC; - case "write" -> SENTINEL_WRITE ; - default -> throw Utils.TODO(); - }; - ((RIPRelSize)src).patch(this, start, _opLen[src._nid], target - start); + int target = externalAddress(dst); + ((RIPRelSize)src).patch(this, start, opLen(src), target - start); } } } diff --git a/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java b/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java index 4adb2fa4..923486c6 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java +++ b/src/main/java/com/seaofnodes/simple/codegen/FinalizeIR.java @@ -1,80 +1,44 @@ package com.seaofnodes.simple.codegen; -import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.*; import java.util.Arrays; -// Settle layouts and detach the analyzed call graph before the backend. +// Comnpute field layouts only after SCCP and its peepholes converge. +// Keep surviving functions rooted without introducing unknown callers. abstract class FinalizeIR { static void run(CodeGen code) { - fieldSizes(code); - code._iter.iterate(code); - // The external-world cycle is an analysis edge, not runtime control. - // Freeze its facts before replacing call edges with emission roots. - code._stop.close(); - code._start.close(); - removeCallGraph(code); - Arrays.sort(code._stop._inputs._es,0,code._stop.nIns(),(x,y) -> x._nid - y._nid); - } - - // Freeze field sizes; do struct layouts; convert field offsets into - // constants. - private static void fieldSizes( CodeGen code ) { + // Compute field layouts and replace symbolic field offsets by constants. for( int i=0; i<code._start.nOuts(); i++ ) { Node use = code._start.out(i); if( use instanceof ConFldOffNode off ) { - TypeMemPtr tmp = (TypeMemPtr) Parser.TYPES.get(off._name); - off.subsume( off.asOffset(tmp._obj) ); + Node con = off.asOffset(); + assert con != null : off._ts._name+"."+off._fname; + off.subsume(con); i--; } } - } - - // The remaining passes assume all calls are unlinked; i.e. we are throwing - // away the Call Graph here. Functions only reachable from internal calls - // need to be re-hooked to stop/start less they go dead. - private static void removeCallGraph(CodeGen code) { - code._start.walk( x -> { - // Unlink all existing (conservative) linkages. - if( x instanceof CallNode call ) { - for( Node y : call._outputs ) { - // Keep function alive and hooked to Start so it gets - // codegen'd. Function is callable by this call via - // TFP, although perhaps not by any other means. - if( y instanceof FunNode fun && !(fun.in(1) instanceof StartNode) ) { - ParmNode rpc = fun.rpc(); - if( rpc==null ) // Ensure valid RPC - rpc = makeRPC(fun); - // Insert a hook to Start and Stop - fun.insertDef(1,code._start); - for( Node use : fun._outputs ) - if( use instanceof ParmNode parm ) - parm.insertDef(1,ConstantNode.make(parm._type).peephole()); - code._stop.addDef(fun.ret()); - } - } - call.unlink_all(); + code._iter.iterate(code); // Propagate field offsets and clean up. + + // Retain surviving functions for emission without adding unknown callers. + for( FunNode fun : code._linker ) + if( fun != null ) { + // A folded static function field can leave a detached body + // with internal cycles. It has no callers; later phases must + // not revive it merely because its nodes are still allocated. + if( fun.isDead() || fun._type.isHigh() ) { + code._linker.set(fun.sig().fidx(),null); + continue; } - return null; - } ); - - } - - // Make a valid Parm RPC. - // First make sure a valid RPC; single-call into multi-fun will have each - // function having a single call site, so the RPC becomes a constant and - // folds - but since multiple targets, the Call never inlines. Recreate a - // valid RPC so codegen (and Eval2) understands the calling convention. - private static ParmNode makeRPC( FunNode fun ) { - ParmNode rpc = new ParmNode("$rpc",0,fun.ret().rpc()._type,fun); - for( int i=1; i<fun.nIns(); i++ ) { - CallEndNode cend = ((CallNode)fun.in(i)).cend(); - rpc.addDef(ConstantNode.make(cend._rpc).peephole()); + code.keepInCompUnit(fun); + } + for( CompUnit cu : code._compunits.values() ) { + ProjNode mem = cu._start.proj(1); + if( mem != null ) code.add(mem.unkeep()); } - fun.ret().setDef(3,rpc.init()); - return rpc; + // Stable print order: initializers first, then other Returns by NID. + for( Node stop : code._stop._inputs ) + Arrays.sort(stop._inputs._es,0,stop.nIns(), + (x,y) -> ((((ReturnNode)x).fun().isClz() ? -1 : x._nid) - + (((ReturnNode)y).fun().isClz() ? -1 : y._nid) )); } - - } diff --git a/src/main/java/com/seaofnodes/simple/codegen/FinishCalls.java b/src/main/java/com/seaofnodes/simple/codegen/FinishCalls.java new file mode 100644 index 00000000..7ba696d3 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/codegen/FinishCalls.java @@ -0,0 +1,57 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; + +// Detach analysis call edges after building the loop tree. Every live body +// belongs to this invocation; preserve it for scheduling and emission. +abstract class FinishCalls { + + + static void run(CodeGen code) { + + // The remaining passes assume all calls are unlinked; i.e. we are throwing + // away the Call Graph here. Functions only reachable from internal calls + // need to be re-hooked to stop/start less they go dead. + for( FunNode fun : code._linker ) { + // Already linked to start, not going dead + if( fun==null || fun.isDead() ) + continue; + + // Insert the post-Opto code-generation hook. Unlike an Opto-time + // Start input, this no longer carries unknown-caller semantics. + ReturnNode ret = fun.ret(); + StartNode start = fun._compunit._start; + assert start!=null; + if( fun.nIns() <= 1 || fun.in(1) != start ) { + fun.insertDef(1,start); + for( Node use : fun._outputs ) + if( use instanceof ParmNode parm ) + parm.insertDef(1,ConstantNode.raw(parm._type)); + if( fun._compunit._stop._inputs.find(ret) == -1 ) + fun._compunit._stop.addDef(ret); + } + // Unlink from Call + for( int i=2; i<fun.nIns(); i++ ) + ((CallNode)fun.in(i)).unlink(fun,i--); + if( fun.rpc()==null ) { // Ensure valid RPC + // First make sure a valid RPC; single-call into multi-fun will have each + // function having a single call site, so the RPC becomes a constant and + // folds - but since multiple targets, the Call never inlines. Recreate a + // valid RPC so codegen (and Eval2) understands the calling convention. + ParmNode rpc = new ParmNode( "$rpc", 0, ret.rpc()._type, fun ); + rpc.addDef(ret.rpc()); + ret.setDef( 3, rpc.init() ); + } + } + + code._stop.walk(n -> { + if( n instanceof CallEndNode cend ) + for( int i=1; i<cend.nIns(); i++ ) + if( cend.in(i) instanceof ReturnNode ) + cend.delDef(i--); + if( n instanceof CallNode call ) + call.unlink_all(); + return null; + }); + } +} diff --git a/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java b/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java index 9be662e8..4477025b 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java +++ b/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java @@ -27,16 +27,15 @@ public abstract class GlobalCodeMotion { breakUpGlobalConstants(code._start); code._visit.clear(); - schedLate (code); + schedLate(code); } // Post-Order of CFG private static void _rpo_cfg(CFGNode def, Node use, BitSet visit, Ary<CFGNode> rpo) { if( !(use instanceof CFGNode cfg) || visit.get(cfg._nid) ) return; // Been there, done that - if( def instanceof ReturnNode && use instanceof CallEndNode ) - return; - assert !( def instanceof CallNode && use instanceof FunNode ); // All calls unwired now + assert !( def instanceof ReturnNode && use instanceof CallEndNode ); // All calls unwired now + assert !( def instanceof CallNode && use instanceof FunNode ); // All calls unwired now visit.set(cfg._nid); for( Node useuse : cfg._outputs ) _rpo_cfg(cfg,useuse,visit,rpo); @@ -50,7 +49,7 @@ public abstract class GlobalCodeMotion { var cons = new ArrayList<Node>(); for( Node con : start._outputs ) if( con!=null && !(con instanceof CFGNode) && - (con.isConst() || (con instanceof MachNode mach && mach.outregmap()!=null)) ) { + (con.isConst() || con instanceof MachNode mach && mach.outregmap()!=null) ) { globals.put(con,true); cons.add(con.keep()); // Preserve original inputs while rewiring users. } @@ -61,6 +60,7 @@ public abstract class GlobalCodeMotion { if( use==null || globals.containsKey(use) ) continue; FunNode fun = useFun(use); if( fun==null ) continue; // Global metadata has no function owner. + assert CodeGen.CODE._mach==null || CodeGen.CODE.owns(fun); var local = copies.computeIfAbsent(fun,f -> new IdentityHashMap<>()); Node copy = cloneGlobal(con,fun,globals,local); for( int i=0; i<use.nIns(); i++ ) @@ -142,8 +142,9 @@ public abstract class GlobalCodeMotion { // Record Load NIDs at all their CFG block choices, then check against // Store block choices to force a Load above an anti-dependent Store. int[] anti = new int[code.UID()]; + Ary<Node> antideps = new Ary<>(Node.class); // Breadth-first scheduling - breadth(code._stop,ns,late,anti); + breadth(code._stop,ns,late,anti, antideps); // Copy the best placement choice into the control slot for( int i=0; i<late.length; i++ ) @@ -151,7 +152,7 @@ public abstract class GlobalCodeMotion { ns[i].setDef(0,late[i]); } - private static void breadth(Node stop, Node[] ns, CFGNode[] late, int[] anti) { + private static void breadth(Node stop, Node[] ns, CFGNode[] late, int[] anti, Ary<Node> antideps) { // Things on the worklist have some (but perhaps not all) uses done. WorkList<Node> work = new WorkList<>(); work.push(stop); @@ -172,12 +173,12 @@ public abstract class GlobalCodeMotion { // Loads need their memory inputs' uses also done if( n instanceof MemOpNode ld && ld._isLoad ) - for( Node memuse : ld.antiDeps() ) + for( Node memuse : ld.antiDeps(antideps) ) if( late[memuse._nid]==null ) continue outer; // All uses done, schedule - _doSchedLate(n,ns,late,anti); + _doSchedLate(n,ns,late,anti,antideps); } // A use just finished; reconsider its inputs and waiting loads, @@ -194,7 +195,7 @@ public abstract class GlobalCodeMotion { } } - private static void _doSchedLate(Node n, Node[] ns, CFGNode[] late, int[] anti) { + private static void _doSchedLate(Node n, Node[] ns, CFGNode[] late, int[] anti, Ary<Node> antideps) { // Walk uses, gathering the LCA (Least Common Ancestor) of uses CFGNode early = n.in(0) instanceof CFGNode cfg ? cfg : n.in(0).cfg0(); assert early != null; @@ -205,8 +206,7 @@ public abstract class GlobalCodeMotion { // Loads may need anti-dependencies, raising their LCA if( n instanceof MemOpNode load && load._isLoad ) - lca = find_anti_dep(lca,load,early,late,anti); - + lca = find_anti_dep(lca,load,early,late,anti, antideps); // Walk up from the LCA to the early, looking for best place. This is // the lowest execution frequency, approximated by least loop depth and @@ -224,7 +224,8 @@ public abstract class GlobalCodeMotion { // Block of use. Normally from late[] schedule, except for Phis, which go // to the matching Region input. private static CFGNode use_block(Node n, Node use, CFGNode[] late) { - if( use instanceof ParmNode ) return late[use._nid]; + if( use instanceof ParmNode parm ) + return late[use._nid]; if( !(use instanceof PhiNode phi) ) return late[use._nid]; CFGNode found=null; @@ -245,46 +246,37 @@ public abstract class GlobalCodeMotion { best instanceof IfNode; } - // An allocation may consume a load's memory through a partial aggregate. + // A writer may consume a load's memory through an aggregate. private static void wakeLoads(Node def, CFGNode[] late, WorkList<Node> work, BitSet visit) { if( visit.get(def._nid) ) return; visit.set(def._nid); for( Node out : def._outputs ) if( out instanceof MemOpNode ld && ld._isLoad && late[ld._nid]==null ) work.push(ld); - if( def instanceof MemMergeNode || def instanceof EscapeNode ) + if( def instanceof MemMergeNode ) for( int i=1; i<def.nIns(); i++ ) if( def.in(i)!=null ) wakeLoads(def.in(i),late,work,visit); } - private static CFGNode find_anti_dep(CFGNode lca, MemOpNode load, CFGNode early, CFGNode[] late, int[] anti) { + private static CFGNode find_anti_dep(CFGNode lca, MemOpNode load, CFGNode early, CFGNode[] late, int[] anti, Ary<Node> antideps) { // We could skip final-field loads here. // Walk LCA->early, flagging Load's block location choices - for( CFGNode cfg=lca; early!=null && cfg!=early.idom(); cfg = cfg.idom() ) + for( CFGNode cfg=lca; cfg!=early.idom(); cfg = cfg.idom() ) anti[cfg._nid] = load._nid; // Walk load->mem uses, looking for Stores causing an anti-dep - for( Node mem : load.antiDeps() ) { - switch( mem ) { - case MemOpNode st: - assert late[st._nid]!=null; - lca = anti_dep(load,late[st._nid],st.cfg0(),lca,st,anti); - break; - case CallNode st: - assert late[st._nid]!=null; - lca = anti_dep(load,late[st._nid],st.cfg0(),lca,st,anti); - break; - case PhiNode phi: + for( Node mem : load.antiDeps(antideps) ) { + if( mem instanceof PhiNode phi ) { // Inputs carrying different memory might overwrite this alias. - // Raise the LCA for those paths; unchanged memory does not - // clobber the load. No scheduling edge to the Phi. + // Raise the LCA for those paths; the unchanged-memory paths + // do not clobber the load. No scheduling edge to the Phi. for( int i=1; i<phi.nIns(); i++ ) if( phi.in(i)!=load.mem() ) lca = anti_dep(load,phi.region().cfg(i),load.mem().cfg0(),lca,null,anti); - break; - case NewNode st: - assert late[st._nid]!=null; - lca = anti_dep(load,late[st._nid],st.cfg0(),lca,st,anti); - break; - default: throw Utils.TODO(); + } else { + // Hunt for clobbering stores + assert mem instanceof MemOpNode || mem instanceof EscapeNode || + mem instanceof CallNode || mem instanceof CLInitNode; + assert late[mem._nid]!=null; + lca = anti_dep(load,late[mem._nid],mem.cfg0(),lca,mem,anti); } } return lca; @@ -294,14 +286,15 @@ public abstract class GlobalCodeMotion { private static CFGNode anti_dep( MemOpNode load, CFGNode stblk, CFGNode defblk, CFGNode lca, Node st, int[] anti ) { // A conditional writer can precede the load without dominating its late block. // Walk back to the first overlap with the load's placement range. - // Another Phi input need not be dominated by this memory definition. - // Include its depth, and retain the sentinel for an entry's null idom. + // A different Phi input need not be dominated by this memory's + // definition. Stop at its depth rather than looking for that ancestor. + // Retain the sentinel too: a function entry has a null idom. for( ; stblk != defblk.idom() && stblk.idepth() >= defblk.idepth(); stblk = stblk.idom() ) { // Store and Load overlap, need anti-dependence if( anti[stblk._nid]==load._nid ) { lca = stblk.domLCA(lca,null); // Raise Loads LCA if( lca == stblk && st != null && !(st instanceof CallNode) && st._inputs.find(load) == -1 ) // And if something moved, - st.addDef(load); // Add anti-dep as well; Calls already end their block. + st.addDef(load); // Add anti-dep as well (calls already end their block) return lca; // Cap this stores' anti-dep to here } } diff --git a/src/main/java/com/seaofnodes/simple/codegen/IFG.java b/src/main/java/com/seaofnodes/simple/codegen/IFG.java index bcd1957d..bcdb14bf 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/IFG.java +++ b/src/main/java/com/seaofnodes/simple/codegen/IFG.java @@ -69,9 +69,9 @@ abstract public class IFG { // Reset all to empty resetBBLiveOut(); resetIFG(); + WORK.clear(); // Last block has nothing live out - assert WORK.isEmpty(); for( CFGNode bb : alloc._code._cfg ) if( bb.blockHead()) WORK.push( bb ); @@ -208,7 +208,13 @@ abstract public class IFG { assert tlrg.leader(); // Always, tlrg cannot use kills if( tlrg._mask.overlap(killMask) ) { - if( !tlrg.sub(killMask) ) alloc.fail(tlrg); + Node live = TMP.get(tlrg); + // Disallow clone-ables from living across killed registers. + // Fail them and let splitting re-clone closer to the target use. + if( live instanceof MachNode mach && mach.isClone() ) + alloc.fail(tlrg); + else if( !tlrg.sub(killMask) ) + alloc.fail(tlrg); } } } @@ -316,7 +322,7 @@ abstract public class IFG { // Pull all lrgs from IFG, in trivial order if possible while( sptr < color_stack.length ) { // Swap best color up front - pickColor(color_stack,sptr,swork); + pickColor(color_stack,sptr,swork,alloc); // Pick a trivial lrg, and (temporarily) remove from the IFG. LRG lrg = color_stack[sptr++]; @@ -372,10 +378,10 @@ abstract public class IFG { } // Pick LRG from color stack - private static void pickColor(LRG[] color_stack, int sptr, int swork) { + private static void pickColor(LRG[] color_stack, int sptr, int swork, RegAlloc alloc) { // Out of trivial colorable, pick an at-risk to pull if( sptr==swork ) - swap(color_stack,sptr,pickRisky(color_stack,sptr)); + swap(color_stack,sptr,pickRisky(color_stack,sptr,alloc)); // When coloring, we'd like to give more choices; so when coloring we'd // like to see the single-def first (since no choices anyway), then // non-split related (so more live ranges get colored), then @@ -392,6 +398,7 @@ abstract public class IFG { swap(color_stack,sptr,bidx); // Pick best at sptr } + // Returns true if 'lrg' is better than 'best' private static boolean betterLRG( LRG best, LRG lrg ) { // If single-def varies, keep the not-single-def if( best.size1() != lrg.size1() ) @@ -399,18 +406,22 @@ abstract public class IFG { // If hasSplit varies, keep the hasSplit if( best.hasSplit() != lrg.hasSplit() ) return lrg.hasSplit(); + // If both are single register (so both sizes are 1), + // return a multi-use over a multi-def + if( best.size1() ) + return !best._multiUse && lrg._multiUse; // Keep large register count return best.size() < lrg.size(); } // Pick a live range that hasn't already spilled, or has a single-def- // single-use that are not adjacent. - private static int pickRisky( LRG[] color_stack, int sptr ) { + private static int pickRisky( LRG[] color_stack, int sptr, RegAlloc alloc ) { int best=sptr; - int bestScore = pickRiskyScore(color_stack[best]); + int bestScore = pickRiskyScore(color_stack[best],alloc); for( int i=sptr+1; i<color_stack.length; i++ ) { if( bestScore == 1000000 ) return best; // Already max score - int iScore = pickRiskyScore(color_stack[i]); + int iScore = pickRiskyScore(color_stack[i],alloc); if( iScore > bestScore ) { best = i; bestScore = iScore; } } @@ -425,30 +436,60 @@ abstract public class IFG { // // Picking a live range that is very close to coloring might allow it to // color despite being risky. - private static int pickRiskyScore( LRG lrg ) { + private static int pickRiskyScore( LRG lrg, RegAlloc alloc ) { + // Callee-save live ranges cover the whole function and are cheap to + // split. Rank scheduled area recovered against the loop-scaled cost + // of the def- and use-side moves. + if( lrg._machDef instanceof CalleeSaveNode ) + return areaScore(functionArea(lrg._fun,alloc), + splitCost((Node)lrg._machDef,(Node)lrg._machUse)); + if( lrg._splitDef != null && lrg._splitDef. in(1) instanceof CalleeSaveNode && + lrg._splitUse != null && lrg._splitUse.out(0) instanceof ReturnNode ) + return areaScore(functionArea(lrg._fun,alloc), + splitCost(lrg._splitDef,lrg._splitUse)); + // Pick single-def clonables that are not right next to their single-use. - // Failing to color these will clone them closer to their uses. + // Rank by the area recovered by cloning closer to the use. if( !lrg._multiDef && lrg._machDef!=null && lrg._machUse!=null && lrg._machDef.isClone() ) { - Node def = (Node)lrg._machDef; - Node use = (Node)lrg._machUse; + Node def = ((Node)lrg._machDef); + Node use = ((Node)lrg._machUse); CFGNode cfg = def.cfg0(); - if( lrg._multiUse || cfg != use.cfg0() || // Many uses or different blocks OR - // Same block, but not close - cfg._outputs.find(def)+1 < cfg._outputs.find(use) ) + int area = cfg != use.cfg0() + ? functionArea(lrg._fun,alloc) + : cfg._outputs.find(use)-cfg._outputs.find(def)-1; + // A single-use function address can move next to its use without + // adding an instruction. Prefer that to saving a callee-save + // register just to hold the address. Stay in the same block and + // require a gap so the clone makes progress. + if( def instanceof FunPtrNode && !lrg._multiUse && + cfg == use.cfg0() && area > 0 ) return 1000000; + if( area > 0 ) + return areaScore(area,loopCost(use)); } - // Always pick callee-save registers as being very large area recovered - // and very cheap to spill. - if( lrg._machDef instanceof CalleeSaveNode ) - return 1000000-2-lrg._mask.firstReg(); - if( lrg._splitDef != null && lrg._splitDef.in(1) instanceof CalleeSaveNode && - lrg._splitUse != null && lrg._splitUse.out(0) instanceof ReturnNode ) - return 1000000-1; + // Default for ranges without a specific area/cost estimate. + return 1000 + (lrg._multiUse ? -100 : 0); + } + + private static int functionArea(FunNode fun, RegAlloc alloc) { + int area=0; + for( CFGNode bb : alloc._code._cfg ) + if( bb.fun()==fun ) + area += bb.nOuts(); + return area; + } + + private static int areaScore(int area, int cost) { + return 1000 + (area<<10)/cost; + } + + private static int splitCost(Node def, Node use) { + return loopCost(def) + (use==null ? 0 : loopCost(use)); + } - // TODO: cost/benefit model. Perhaps counting loop-depth (freq) of def/use for cost - // and "area" for benefit - return 1000; + private static int loopCost(Node n) { + return 1 << (n.cfg0().loopDepth()*3); } private static short biasColor( RegAlloc alloc, LRG lrg, short reg, RegMask mask ) { diff --git a/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java b/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java index b0351c98..db35d524 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java +++ b/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java @@ -1,30 +1,43 @@ package com.seaofnodes.simple.codegen; import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; import java.util.BitSet; import java.util.IdentityHashMap; -// Replace ideal nodes with machine nodes, then rebuild the copied graph's uses. +// Replace ideal nodes with machine nodes while preserving CFG/loop metadata. +// Selection also owns target ABI masks and rebuilding the copied graph's uses. class InstSelect { private final CodeGen _code; - InstSelect(CodeGen code) { _code=code; } + private InstSelect(CodeGen code) { _code=code; } - @SuppressWarnings("unchecked") - void run(String cpu, String callingConv, String base) { + static CodeGen select(CodeGen code, String cpu, String callingConv, String base) { + // Ideal evaluation schedules the ideal graph without a CPU port. + if( cpu==null ) return code; + return code.phase(CodeGen.Phase.Select, code._phase==CodeGen.Phase.LoopTree, + () -> new InstSelect(code).run(cpu,callingConv,base)); + } + + private void run(String cpu, String callingConv, String base) { _code._callingConv = callingConv; // Look for CPU in fixed named place: // com.seaofnodes.simple.node.cpus."cpu"."cpu.class" String clzFile = base+"."+cpu+"."+cpu; - try { _code._mach = ((Class<Machine>) Class.forName( clzFile )).getDeclaredConstructor(new Class[]{CodeGen.class}).newInstance(_code); } + try { _code._mach = Class.forName(clzFile).asSubclass(Machine.class).getDeclaredConstructor(CodeGen.class).newInstance(_code); } catch( Exception e ) { throw new RuntimeException(e); } + _code._cCallingConv = _code._mach.cCallingConv(callingConv); + // Build global copies of common register masks. long callerSave = _code._mach.callerSave(); + long cCallerSave= _code._mach.callerSave(_code._cCallingConv); long neverSave = _code._mach. neverSave(); int maxReg = Math.min(64,_code._mach.regs().length); assert maxReg>=64 || (-1L << maxReg & callerSave)==0; // No stack slots in callerSave + assert maxReg>=64 || (-1L << maxReg & cCallerSave)==0; // No stack slots in cCallerSave _code._callerSave = new RegMask(callerSave); + _code._cCallerSave= new RegMask(cCallerSave); // Build a Return RegMask array. All returns have the following inputs: // 0 - ctrl @@ -43,11 +56,18 @@ class InstSelect { _code._uid = 1; // All new machine nodes reset numbering var map = new IdentityHashMap<Node,Node>(); _instSelect( _code._stop, map ); - _code._stop = ( StopNode)map.get(_code._stop ); + _code._stop = (StopNode)map.get(_code._stop ); StartNode start = (StartNode)map.get(_code._start); _code._start = start==null ? new StartNode(_code._start) : start; _instOuts(_code._stop,_code.visit()); _code._visit.clear(); + + // Replace the CompUnit stop (and list of functions) + // with hardware-specific ones + for( CompUnit cu : _code._compunits.values() ) { + cu._start= (StartCUNode)map.get(cu._start); + cu._stop = (StopCUNode )map.get(cu._stop ); + } } private Node _instSelect( Node n, IdentityHashMap<Node,Node> map ) { @@ -65,14 +85,28 @@ class InstSelect { } // Produce a machine node from n; map it to flag as done so stops cycles. - map.put(n, x=select(n) ); - // Stop retains the callable body. Its lifetime edge is not a machine operand. - if( n instanceof FunPtrNode ) x._inputs.pop(); + 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)); + } + 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 ) { + xcfg._ltree = ncfg._ltree; + xcfg._pre = ncfg._pre; + } // Walk machine op and replace inputs with mapped inputs for( int i=0; i < x.nIns(); i++ ) x._inputs.set(i, _instSelect(x.in(i),map) ); - if( x instanceof MachNode mach ) + // Post selection action + if( x instanceof MachNode mach ) { + if( n instanceof ReturnNode ret ) + ((ReturnNode)mach)._fun = (FunNode)map.get(ret._fun); mach.postSelect(_code); // Post selection action + } return x; } @@ -83,30 +117,10 @@ class InstSelect { for( Node in : n._inputs ) if( in!=null ) { in._outputs.push(n); + // CallNode special: outputs are partially ordered; CallEnd in slot 0 + if( in instanceof CallNode call && n instanceof CallEndNode cend && call.out(0) != cend ) + call._outputs.swap(0,call.nOuts()-1); _instOuts(in,visit); } } - - - private Node select(Node n) { - if( n instanceof EscapeNode esc ) return new EscapeMach(esc); - if( n instanceof GuardNode guard ) return new GuardMach(guard); - if( n instanceof PtrToIntNode ptr ) return new PtrToIntMach(ptr); - Node op = n; - if( n instanceof ModeNode mode && mode.mode()==2 ) { - // Adapt to the existing CPU selectors. As with machine expansion, - // copy inputs without adding uses to the old graph; _instOuts - // rebuilds reverse edges after selection has finished. - op = switch(n) { - case AddNode add -> new AddFNode(add); - case SubNode sub -> new SubFNode(sub); - case MulNode mul -> new MulFNode(mul); - case DivNode div -> new DivFNode(div); - case MinusNode neg -> new MinusFNode(neg); - default -> n; - }; - op._type = n._type; - } - return _code._mach.instSelect(op); - } } diff --git a/src/main/java/com/seaofnodes/simple/codegen/Iter.java b/src/main/java/com/seaofnodes/simple/codegen/Iter.java new file mode 100644 index 00000000..1ffd9737 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/codegen/Iter.java @@ -0,0 +1,38 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; + +// Establish a pessimistic fixed point after parsing. Later Iter runs +// reuse the worklist and types produced by the preceding phase. +abstract class Iter { + static void run(CodeGen code) { + // Close parser-in-progress states. Seed a fresh pessimistic solve; + // exposure sets start at FULL and can only shrink during Iter. + code._start.walk(n -> { + if( !(n instanceof ConstantNode) ) + n._type = iterEscapeReset(n._type); + code.add(n); + return null; + }); + code._iter.iterate(code); + // Expensive assert. + assert !CodeGen.expensiveAssert() || Opto.fixedPointCheck(code); + } + + private static Type iterEscapeReset(Type t) { + if( t instanceof TypeMem mem ) + return mem._one ? mem : mem.makeFrom(XInt.FULL,XInt.FULL); + if( t instanceof TypeTuple tt ) { + TypeTuple rez = tt; + for( int i=0; i<tt._types.length; i++ ) { + Type tx = iterEscapeReset(tt._types[i]); + if( tx != tt._types[i] ) rez = rez.makeFrom(i,tx); + } + return rez; + } + return t; + } + + +} diff --git a/src/main/java/com/seaofnodes/simple/codegen/LRG.java b/src/main/java/com/seaofnodes/simple/codegen/LRG.java index 0c36ae2b..fdde8838 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/LRG.java +++ b/src/main/java/com/seaofnodes/simple/codegen/LRG.java @@ -56,6 +56,10 @@ public class LRG { // Adjacent Live Range neighbors. Only valid during coloring Ary<LRG> _adj; + // Function fully containing the live range; used to find the stack frame + // layout, for spill op offsets. + final public FunNode _fun; + public int nadj() { return _adj==null ? 0 : _adj._len; } public RegMask useMask() { return _machUse==null ? null : _machUse.regmap(_uidx); } @@ -80,7 +84,7 @@ public class LRG { // More registers than neighbors boolean lowDegree() { return nadj() < _mask.size(); } - LRG( short lrg ) { _lrg = lrg; _reg = -1; } + LRG( short lrg, FunNode fun ) { _lrg = lrg; _reg = -1; _fun = fun; } boolean leader() { return _leader == null; } @@ -116,6 +120,7 @@ public class LRG { } // Union `this` and `lrg`, folding together all stats. private LRG _union( LRG lrg ) { + assert _fun == lrg._fun; // Set U-F leader lrg._leader = this; // Fold together stats diff --git a/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java b/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java index 2415203d..252b5bf0 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java +++ b/src/main/java/com/seaofnodes/simple/codegen/LinkMem.java @@ -1,6 +1,8 @@ package com.seaofnodes.simple.codegen; import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; // In-memory linking @@ -8,16 +10,24 @@ public class LinkMem { final CodeGen _code; LinkMem( CodeGen code ) { _code = code; } - public CodeGen link() { - Encoding enc = _code._encoding; - + public CodeGen link(Encoding enc) { // Patch external calls internally enc.patchGlobalRelocations(); - // Write any large constants into a constant pool; they - // are accessed by RIP-relative addressing. - enc.writeConstantPool(enc._bits,true); + // Place constant pool after code, static data after constants + int code = 0; // Code start + int cpool = (enc._bits .size() + code + 15) & -16; // pad to 16 + int sdata = (enc._cpool.size() + cpool + 15) & -16; + + + // Patch local ops, e.g. loading float constants from the constant pool + for( Node op : enc._bigCons.keySet() ) { + Encoding.Relo relo = enc._bigCons.get(op); + int target = enc._data.address(enc._data.object(relo._t),cpool,sdata); + ((RIPRelSize)relo._op).patch(enc, relo._opStart, enc.opLen(relo._op), target - relo._opStart); + } + enc._data.link(cpool,sdata); return _code; } } diff --git a/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java b/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java index ddf5a679..b6b3a22c 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java +++ b/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java @@ -28,7 +28,7 @@ public abstract class ListScheduler { } void free() { _n = null; FREE.push(this); } - static XSched get(Node n) { return n==null ? null : XS.get(n instanceof ProjNode && !(n.in(0) instanceof CallEndNode) ? n.in(0) : n); } + static XSched get(Node n) { return n==null ? null : XS.get(n instanceof ProjNode && !(n.in(0) instanceof CFGNode) ? n.in(0) : n); } private XSched init(CFGNode bb, Node n) { _n = n; diff --git a/src/main/java/com/seaofnodes/simple/codegen/LocalIds.java b/src/main/java/com/seaofnodes/simple/codegen/LocalIds.java new file mode 100644 index 00000000..462248dd --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/codegen/LocalIds.java @@ -0,0 +1,33 @@ +package com.seaofnodes.simple.codegen; + +import java.util.HashMap; +import java.util.Arrays; + +// Dense IDs for this invocation. A class slot can be reserved before its file +// is parsed; every later reference to that slot gets the same ID. +public class LocalIds { + public static final int RESERVED=2; + private int _next=RESERVED; + private final com.seaofnodes.simple.util.Ary<String> _owners=new com.seaofnodes.simple.util.Ary<>(String.class); + private final HashMap<String,int[]> _slots=new HashMap<>(); + + public int next(String owner) { return next(owner,-1); } + public int next(String owner, int slot) { + int[] ids=_slots.get(owner); + if( ids==null ) ids=new int[]{RESERVED,1}; + if( slot==-1 ) slot=ids[0]++; + else if( slot>=ids[0] ) ids[0]=slot+1; + if( slot>=ids.length ) ids=Arrays.copyOf(ids,Math.max(slot+1,ids.length*2)); + if( ids[slot]==0 ) { ids[slot]=_next++; _owners.setX(ids[slot],owner); } + _slots.put(owner,ids); + return ids[slot]; + } + public String owner(int id) { return _owners.atX(id); } + public String symbol(int id) { return owner(id)+".$fun"+id; } + public int nextInline() { return _next++; } + public int next(int old) { + int id=_next++; + _owners.setX(id,owner(old)); + return id; + } +} diff --git a/src/main/java/com/seaofnodes/simple/codegen/Machine.java b/src/main/java/com/seaofnodes/simple/codegen/Machine.java index 7ca465ba..3eee8fc3 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/Machine.java +++ b/src/main/java/com/seaofnodes/simple/codegen/Machine.java @@ -7,28 +7,42 @@ import com.seaofnodes.simple.type.TypeInteger; abstract public class Machine { // Human readable machine name. Something like "x86-64" or "arm" or "risc5" public abstract String name(); + // Default opcode size for printing; 4 bytes for most 32-bit risc chips public int defaultOpSize() { return 4; } + // Human-readable name for a register number, e.g. "RAX" or "R0" public abstract String[] regs(); - // Create a split op; any register to any register, including stack slots - public abstract SplitNode split( String kind, byte round, LRG lrg); + + // Create a split op; any register to any register, including stack slots. + // The FunNode holds the stack layout after RegAlloc, which in turn + // dictates the stack slot (the frame offset) for stack splits. + public abstract SplitNode split( LRG lrg, String kind, byte round ); + // List of caller-save registers, as a 64bit mask public abstract long callerSave(); + public long callerSave(String conv) { return callerSave(); } + // List of never-save registers, e.g. RSP or a ZERO register if you have one public abstract long neverSave(); + // Call Argument Mask. Passed in the function signature and argument // number (2-based; 0 is for control and 1 for memory). Also passed in a 0 // for the function itself, or for *outgoing* calls, the maximum stack slot // given to the incoming function arguments (stack slots reserved for // incoming arguments). public abstract RegMask callArgMask(TypeFunPtr tfp, int arg, int maxArgSlot); + public RegMask callArgMask(TypeFunPtr tfp, int arg, int maxArgSlot, String conv) { return callArgMask(tfp,arg,maxArgSlot); } + // Return register mask, based on signature (GPR vs FPR) public abstract RegMask retMask(TypeFunPtr tfp); + // Return PC register public abstract int rpc(); + // Return a MachNode unconditional branch public abstract CFGNode jump(); + // Instruction select from ideal nodes public abstract Node instSelect( Node n ); @@ -94,13 +108,17 @@ Pre-alloc Post-Alloc SP+48 RPC // shadow slots if defined in the ABI, even if all arguments are passed in // registers. public abstract short maxArgSlot(TypeFunPtr tfp); + public short maxArgSlot(TypeFunPtr tfp, String conv) { return maxArgSlot(tfp); } + + // Calling convention used for external C symbols. + public String cCallingConv(String callingConv) { return callingConv; } // Only the declared C result width describes the physical return register. // Simple functions already return canonical full-width integer values. protected static TypeInteger cReturn(ProjNode prj) { if( prj._idx==2 && prj.in(0) instanceof CallEndNode cend && - cend.call().fptr()._type instanceof TypeFunPtr tfp && - CodeGen.CODE.externFunc(tfp)!=null && + cend.call().fptr()._type instanceof TypeFunPtr tfp && tfp.isConstant() && + CodeGen.CODE.externFunc(tfp.fidx())!=null && tfp.ret() instanceof TypeInteger ret && ret.log_size()<3 ) return ret; return null; diff --git a/src/main/java/com/seaofnodes/simple/codegen/Opto.java b/src/main/java/com/seaofnodes/simple/codegen/Opto.java index 3c34b44c..4db277df 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/Opto.java +++ b/src/main/java/com/seaofnodes/simple/codegen/Opto.java @@ -1,9 +1,9 @@ package com.seaofnodes.simple.codegen; -import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.Ary; + import java.util.BitSet; // Optimistic Algorithm @@ -24,36 +24,25 @@ abstract public class Opto { // get a fairly precise set of Call Graph edges between CallNodes and // FunNodes. public static void opto(CodeGen code) { - // Reset Types before running the worklist algorithm Ary<Type> oldTypes = new Ary<>(Type.class); // Used for asserts only resetToTop(code,oldTypes); - - // Main seeds a whole program. Without main, named exported functions - // seed discovery. Either way, escaping addresses can add more unknown - // callers as Start and Stop reach their shared fixed point. - boolean wholeWorld = code.link(code._main) != null; - - - // If wholeWorld, remove the Start inputs to functions - they are not - // called unless they get linked (hence go dead and can be removed). - // If not wholeWorld, keep only public methods (and not the std lib). - unlinkStart(wholeWorld,code); - + unlinkStart(code); // OPTIMISTIC INTERPROCEDURAL SCCP sccp(code, oldTypes); + + // SCCP can leave a recursive numeric component at BOTTOM with no + // arithmetic mode selected. Resolve the whole component from all + // available int/float evidence before IterPeeps consumes the types. resolveNumericModes(code); - if( !code._iter._work.isEmpty() ) sccp(code, oldTypes); - ProjNode startMem=code._start.proj(1); - if( startMem!=null ) { startMem.unkeep(); code.add(startMem); } + if( !code._iter._work.isEmpty() ) + sccp(code, oldTypes); // After SCCP add everything changed to the worklist and see if // peepholes can make more progress. moveChangesToWorklist(code, oldTypes); - - } // Reset Types before running the worklist algorithm @@ -62,9 +51,6 @@ abstract public class Opto { oldTypes.setX(x._nid,x._type); // Record original types for asserts x._type = Type.TOP; // Reset all Nodes to TOP code._iter.add(x); // Visit everybody at least once; most Nodes produce better than TOP - // Unlink all existing (conservative) linkages. - if( x instanceof CallNode call ) - call.unlink_all(); return null; } ); } @@ -73,48 +59,56 @@ abstract public class Opto { // If wholeWorld, remove the Start inputs to functions - they are not // called unless they get linked (hence go dead and can be removed). If // not wholeWorld, keep only public methods (and not the std lib). - private static void unlinkStart(boolean wholeWorld, CodeGen code) { - ProjNode startMem=code._start.proj(1); - if( startMem!=null ) startMem.keep(); - for( FunNode fun : code._linker ) { - if( fun != null && !fun.isDead() && !funIsPublic(code,wholeWorld,fun) ) { - assert fun.in(1)==code._start; // Start always in slot 1 - fun.removeDeadPath(1); - // By same logic, remove stop->return linkage, without - // remove the (now dead) ReturnNode; it awaits in limbo - // for the opto phase to link to some valid call - code._stop._inputs.del(code._stop._inputs.find(fun.ret())); - fun.ret().delUse(code._stop); - } + private static void unlinkStart(CodeGen code) { + // Keep the Start & Start.ProjMem alive even as we remove all uses + for( CompUnit cu : code._compunits.values() ) { + ProjNode mem = cu._start.proj(1); + if( mem!=null ) mem.keep(); } - } - // Function is public (callable from Start directly). - // - Always true for main - // - Otherwise false for stdlib or anonymous functions - // - Named functions use `!wholeWorld` - private static boolean funIsPublic( CodeGen code, boolean wholeWorld, FunNode fun ) { - // Always true for main - if( fun.sig().fidx() == code._main.fidx() ) return true; - // Hidden allocation helpers are never external entry points. - if( fun._constructor ) return false; - // Taking an address does not expose it. Stop discovers escapes from - // reachable returns and memory; linkStart restores only those callers. - // Never true for stdlib or anonymous functions - if( fun._name == null || fun._name.startsWith("sys.") ) - return false; - // Named functions (and not stdlib) are callable if not whole-world. - return !wholeWorld; + // Iterate the Stop-of-Stops + for( Node stop : code._stop._inputs ) { + for( int i=0; i<stop.nIns(); i++ ) { + ReturnNode ret = (ReturnNode)stop.in(i); + FunNode fun = ret.fun(); + // Start is a semantic unknown-caller edge. Public functions + // keep it; closed-world functions must be reached by a + // concrete Call edge discovered during SCCP. Initializers, + // including private ones, are called by opaque CLInit + // obligations whose Call edges are introduced only in Lower. + if( !fun.isClz() && + !code.publicFIDX(fun.sig().fidx()) && + fun.in(1) instanceof StartNode ) { + fun.removeDeadPath(1); + // Unhook from Stop without treating the ReturnNode as + // DEAD. It stays in limbo until Opto links when a caller + // is discovered (or not, and the function is really dead). + stop._inputs.del(i--); + ret.delUse(stop); + } + // Unlink the existing conservative call linkages. + for( int j=1; j<fun.nIns(); j++ ) + if( fun.in(j) instanceof CallNode call ) + call.unlink(fun,j--); + } + } } // As part of building a CallGraph, when opto finds a function ptr flowing // into a Call, link the Call and Fun. private static void linkCG(CodeGen code, TypeFunPtr tfp, CallNode call) { if( tfp.nargs() != call.nargs() ) return; // Error calls hit this - if( XInt.isHigh(tfp.fidxs()) ) return; - for( int fidx=XInt.next(tfp.fidxs(),-1); fidx>=0; fidx=XInt.next(tfp.fidxs(),fidx) ) { - // A disconnected function can still be revived by a discovered - // call or escaping address. Do not clean the linker entry here. + int[] fidxs = tfp.fidxs(); + // An unresolved forward call can still carry the infinite, inferred + // function set into Opto. There is no finite call graph to link yet; + // type checking will report the unresolved reference afterwards. This + // can yet be valid if the call is eventually dead. + // `if( false ) frefCall(args);` + if( XInt.isHigh(fidxs) ) return; + for( int fidx = XInt.next(fidxs,0); fidx >=0; fidx = XInt.next(fidxs,fidx) ) { + // unlinkStart deliberately leaves uncalled functions in a + // temporarily dead limbo. A newly discovered call revives them; + // do not use the cleanup lookup which removes dead linker entries. FunNode fun = code._link(fidx); // null here means an external function; i.e. this Call // calls to an outside library and all its arguments escape. @@ -126,24 +120,47 @@ abstract public class Opto { } } - // Exposure is a call edge from the unknown world. Restore conservative - // declared arguments, never the narrower values inferred from known calls. - private static void linkStart(CodeGen code, TypeTuple state) { - for( FunNode fun : code._linker ) { - if( fun==null || fun.isDead() || fun._constructor || fun.unknownCallers() || - !XInt.bit(state.mem()._escFs,fun.sig().fidx()) ) continue; - fun.insertDef(1,code._start); - if( !code._stop._inputs.contains(fun.ret()) ) code._stop.addDef(fun.ret()); - code.add(fun); code.add(code._stop); - for( Node n : fun._outputs ) - if( n instanceof ParmNode parm ) { - Node def=parm._idx==1 ? code._start.proj(1) : new ConstantNode(parm._declaredType).peephole(); - parm.insertDef(1,def); + // Start added some FIDXs, the same functions now can be reached from start + // and called by anybody. Add the CG edges. + private static void linkStart( CodeGen code, TypeTuple tt ) { + TypeMem tmem = (TypeMem)tt._types[1]; + int[] fidxs = tmem._escFs; + if( XInt.isHigh(fidxs) ) { + // A scalar BOTTOM can flow into memory while compiling an error + // program. It exposes all public functions, not private fields. + for( FunNode fun : code._linker ) + if( fun != null && code.publicFIDX(fun.sig().fidx()) ) + linkStart(code,fun); + return; + } + for( int fidx = XInt.next(fidxs,0); fidx >=0; fidx = XInt.next(fidxs,fidx) ) { + FunNode fun = code._linker.atX(fidx); + if( fun==null ) assert code._externFunc.containsKey(fidx); + else linkStart(code,fun); + } + } + + // Function is now reachable by unknown callers through Start. The + // default inputs must therefore use conservative declared types. + private static void linkStart( CodeGen code, FunNode fun ) { + assert !fun.isDead(); + StartCUNode start = fun._compunit._start; + if( !fun.unknownCallers() ) { + assert !start.isDead(); + code.keepInCompUnit(fun); + // Function is reachable by any *remote* caller who gets the pointer! + fun.insertDef(1,start); + code._iter.add(fun); + for( Node p : fun._outputs ) + if( p instanceof ParmNode parm ) { + Node defalt = parm._idx==1 ? start.proj(1) : ConstantNode.seed(parm._declaredType).peephole(); + parm.insertDef(1,defalt); code.add(parm); } } } + // The core SCCP algorithm private static void sccp( CodeGen code, Ary<Type> oldTypes ) { // Iter workList is empty at this point @@ -155,50 +172,85 @@ abstract public class Opto { count++; Type oval = n._type, nval = n.compute(); if( oval == nval ) continue; - assert oval.isa(nval) : n.label()+"#"+n._nid+" old="+oval+" new="+nval; // Types start high and always fall + assert oval.isa(nval) : "Non-monotonic SCCP: "+n.getClass().getSimpleName()+"#"+n._nid+" "+n+" old="+oval+" new="+nval; // Types start high and always fall Type pesiVal = oldTypes.atX(n._nid); - assert pesiVal==null || nval.isa(pesiVal) : n.label()+"#"+n._nid+" pessimistic="+pesiVal+" new="+nval; // Never fall worse than the pessimistic pass + assert pesiVal==null || nval.isa(pesiVal) : "SCCP below pessimistic type: "+n.getClass().getSimpleName()+"#"+n._nid+" "+n+" pessimistic="+pesiVal+" new="+nval; // Never fall worse than the pessimistic pass n._type = nval; - // If a TFP adds a new function input to a call, link to the new Fun + // Now we have a series of stanzas where we lazily create the Call + // Graph - adding graph edges between every call site and called + // function, including external (unknown) callers. These edges are + // not in from the start because they are *too many*; we would get + // O(n^2) edges. So all along we have been treating them as + // "virtual" CFG edges - a FunNode taking a Start input is treated + // as if every call on the planet might call it. Now we make these + // CG edges *concrete*, adding them back as we discover that a call + // calls a particular function. + + // If a TFP adds a new function input to a call, link to the new + // Fun. This adds new edges to the graph allowing argument values + // to flow from calls into functions. The added edges are + // effectively a refined Call Graph. if( nval instanceof TypeFunPtr tfp ) { for( Node use : n._outputs ) if( !use._type.isHigh() && use instanceof CallNode call && call.fptr() == n ) linkCG(code,tfp,call); } - // Link a Call which becomes alive - if( n instanceof CallNode call && oval.isHigh() && !nval.isHigh() && call.fptr()._type instanceof TypeFunPtr ) + // Link a Call which becomes alive. Like above, adds new Call + // Graph edges to the graph. + if( n instanceof CallNode call && nval == Type.CONTROL && call.fptr()._type instanceof TypeFunPtr ) linkCG(code,call.tfp(),call); - if( n instanceof StartNode && nval instanceof TypeTuple tt ) linkStart(code,tt); + // If an otherwise-dead function pointer escapes, any future linked + // caller might find and call it. Force the function to be alive + // and called by Start. + if( n instanceof StartNode ) + linkStart(code,(TypeTuple)n._type); // Since n._type changed, visit all output neighbors code._iter.addAll(n._outputs); n.moveDepsToWorklist(code._iter); - // Quadratic (expensive) small-step assert - assert check(code); + // Quadratic (expensive) small-step assert. + assert !CodeGen.expensiveAssert(count) || worklistCheck(code); } } - // Quadratic (expensive) small-step assert - private static boolean check(CodeGen code) { - code._start.walk( x -> { + // Quadratic (expensive) fixed-point assert + public static boolean fixedPointCheck(CodeGen code) { + return fixedPointCheck(code,code._stop); + } + public static boolean fixedPointCheck(CodeGen code, Node root) { + root.walk( x -> { Type xval = x.compute(); - assert xval == x._type || (x._type.isa( xval ) && code._iter._work.on( x )) - : x.label()+"#"+x._nid+" old="+x._type+" new="+xval+" queued="+code._iter._work.on(x); + assert xval == x._type; return null; }); return true; } + // Quadratic (expensive) small-step assert for SCCP, where changed nodes + // may already be on the worklist. + private static boolean worklistCheck(CodeGen code) { + code._midAssert = true; + Node root = code._start; + root.walk( x -> { + Type xval = x.compute(); + assert xval == x._type || (x._type.isa( xval ) && code._iter._work.on( x )) + : "Non-monotonic or missed SCCP work: "+x.getClass().getSimpleName()+"#"+x._nid+" "+x+" old="+x._type+" new="+xval; + return null; + }); + code._midAssert = false; + return true; + } + // After SCCP add everything changed to the worklist and see if peepholes // can make more progress. private static void moveChangesToWorklist(CodeGen code, Ary<Type> oldTypes) { code._start.walk( x -> { assert x.compute() == x._type; // Hit the fixed point assert x._nid >= oldTypes._len || x._type.isa(oldTypes.at(x._nid)); // Hit at least the bottom-up type - code._iter.add(x); + code.add(x); return null; }); } diff --git a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java new file mode 100644 index 00000000..171d9923 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java @@ -0,0 +1,285 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.print.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.io.File; +import java.io.IOException; +import java.util.*; + +// Parse a given file, and all its dependencies +public abstract class ParseAll { + + static void parse(CodeGen code, String src) { + Parser.TYPES.clear(); + Parser.TYPES.putAll(Parser.INIT_TYPES); + if( src != null ) { + // No source file, just the source itself. + parseSource(code,code._srcName==null ? "Test" : code._srcName,src); + } else { + // Path from module root to source file + parsePath(code,code._srcName); + } + } + + // Worklist of symbols to search and files to parse + private static final Ary<CompUnit> WORK = new Ary<>(CompUnit.class); + + + // Return a compilation unit (cached or new). If new, it does not necessarily + // need to be compiled. + public static CompUnit makeCUnit( CodeGen code, CompUnit par, String name ) throws IOException { + int idx=name.indexOf('/'); + if( idx>=0 ) return makeCUnit(code,makeCUnit(code,par,name.substring(0,idx)),name.substring(idx+1)); + String fname=par==null ? name : par._fname+"/"+name; + CompUnit cu=code._compunits.get(fname); + if( cu==null ) { + String cname=par==null ? name : par._cname+"."+name; + code._compunits.put(fname,cu=new CompUnit(par,fname,cname,name)); + } + return cu; + } + + + // Parse a top-level string, used for e.g. testing + static void parseSource( CodeGen code, String src ) { + parseSource(code,"Test",src); + } + + // Parse a top-level string with an explicit compilation-unit name. + static void parseSource( CodeGen code, String fname, String src ) { + CompUnit cunit = new CompUnit(fname,src); + code._compunits.put(cunit._fname,cunit); + parseAll(code,cunit); + } + + // module_root/.../src.smp is source file name. + static void parsePath(CodeGen code, String fname) { + try { + // Split and build the parent-path down to the source file. + CompUnit cu = makeCUnit( code, null, fname ); + // Compile this CompUnit + parseAll(code,cu); + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + + private static void enqueue(CompUnit cu) { + if( cu._par!=null ) enqueue(cu._par); + if( cu._start==null && WORK.find(cu)==-1 ) WORK.add(cu); + } + + private static void parseAll(CodeGen code, CompUnit cunit) { + WORK.clear(); // Cleanup from prior tests + // Prime the pump with the first CompUnit + enqueue(cunit); + // Work through all unparsed compilation units and parse + while( !WORK.isEmpty() ) { + CompUnit cu = WORK.remove(0); + if( cu._src==null ) + throw new RuntimeException("Missing source file '"+cu._fname+".smp'"); + parseOne(code,cu); + } + + // Many types will have been declared cyclically either internally or + // across multiple compilation units. + + // Close over all recursive types, and upgrade TYPES + Ary<TypeStruct> ary = new Ary<>(TypeStruct.class); + for( Type t : Parser.TYPES.values() ) + if( t instanceof TypeStruct ts ) + ary.add(ts); + TypeStruct[] tss = Type.closeOver(ary.asAry(),Parser.TYPES); + for( TypeStruct ts : tss ) + Parser.TYPES.put(ts._name,ts); + for( CompUnit cu : code._compunits.values() ) + cu._clz = (TypeStruct)Parser.TYPES.get(Parser.addClzPrefix(cu._cname)); + + // Walk over all Nodes, and upgrade the internal constants to the + // closed-over types. + code._stop.walk( (Node n) -> { + n.upgradeType(Parser.TYPES); + if( n instanceof FRefNode fref ) { + boolean alloc = fref._name.endsWith(".<new>"); + String clzName = alloc + ? fref._name.substring(0,fref._name.length()-".<new>".length()).intern() + : fref._name; + TypeStruct clz = (TypeStruct)Parser.TYPES.get(clzName); + if( clz == null ) + return null; + Node con; + if( alloc ) { + Field ctor = clz.field("<new>"); + if( ctor == null || !(ctor._t instanceof TypeFunPtr tfp) ) { + String name = clzName.startsWith(Parser.CLZ) + ? clzName.substring(Parser.CLZ.length()) : clzName; + int dot = name.lastIndexOf('.'); + throw Parser.error("Unknown struct type '"+(dot==-1 ? name : name.substring(dot+1))+"'",fref._loc); + } + FunNode fun = code.link(tfp); + if( fun == null ) return null; + con = new FunPtrNode(tfp,code._start,fun.ret()).peephole(); + } else { + // Class pointer constant: + TypeMemPtr clzptr = TypeMemPtr.make((byte)2,clz,true); + con = code.con(clzptr); + } + // Replace and optimize + fref.addDef(con); + code.add(fref); + } + // Adds sort by NIDs, which just got shuffled + if( n instanceof AddNode add ) + code.add(add); + return null; + } ); + + // The complete public interface is now known. Keep this independent + // of later graph/linker liveness so SCCP computations remain local and + // monotonic. + code.freezePublicInterface(); + } + + // Parse one Simple source code file. Add all the FRefs produced to the worklist. + private static void parseOne( CodeGen code, CompUnit cunit ) { + cunit.setStart(code); + // Parse a source file, return missing external references + Ary<FRefNode> frefs = code.P.parse(cunit); + + // Find all missing external references, or complain. This can trigger more parses. + while( !frefs.isEmpty() ) { + FRefNode fref = frefs.remove(0); + // Symbol not found in source code (or we would not be here). + // Search the module for the fref + try { + CompUnit xcunit = findCUnitModule(code,cunit,fref._name); + if( xcunit != null ) { + // Replace the FRef with the discovered class name. + fref._name = Parser.addClzPrefix(xcunit._cname); + cunit.addDep(xcunit); + } else { + throw new RuntimeException("Undefined name '" + fref._name +"'"); + } + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + addRequiredTypes(code,cunit); + } + + private static void addRequiredTypes(CodeGen code, CompUnit cunit) { + LinkedHashSet<String> required = new LinkedHashSet<>(); + code._stop.walk(n -> { + TypeStruct ts = CodeGen.requiredStruct(n); + if( ts!=null && ts._fref ) required.add(ts._name); + return null; + }); + for( String typeName : required ) { + try { + CompUnit xcunit = findRequiredType(code,cunit,typeName); + if( xcunit != null ) + cunit.addDep(xcunit); + else + assert false : "expected to find required type"; + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + } + + private static CompUnit findRequiredType(CodeGen code, CompUnit cunit, String typeName) throws IOException { + for( CompUnit cu = cunit; cu != null; cu = cu._par ) { + String symbol = typeName.startsWith(cu._cname+".") + ? typeName.substring(cu._cname.length()+1) + : typeName; + CompUnit xcunit = findCUnitModule(code,cu,symbol); + if( xcunit != null ) + return xcunit; + } + return null; + } + + + // 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 + try { + CompUnit local = findCUnitModule(code,cu,symbol); + if( local != null ) + return local; + return null; + } catch( IOException ioe ) { + throw new RuntimeException(ioe); + } + } + + // Find the symbol in module, starting from cunit. This searches up the nested + // class chain and sideways 1 step, so that any prefix of the class chain can + // be skipped as a shortcut. Symbol can have dotted names, which then go + // down into classes. e.g. + // A/X/Y exists. + // A/B contains X.Y, which is a shortcut for A.X.Y + + // 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 ) { + if( !symbol.equals("sys") || !CompUnit.source("sys").exists() ) return null; + CompUnit sys=makeCUnit(code,null,"sys"); + enqueue(sys); + return sys; + } + + // Check self + if( cunit._name.equals(symbol) ) + return cunit; + + // If symbol is itself nested (has embedded dots), move the leading part + // over to the CompUnit path. So e.g.: + // CompUnit{A.B}, Symbol="X.Y" + // becomes + // CompUnit{A.B.X}, Symbol="Y" + int dotIdx = symbol.indexOf("."); + if( dotIdx != -1 ) { + String pre = symbol.substring(0,dotIdx); + String rest = symbol.substring(dotIdx+1 ); + String fname = cunit._fname + "/" + pre; + File f = CompUnit.source(fname); + if( f.exists() ) { + CompUnit sub = makeCUnit(code,cunit,pre); + // enqueue also parses parents: their initialization must precede a child. + CompUnit dcunit = findCUnitModule(code,sub,rest); + if( dcunit != null ) + return dcunit; // Nested dot search found cunit + } + } + + // Look in mod / fname / name / symbol.smp + String fname = cunit._fname + "/" + symbol; + File f = CompUnit.source(fname); + if( f.exists() ) { + CompUnit sub = makeCUnit(code,cunit,symbol); + // If not seen before or out of date, parse the new nested cunit + if( sub._clz == null && WORK.find(sub) == -1 ) + enqueue(sub); + return sub; + } + + // Look up the scope + return findCUnitModule( code, cunit._par, symbol ); + } + + +} diff --git a/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java b/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java index 777182dc..48319006 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java +++ b/src/main/java/com/seaofnodes/simple/codegen/RIPRelSize.java @@ -3,7 +3,7 @@ package com.seaofnodes.simple.codegen; // This Node rip-relative encodings has sizes that vary by delta public interface RIPRelSize { // delta is the distance from the opcode *start* to the target start. Each - // hardware target adjusts as needed, X86 measures from the opcode *end* + // hardware target adjusts as needed. X86 measures from the opcode *end* // and small jumps are 2 bytes, so they'll need to subtract 2. byte encSize(int delta); diff --git a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java index 565fa6ba..ac792988 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java +++ b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java @@ -57,6 +57,8 @@ public class RegAlloc { public int _spills, _spillScaled; + private boolean _done; + // ----------------------- // Live ranges with self-conflicts or no allowed registers private final IdentityHashMap<LRG,String> _failed = new IdentityHashMap<>(); @@ -68,6 +70,7 @@ public class RegAlloc { _failed.put(lrg,""); } boolean success() { return _failed.isEmpty(); } + public boolean done() { return _done; } // ----------------------- @@ -76,16 +79,16 @@ public class RegAlloc { short _lrg_num; // Define a new LRG, and assign n - LRG newLRG( Node n ) { + LRG newLRG( Node n, FunNode fun ) { LRG lrg = lrg(n); if( lrg!=null ) return lrg; - lrg = new LRG(_lrg_num++); + lrg = new LRG(_lrg_num++, fun); LRG old = _lrgs.put(n,lrg); assert old==null; return lrg; } // LRG for n - LRG lrg( Node n ) { + public LRG lrg( Node n ) { LRG lrg = _lrgs.get(n); if( lrg==null ) return null; LRG lrg2 = lrg.find(); @@ -142,18 +145,19 @@ public class RegAlloc { } // Printable register number for node n - String reg( Node n ) { return reg(n,null); } - String reg( Node n, FunNode fun ) { + String reg( Node n ) { LRG lrg = _lrg(n); - if( lrg==null ) return null; + return lrg==null ? null : reg(lrg); + } + String reg( LRG lrg ) { // No register yet, use LRG if( lrg._reg == -1 ) return "V"+lrg._lrg; // Chosen machine register unless stack-slot and past RA String[] regs = _code._mach.regs(); - if( lrg._reg < regs.length || _code._phase.ordinal() <= CodeGen.Phase.RegAlloc.ordinal() || fun==null ) + if( lrg._reg < regs.length || _code._phase.ordinal() <= CodeGen.Phase.RegAlloc.ordinal() || lrg._fun==null ) return RegMask.reg(regs,lrg._reg); // Stack-slot past RA uses the frame layout logic - return "[rsp+"+fun.computeStackOffset(_code,lrg._reg)+"]"; + return "[rsp+"+lrg._fun.computeStackOffset(_code,lrg._reg)+"]"; } // ----------------------- @@ -179,8 +183,7 @@ public class RegAlloc { for( Node n : bb._outputs ) if( n instanceof NewNode nnn ) nnn.cacheRegs(_code); } - - // Optional diagnostic for values accidentally shared across functions. + // Expensive assert: useful when chasing cross-function allocator edges. //assert verifyFunctionLocalEdges(); // Top driver: repeated rounds of coloring and splitting. @@ -192,23 +195,66 @@ public class RegAlloc { round++; } postColor(); // Remove no-op spills + _done = true; } boolean verifyFunctionLocalEdges() { - for( CFGNode bb : _code._cfg ) { - if( bb instanceof StartNode ) continue; - FunNode fun = bb.fun(); - for( Node use : bb._outputs ) - if( use instanceof MachNode mach && !(use instanceof ParmNode) ) - for( int i=1; i<use.nIns(); i++ ) { - Node def = use.in(i); - if( def==null || mach.regmap(i)==null || def.cfg0()==null || def.cfg0() instanceof StartNode ) continue; - assert def.cfg0().fun()==fun : "Cross-function register edge: "+def.uniqueName()+" -> "+use.uniqueName()+" input "+i; - } - } + for( CFGNode bb : _code._cfg ) + for( Node use : bb._outputs ) { + FunNode useFun = funOf(use); + for( int i=1; i<use.nIns(); i++ ) { + Node def = use.in(i); + if( def==null || def.isConst() || allowedCrossFunctionEdge(use,i,def) ) + continue; + FunNode defFun = funOf(def); + if( defFun != null && useFun != null && defFun != useFun ) + throw new AssertionError(crossFunctionMessage(def,defFun,use,useFun,i)); + } + } return true; } + private static String crossFunctionMessage(Node def, FunNode defFun, Node use, FunNode useFun, int idx) { + StringBuilder sb = new StringBuilder("Cross-function value edge: "); + sb.append(def.uniqueName()).append("(").append(defFun.label()).append(") -> ") + .append(use.uniqueName()).append("(").append(useFun.label()).append(") input ").append(idx) + .append(" def=").append(def).append(" use=").append(use).append("\n all uses:"); + for( Node out : def.outs() ) { + FunNode outFun = funOf(out); + sb.append("\n ").append(out.uniqueName()).append("(") + .append(outFun==null ? "?" : outFun.label()).append(")"); + } + return sb.toString(); + } + + private static boolean allowedCrossFunctionEdge(Node use, int idx, Node def) { + // Function metadata/control edges are not value live ranges. + if( use instanceof FunNode || use instanceof StopNode || use instanceof StopCUNode ) + return true; + // Return-linked function pointers carry function identity/lifetime, not + // an allocator value flowing from the callee body into this function. + if( use instanceof FunPtrNode && def instanceof ReturnNode ) + return true; + // Parm defaults and call-linked argument edges are the entry protocol; + // BuildLRG also skips Parm input 1. + if( use instanceof ParmNode ) + return true; + // Linked CallEnd -> Return edges should be gone before RegAlloc, but + // diagnose allocator value edges rather than call graph metadata here. + if( use instanceof CallEndNode && def instanceof ReturnNode ) + return true; + return false; + } + + private static FunNode funOf(Node n) { + if( n instanceof ParmNode parm ) return parm.fun(); + if( n instanceof ReturnNode ret ) return ret.fun(); + if( n instanceof FunNode fun ) return fun; + if( n instanceof CFGNode cfg ) return cfg.fun(); + CFGNode cfg = n.cfg0(); + return cfg==null ? null : cfg.fun(); + } + private boolean graphColor(byte round) { _failed.clear(); _lrgs.clear(); @@ -252,7 +298,7 @@ public class RegAlloc { // Register mask when empty; split around defs and uses with limited // register masks. - if( lrg._mask.isEmpty() && (!lrg._multiDef || lrg._1regUseCnt==1) ) { + if( lrg._mask.isEmpty() ) { if( lrg._1regDefCnt <= 1 && lrg._1regUseCnt <= 1 && (lrg._1regDefCnt + lrg._1regUseCnt) > 0 && @@ -265,6 +311,10 @@ public class RegAlloc { if( splitEmptyMaskByUse(round,lrg) ) return true; } + if( lrg._mask.isEmpty() && lrg._multiDef && lrg._1regUseCnt > 1 && lrg._1regDefCnt <= 1 ) { + if( splitEmptyMaskSingleUses(round,lrg) ) + return true; + } // Generic split-by-loop depth. return splitByLoop(round,lrg); @@ -277,18 +327,20 @@ public class RegAlloc { // single-register. Split after the def and before the use. Does not // require a full pass. + // Compute def nOuts *before* replacing those uses with a split + int defNouts = ((Node)lrg._machDef).nOuts(); // Split just after def if( lrg._1regDefCnt==1 && !lrg._machDef.isClone() ) - // An earlier copy is reusable only if no intervening clobber. - // Being in the same block alone is insufficient. Example: + // Force must-split, even if a prior split same block because register + // conflicts. Example: // alloc // V1/rax - forced by alloc // alloc // V2/rax - kills prior RAX // st4 [V1],len - No good, must split around - insertAfterAndReplace(makeSplit("def/empty1",round,lrg),(Node)lrg._machDef,false); + insertAfterAndReplace( makeSplit("def/empty1",round,lrg), (Node)lrg._machDef, false/*true*/); // Split just before use - if( lrg._1regUseCnt==1 ) + if( lrg._machUse!=null && (lrg._1regUseCnt==1 || (lrg._1regDefCnt==1 && defNouts==1)) ) insertBefore((Node)lrg._machUse,lrg._uidx,"use/empty1",round,lrg); return true; } @@ -331,6 +383,27 @@ public class RegAlloc { return true; } + // Empty-mask live range with several direct single-register uses, often + // through a Phi with multiple defs. Split those uses directly instead of + // doing a loop-depth split, which might keep isolating the wrong edge. + boolean splitEmptyMaskSingleUses( byte round, LRG lrg ) { + findAllLRG(lrg); + if( _ns._len <= 5 ) + return false; + boolean progress = false; + for( Node n : _ns ) { + if( !(n instanceof MachNode mach) ) continue; + for( int i=1; i<n.nIns(); i++ ) { + RegMask use = mach.regmap(i); + if( use != null && use.size1() && lrg(n.in(i))==lrg ) { + insertBefore(n,i,"use/emptyN",round,lrg); + progress = true; + } + } + } + return progress; + } + // Narrowing preserves all earlier users of a class; new classes are disjoint. // Each use contains its first overlapping class, so one pass suffices. private static void putIntoRegClass( Ary<RegMask> rclass, RegMask rmask ) { @@ -439,9 +512,11 @@ public class RegAlloc { int min = (int)ld; int max = (int)(ld>>32); - - // If the minLoopDepth is less than the maxLoopDepth: for-all defs and - // uses, if at minLoopDepth or lower, split after def and before use. + boolean emptyMulti = lrg._mask.isEmpty() && lrg._multiDef && lrg._1regDefCnt <= 1 && + (lrg._1regUseCnt > 1 || _ns._len > 5); + // Multi-def empty-mask conflicts need the hot/deep side isolated from + // fixed register uses. Ordinary pressure splits keep the old shallow + // boundary behavior. for( Node n : _ns ) { if( n instanceof SplitNode split && !split.isNonStackSplit() && min!=max ) continue; // Ordinary splits already reach stack storage. if( n.isDead() ) continue; // Some Cloneable went dead by other spill changes @@ -452,7 +527,7 @@ public class RegAlloc { if( lrg(n)==lrg && // This is a LRG def // At loop boundary, or splitting in inner loop - (min==max || n.cfg0().loopDepth() <= min) ) { + (min==max || (emptyMulti ? n.cfg0().loopDepth() >= max : n.cfg0().loopDepth() <= min)) ) { // Cloneable constants will be cloned at uses, not after def if( !(n instanceof MachNode mach && mach.isClone()) && // Single user is already a split adjacent @@ -467,7 +542,7 @@ public class RegAlloc { // No split in front of a split if( !(n.in(i) instanceof SplitNode) && // splitting in inner loop or at loop border - (min==max || phi.region().cfg(i).loopDepth() <= min) && + (min==max || (emptyMulti ? phi.region().cfg(i).loopDepth() >= max : phi.region().cfg(i).loopDepth() <= min)) && // and not around the backedge of a loop (bad place to force a split, hard to remove) !(phi.region() instanceof LoopNode && i==2 && (phi.in(i) instanceof PhiNode pp && pp.region()==phi.region())) ) // Split before phi-use in prior block @@ -479,7 +554,8 @@ public class RegAlloc { // This is a LRG use // splitting in inner loop or at loop border if( lrgSame( n.in( i ), lrg ) && - (min == max || (n.in(i) instanceof MachNode mach && mach.isClone()) || n.cfg0().loopDepth() <= min) ) + (min == max || (n.in(i) instanceof MachNode mach && mach.isClone()) || + (emptyMulti ? n.cfg0().loopDepth() >= max : n.cfg0().loopDepth() <= min)) ) // Split before in this block insertBefore( n, i, "use/loop/use", round,lrg, false ); } @@ -528,8 +604,8 @@ public class RegAlloc { void findAllLRG( LRG lrg ) { _ns.clear(); int wd = 0; - if( lrg._machDef!=null ) _ns.push((Node)lrg._machDef); - if( lrg._machUse!=null ) _ns.push((Node)lrg._machUse); + if( lrg._machDef != null ) _ns.push((Node)lrg._machDef); + if( lrg._machUse != null ) _ns.push((Node)lrg._machUse); while( wd < _ns._len ) { Node n = _ns.at(wd++); if( lrg(n)!=lrg ) continue; @@ -590,12 +666,12 @@ public class RegAlloc { // Clone simple constants if possible Node split = def instanceof MachNode mach && mach.isClone() && (umask==null || mach.outregmap().overlap(umask)) ? mach.copy() - : _code._mach.split(kind,round,lrg); + : _code._mach.split(lrg,kind,round); _lrgs.put(split,lrg); return split; } private SplitNode makeSplit( String kind, byte round, LRG lrg ) { - SplitNode split = _code._mach.split(kind,round,lrg); + SplitNode split = _code._mach.split(lrg,kind,round); _lrgs.put(split,lrg); return split; } @@ -699,6 +775,6 @@ public class RegAlloc { if( clobbers(n,defreg) ) return false; // Clobbered } - throw Utils.TODO(); + throw Utils.TODO("Should not reach here"); } } diff --git a/src/main/java/com/seaofnodes/simple/codegen/RegMask.java b/src/main/java/com/seaofnodes/simple/codegen/RegMask.java index 303bba9e..9cb9ec16 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/RegMask.java +++ b/src/main/java/com/seaofnodes/simple/codegen/RegMask.java @@ -33,7 +33,6 @@ public class RegMask { public RegMask(long bits ) { _bits0 = bits; } public RegMask(long bits0, long bits1 ) { _bits0 = bits0; _bits1 = bits1; } public RegMask(RegMask r) { _bits0 = r._bits0; _bits1 = r._bits1; } - private RegMask() { _bits0 = _bits1 = 0; } // AND, with copy-on-write if changing RegMask and( RegMask mask ) { diff --git a/src/main/java/com/seaofnodes/simple/codegen/SourceError.java b/src/main/java/com/seaofnodes/simple/codegen/SourceError.java new file mode 100644 index 00000000..f2f46146 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/codegen/SourceError.java @@ -0,0 +1,49 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.util.SB; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +/** Format one parser byte offset. Source is read and text built only on error. */ +public abstract class SourceError { + public static String format(CompUnit cu, int pos, String message) { + String file=cu.sourceFile(); + String name=file==null ? cu._fname+".smp" : file; + byte[] source; + try { + source=cu._src!=null ? cu._src.getBytes(StandardCharsets.UTF_8) + : file==null ? null : Files.readAllBytes(Path.of(file)); + } catch( IOException ex ) { source=null; } + if( source==null || pos<0 || pos>source.length ) + return name+": error: "+message+" (source unavailable at byte offset "+pos+")"; + + int line=1, start=0; + for( int i=0; i<pos; i++ ) + if( source[i]=='\n' ) { line++; start=i+1; } + int end=pos; + while( end<source.length && source[end]!='\n' ) end++; + if( end>start && source[end-1]=='\r' ) end--; + 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 "); + 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(' '); + width+=spaces; + } else { sb.p(new String(Character.toChars(ch))); width++; } + i+=Character.charCount(ch); + } + if( before.length()==text.length() ) caret=width; + sb.p("\n "); + for( int i=0; i<caret; i++ ) sb.p('-'); + return sb.p('^').toString(); + } +} diff --git a/src/main/java/com/seaofnodes/simple/codegen/StaticData.java b/src/main/java/com/seaofnodes/simple/codegen/StaticData.java new file mode 100644 index 00000000..616753d4 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/codegen/StaticData.java @@ -0,0 +1,177 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.BAOS; +import java.util.*; + +// Pool objects have identity independently of their initial field values. +// References in their contents use the same symbols in ELF and emulator images. +public final class StaticData { + public static final class ObjectData { + public final Type type; + public final String symbol; + public final boolean readOnly, external; + public final int alignment, size; + public int offset; + ObjectData(Type t, String name, boolean ro, boolean ext) { + type=t; symbol=name; readOnly=ro; external=ext; + alignment=t.alignment(); + int align=1<<alignment; + size=t instanceof TypeStruct ts ? (Math.max(1,ts.size())+align-1)&-align : align; + } + } + public record Relocation(ObjectData object, int offset, int logSize, Type value) {} + + final Encoding enc; + final LinkedHashMap<Object,ObjectData> objects = new LinkedHashMap<>(); + final ArrayList<Relocation> relocations = new ArrayList<>(); + final String prefix; + + public ArrayList<ObjectData> entries() { + ArrayList<ObjectData> entries=new ArrayList<>(objects.values()); + entries.sort(Comparator.comparingInt((ObjectData obj) -> obj.alignment).reversed()); + return entries; + } + + StaticData(Encoding enc) { + this.enc=enc; + prefix=enc._code.entryClinitName()+"$DATA$"; + } + + static boolean isClass(Type t) { + return t instanceof TypeStruct ts && Parser.startsClzPrefix(ts._name); + } + + static CompUnit owner(CodeGen code,TypeStruct ts) { + return owner(code,ts._name.substring(Parser.CLZ.length())); + } + static CompUnit owner(CodeGen code,String name) { + if(name==null) return null; + CompUnit owner=null; + for( CompUnit cu : code._compunits.values() ) + if( cu._cname!=null && (name.equals(cu._cname) || name.startsWith(cu._cname+".")) && + (owner==null || cu._cname.length()>owner._cname.length()) ) owner=cu; + return owner; + } + + ObjectData object(Type t) { + Object key=isClass(t) ? ((TypeStruct)t)._name + : t instanceof TypeFunPtr fp ? functionSymbol(fp) : t; + ObjectData old=objects.get(key); + if( old!=null ) return old; + boolean clz=isClass(t); + if( clz && Parser.TYPES.get(((TypeStruct)t)._name) instanceof TypeStruct canonical ) t=canonical; + CompUnit owner=clz ? owner(enc._code,(TypeStruct)t) + : t instanceof TypeFunPtr fp ? owner(enc._code,enc._code._fidxs.owner(fp.fidx())) : null; + String symbol=clz ? ((TypeStruct)t)._name.substring(Parser.CLZ.length())+".$class" + : t instanceof TypeFunPtr fp ? functionSymbol(fp) : prefix+objects.size(); + ObjectData obj=new ObjectData(t,symbol,!clz && (!(t instanceof TypeStruct ts) || ts.isConstant()), + owner!=null && owner._src==null); + objects.put(key,obj); + // Register before following fields: class objects may refer to each other. + if( !obj.external && t instanceof TypeStruct ts ) + for( Field f : ts._fields ) + if( !f._extern && f._t.isConstant() ) target(f._t); + return obj; + } + + private static boolean pointerConstant(TypeMemPtr ptr) { + return ptr.notNull() && (isClass(ptr._obj) || (ptr._obj.isConstant() && (!ptr._obj.isAry() || ptr._obj.isConAry()))); + } + + private void target(Type t) { + if( t instanceof TypeMemPtr ptr && pointerConstant(ptr) ) object(ptr._obj); + if( t instanceof TypeFunPtr fp && fp.notNull() && + enc._code.link(fp)==null && !enc._code._externFunc.containsKey(fp.fidx()) ) + // A dead function still needs a unique non-null identity for comparisons. + object(fp); + } + + void write() { + ArrayList<Encoding.Relo> refs=new ArrayList<>(enc._bigCons.values()); + refs.sort(Comparator.comparingInt(r -> r._op._nid)); + for( Encoding.Relo ref : refs ) object(ref._t); + // Definitions belong to their compilation unit, even if every local use folded. + ArrayList<CompUnit> units=new ArrayList<>(enc._code._compunits.values()); + units.sort(Comparator.comparing(cu -> cu._cname==null ? "" : cu._cname)); + for( CompUnit cu : units ) if( cu._src!=null && cu._clz!=null ) object(cu._clz); + for( ObjectData obj : entries() ) { + if( obj.external ) continue; + BAOS bits=obj.readOnly ? enc._cpool : enc._sdata; + Encoding.padN(1<<obj.alignment,bits); + obj.offset=bits.size(); + if( obj.type instanceof TypeStruct ts ) writeStruct(obj,ts,bits); + else Encoding.addN(obj.alignment,obj.type,bits); + while( bits.size()<obj.offset+obj.size ) bits.write(0); + } + for( Encoding.Relo ref : refs ) { + ObjectData obj=object(ref._t); + ref._target=obj.offset; + ref._opStart=enc.opStart(ref._op); + } + } + + private void writeStruct(ObjectData obj, TypeStruct ts, BAOS bits) { + int[] layout=ts.layout(); + for( int idx : layout ) { + if( idx==ts._fields.length ) continue; + Field f=ts._fields[idx]; + if( f._extern ) continue; + int off=ts.offset(idx); + while( bits.size()<obj.offset+off ) bits.write(0); + if( f._fname.equals("[]") ) ((TypeConAry)f._t).write(bits); + else { + Type value=f._t.isConstant() ? f._t : f._t.makeZero(); + int log=f._t.log_size(); + Encoding.addN(log,value,bits); + if( (value instanceof TypeFunPtr fp && fp.notNull()) || + (value instanceof TypeMemPtr ptr && pointerConstant(ptr)) ) + relocations.add(new Relocation(obj,off,log,value)); + } + } + while( bits.size()<obj.offset+ts.size() ) bits.write(0); + } + + String functionSymbol(TypeFunPtr fp) { + String ext=enc._code._externFunc.get(fp.fidx()); + return ext!=null ? ext : enc._code._fidxs.symbol(fp.fidx()); + } + + String targetSymbol(Type value) { + if( value instanceof TypeMemPtr ptr ) return object(ptr._obj).symbol; + TypeFunPtr fp=(TypeFunPtr)value; + return enc._code.link(fp)==null && !enc._code._externFunc.containsKey(fp.fidx()) + ? object(fp).symbol : functionSymbol(fp); + } + + void link(int cpool, int sdata) { + for( Relocation rel : relocations ) { + long address; + if( rel.value instanceof TypeMemPtr ptr ) address=address(object(ptr._obj),cpool,sdata); + else { + TypeFunPtr fp=(TypeFunPtr)rel.value; + String ext=enc._code._externFunc.get(fp.fidx()); + FunNode fun=enc._code.link(fp); + if( ext!=null ) address=enc.externalAddress(ext); + else if( fun==null ) address=address(object(fp),cpool,sdata); + else { + if( !enc._code.owns(fun) ) throw new IllegalArgumentException("Unlinked function "+functionSymbol(fp)); + address=enc.opStart(fun); + } + } + // Emulator native-call sentinels are signed 32-bit addresses. + if( rel.logSize==2 && (address<Integer.MIN_VALUE || address>0xffff_ffffL) ) + throw new IllegalArgumentException("Function address outside 32 bits: "+address); + BAOS bits=rel.object.readOnly ? enc._cpool : enc._sdata; + int off=rel.object.offset+rel.offset; + for( int i=0; i<(1<<rel.logSize); i++ ) bits.buf()[off+i]=(byte)(address>>>(8*i)); + } + } + + int address(ObjectData obj,int cpool,int sdata) { + if( obj.external ) throw new IllegalArgumentException("Unlinked class object "+obj.symbol); + return obj.offset+(obj.readOnly ? cpool : sdata); + } +} diff --git a/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java b/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java index 7dadb4d2..9b4b0995 100644 --- a/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java +++ b/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java @@ -5,34 +5,47 @@ import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.util.Ary; +// Reject invalid optimized programs before lowering erases their source-level +// obligations. Queue surviving CLInits during the same walk for Lower's Iter. abstract class TypeCheck { static void run(CodeGen code) { - Ary<Node> errs = new Ary<>(Node.class); - code._stop.walk(n -> { - if( n.err()!=null ) errs.add(n); - return null; - }); - if( errs.isEmpty() ) return; + // Check for class-init cycles first; such a cycle can prevent other + // things from resolving types. The dependency graph is temporary. + String clzInitCycle = new ClassInitDependencies(code).check(); + if( clzInitCycle != null ) + throw Parser.error(clzInitCycle,null); - // Publication is a producer check. Report an incomplete object before - // its users complain about values or permissions inherited from it. - for( Node n : errs ) - if( n instanceof EscapeNode ) throw n.err(); + Ary<Node> errs = new Ary<>(Node.class); + code._stop.walk( n -> { + if( n.err() != null ) + errs.add(n); + if( n instanceof CLInitNode ) code.add(n); + return null; + }); - // A bad Load now produces BOTTOM. Report the bad access before a - // Convert or arithmetic use that merely received its failed result. - Node first = errs.at(0); - for( Node n : errs ) - if( noBottomInputs(n) ) { first=n; break; } - for( Node n : errs ) - if( noBottomInputs(n) && - ((n instanceof MemOpNode nm && first instanceof MemOpNode fm && - nm._loc!=null && nm._loc.before(fm._loc)) || - (n instanceof CFGNode nc && first instanceof CFGNode fc && nc.idepth()<fc.idepth())) ) - first=n; - Parser.ParseException err = first.err(); - if( code._obs!=null ) code._obs.error(first,err.getMessage()); - throw err; + if( !errs.isEmpty() ) { + // Prefer an originating error over a secondary error caused by a + // BOTTOM input. If all errors are downstream-poisoned, retain + // the old first-error behavior. + Node min = null; + for( Node n : errs ) + if( noBottomInputs(n) ) + { min = n; break; } + if( min==null ) + min = errs.at(0); + for( Node n : errs ) + if( n!=min && + noBottomInputs(n) && + ((n instanceof MemOpNode nmop && + min instanceof MemOpNode mmop && + nmop._loc.before(mmop._loc)) || + (n instanceof CFGNode ncfg && + min instanceof CFGNode mincfg && + ncfg.idepth() < mincfg.idepth())) ) + min = n; + if( code._obs != null ) code._obs.error(min, min.err().getMessage()); + throw min.err(); + } } private static boolean noBottomInputs(Node n) { diff --git a/src/main/java/com/seaofnodes/simple/node/AddFNode.java b/src/main/java/com/seaofnodes/simple/node/AddFNode.java deleted file mode 100644 index 0547e29c..00000000 --- a/src/main/java/com/seaofnodes/simple/node/AddFNode.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeFloat; - -import java.util.BitSet; - -public class AddFNode extends ArithFNode { - public AddFNode(Node lhs, Node rhs) { super(lhs, rhs); } - public AddFNode(ArithNode n) { super(n); } - - @Override public String label() { return "AddF"; } - - @Override public String glabel() { return "+"; } - - @Override double doOp( double x, double y ) { return x+y; } - - @Override - public Node idealize() { - // Add of 0. - Type t2 = in(2)._type; - if ( t2.isConstant() && t2 instanceof TypeFloat i && i.value()==0 ) - return in(1); - - // Move constants to RHS: con+arg becomes arg+con - if ( in(1)._type.isConstant() && !t2.isConstant() ) - return swap12(); - - return null; - } - @Override Node copy(Node lhs, Node rhs) { return new AddFNode(lhs,rhs); } -} diff --git a/src/main/java/com/seaofnodes/simple/node/AddNode.java b/src/main/java/com/seaofnodes/simple/node/AddNode.java index aa0ee024..8835208b 100644 --- a/src/main/java/com/seaofnodes/simple/node/AddNode.java +++ b/src/main/java/com/seaofnodes/simple/node/AddNode.java @@ -8,6 +8,7 @@ public class AddNode extends ArithNode { public AddNode(Node lhs, Node rhs) { super(null, lhs, rhs); } public AddNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs, mode); } + @Override public String label() { return "Add"; } @Override public String op() { return "+"; } @@ -41,7 +42,7 @@ public class AddNode extends ArithNode { // Add of same to a multiply by 2 if( lhs==rhs && _mode==1 ) - return new ShlNode(null,lhs,com.seaofnodes.simple.Parser.con(1)); + return new ShlNode(null,lhs,CodeGen.CODE.con(1)); // Goal: a left-spine set of adds, with constants on the rhs (which then fold). diff --git a/src/main/java/com/seaofnodes/simple/node/AndNode.java b/src/main/java/com/seaofnodes/simple/node/AndNode.java index 2bb289e9..a737b703 100644 --- a/src/main/java/com/seaofnodes/simple/node/AndNode.java +++ b/src/main/java/com/seaofnodes/simple/node/AndNode.java @@ -7,6 +7,7 @@ import com.seaofnodes.simple.type.TypeInteger; public class AndNode extends ArithNode { public AndNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + @Override public String op() { return "&"; } @Override long doOp( long x, long y ) { return x & y; } @Override TypeInteger doOp(TypeInteger x, TypeInteger y) { diff --git a/src/main/java/com/seaofnodes/simple/node/ArithFNode.java b/src/main/java/com/seaofnodes/simple/node/ArithFNode.java deleted file mode 100644 index 7d219215..00000000 --- a/src/main/java/com/seaofnodes/simple/node/ArithFNode.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.print.ExprPrinter; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeFloat; - -public abstract class ArithFNode extends Node { - public ArithFNode(Node lhs, Node rhs) { super(null, lhs, rhs); } - protected ArithFNode(ArithNode n) { super(n); } - - @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { - return p.open().n(in(1)).p(glabel()).n(in(2)).close(); - } - - abstract double doOp(double x, double y); - - @Override - public Type compute() { - Type t1 = in(1)._type, t2 = in(2)._type; - if( t1.isHigh() || t2.isHigh() ) - return Type.TOP; - if( t1 instanceof TypeFloat f1 && - t2 instanceof TypeFloat f2 ) { - if( f1.isConstant() && f2.isConstant() ) - return TypeFloat.constant(doOp(f1.value(),f2.value())); - } - return t1.meet(t2); - } - -} diff --git a/src/main/java/com/seaofnodes/simple/node/ArithNode.java b/src/main/java/com/seaofnodes/simple/node/ArithNode.java index 5f0862b2..094b927f 100644 --- a/src/main/java/com/seaofnodes/simple/node/ArithNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ArithNode.java @@ -95,6 +95,8 @@ public abstract class ArithNode extends Node implements ModeNode { return null; if( unsupportedFloat || n instanceof BoolNode ) return Parser.error("Cannot '"+op+"' "+bad,loc); + if( n.nIns()==3 ) + return Parser.error("Cannot "+n.in(1)._type+" "+op+" "+n.in(2)._type,loc); return Parser.error("Cannot '"+op+"' "+bad,loc); } diff --git a/src/main/java/com/seaofnodes/simple/node/BoolNode.java b/src/main/java/com/seaofnodes/simple/node/BoolNode.java index 02421e43..dd0bc996 100644 --- a/src/main/java/com/seaofnodes/simple/node/BoolNode.java +++ b/src/main/java/com/seaofnodes/simple/node/BoolNode.java @@ -70,7 +70,7 @@ abstract public class BoolNode extends Node implements ModeNode { public Node idealize() { // Compare of same if( in(1)==in(2) ) - return this instanceof LT ? com.seaofnodes.simple.Parser.ZERO : ConstantNode.make(TRUE); + return this instanceof LT ? CodeGen.CODE.ZERO : ConstantNode.make(TRUE); // Can we decide int vs flt? Type t1 = in(1)._type, t2 = in(2)._type; if( t1==null || t2==null ) @@ -116,7 +116,8 @@ abstract public class BoolNode extends Node implements ModeNode { public static class EQ extends BoolNode { public EQ(Node lhs, Node rhs) { super(lhs,rhs); } public EQ(Node lhs, Node rhs, byte mode) { super(lhs,rhs, mode); } - public String op() { return "=="; } + + public String op() { return "=="; } TypeInteger doOp(TypeInteger i1, TypeInteger i2) { if( i1==i2 && i1.isConstant() ) return TRUE; if( i1._max < i2._min || i1._min > i2._max ) return FALSE; @@ -129,7 +130,8 @@ abstract public class BoolNode extends Node implements ModeNode { public static class NE extends BoolNode { public NE(Node lhs, Node rhs) { super(lhs,rhs); } public NE(Node lhs, Node rhs, byte mode) { super(lhs,rhs,mode); } - public String op() { return "!="; } + + public String op() { return "!="; } TypeInteger doOp(TypeInteger i1, TypeInteger i2) { if( i1==i2 && i1.isConstant() ) return TRUE; if( i1._max < i2._min || i1._min > i2._max ) return FALSE; @@ -142,7 +144,8 @@ abstract public class BoolNode extends Node implements ModeNode { public static class LT extends BoolNode { public LT(Node lhs, Node rhs) { super(lhs,rhs); } public LT(Node lhs, Node rhs, byte mode) { super(lhs,rhs, mode); } - public String op() { return "<" ; } + + public String op() { return "<" ; } TypeInteger doOp(TypeInteger i1, TypeInteger i2) { if( i1._max < i2._min ) return TRUE; if( i1._min >= i2._max ) return FALSE; @@ -155,7 +158,8 @@ abstract public class BoolNode extends Node implements ModeNode { public static class LE extends BoolNode { public LE(Node lhs, Node rhs) { super(lhs,rhs); } public LE(Node lhs, Node rhs, byte mode) { super(lhs,rhs,mode); } - public String op() { return "<="; } + + public String op() { return "<="; } TypeInteger doOp(TypeInteger i1, TypeInteger i2) { if( i1._max <= i2._min ) return TRUE; if( i1._min > i2._max ) return FALSE; @@ -172,7 +176,8 @@ abstract public class BoolNode extends Node implements ModeNode { super(lhs,rhs,(byte)1); assert mode==1; } - public String op() { return "u<" ; } + + public String op() { return "u<" ; } TypeInteger doOp(TypeInteger i1, TypeInteger i2) { if( Long.compareUnsigned(i1._max,i2._min) < 0 ) return TRUE; if( Long.compareUnsigned(i1._min,i2._max) >= 0 ) return FALSE; @@ -180,23 +185,4 @@ abstract public class BoolNode extends Node implements ModeNode { } Node copy(Node lhs, Node rhs) { return new ULT(lhs,rhs); } } - public static class EQF extends EQ { - public EQF(Node lhs, Node rhs) { super(lhs,rhs,(byte)2); } - @Override boolean doOp(double lhs, double rhs) { return lhs == rhs; } - @Override public boolean isFloat() { return true; } - @Override Node copy(Node lhs, Node rhs) { return new EQF(lhs,rhs); } - } - public static class LTF extends LT { - public LTF(Node lhs, Node rhs) { super(lhs,rhs,(byte)2); } - @Override boolean doOp(double lhs, double rhs) { return lhs < rhs; } - @Override public boolean isFloat() { return true; } - @Override Node copy(Node lhs, Node rhs) { return new LTF(lhs,rhs); } - } - public static class LEF extends LE { - public LEF(Node lhs, Node rhs) { super(lhs,rhs,(byte)2); } - @Override boolean doOp(double lhs, double rhs) { return lhs <= rhs; } - @Override public boolean isFloat() { return true; } - @Override Node copy(Node lhs, Node rhs) { return new LEF(lhs,rhs); } - } - } diff --git a/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java index e6f70c64..df9e4fff 100644 --- a/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java +++ b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java @@ -3,7 +3,11 @@ package com.seaofnodes.simple.node; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.util.Utils; + import java.util.BitSet; +import java.util.HashMap; +import java.util.IdentityHashMap; /** * A Phi for bulk memory. It covers all aliases except {@link #_aliases}; @@ -15,24 +19,31 @@ public class BulkMemPhiNode extends PhiNode { private static final BitSet EMPTY = new BitSet(); // Excluded Aliases; empty means NO exclusions. - // Each split clones the exclusions; published sets are never mutated. - public final BitSet _aliases; + // Directly visible by design. Empty sets share EMPTY; splitAlias performs + // copy-on-first-write and removes the node from GVN before mutation. + public BitSet _aliases; public BulkMemPhiNode(String label, Node... inputs) { this(label,EMPTY,inputs); } private BulkMemPhiNode(String label, BitSet aliases, Node... inputs) { - super(label,inputs); + super(label, inputs); _aliases = aliases; } - public boolean isSplit(int alias) { return _aliases.get(alias); } + public BulkMemPhiNode(BulkMemPhiNode phi) { + super(phi,phi._label); + _aliases = phi._aliases.isEmpty() ? EMPTY : (BitSet)phi._aliases.clone(); + } + + public BulkMemPhiNode(RegionNode r, Node sample) { + super(r,sample); + _aliases = EMPTY; + } - public BulkMemPhiNode(BulkMemPhiNode phi) { super(phi); _aliases = phi._aliases; } + public boolean isSplit(int alias) { return _aliases.get(alias); } - @Override public boolean isMem() { return true; } - @Override public String label() { return "BulkPhi"+_aliases; } @Override public Type compute() { @@ -52,7 +63,7 @@ public class BulkMemPhiNode extends PhiNode { // "Peek through" a MemMerge that covers this alias set on its default for( int i=1; i<nIns(); i++ ) if( in(i) instanceof MemMergeNode mmm && canPeek(mmm) ) { - setDef(i,CodeGen.CODE.add(mmm.in(1))); + setDef(i,mmm.in(1)); return this; } @@ -64,8 +75,8 @@ public class BulkMemPhiNode extends PhiNode { return slice(alias); } - for( int i=0; i<_outputs.size(); i++ ) { - int alias = outputAlias(_outputs.get(i)); + for( int i=0; i<nOuts(); i++ ) { + int alias = outputAlias(out(i)); if( alias!=0 ) return slice(alias); } @@ -90,20 +101,22 @@ public class BulkMemPhiNode extends PhiNode { // True if the MemMerge has no precise slice still covered by this Phi. private boolean canPeek(MemMergeNode mmm) { - return mmm.in(1)!=null && missingAlias(mmm)==0; + return missingAlias(mmm)==0; } // Return an alias required by an input but still covered by this Phi, or 0. private int inputAlias(Node n) { return switch(n) { + case ParmNode p -> { assert p._idx==1; yield 0; } case ProjNode proj -> 0; - case ParmNode parm -> 0; - case ConstantNode con -> 0; + case ConstantNode con -> { + assert !(con._con instanceof TypeMem tmem && tmem._alias != 1); + yield 0; + } case MemMergeNode mmm -> missingAlias(mmm); case MemOpNode mem -> unsplit(mem._alias); - case EscapeNode esc -> unsplit(esc._field._alias); case BulkMemPhiNode bulk -> missingAlias(bulk); - default -> throw new AssertionError("Unexpected memory input "+n); + default -> throw Utils.TODO("Should not reach here"); }; } @@ -113,17 +126,18 @@ public class BulkMemPhiNode extends PhiNode { // Normal graph-neighbor enqueueing flows from defs to uses, so retain // an explicit forward dependency to revisit this Phi when the user // sharpens. - addDepForwards(use); + use = addDepForwards(use); return switch(use) { case ScopeNode scope -> 0; + case ParmNode parm -> 0; case MemMergeNode mmm -> missingAlias(mmm); case BulkMemPhiNode bulk -> missingAlias(bulk); case MemOpNode mem -> unsplit(mem._alias); - case EscapeNode esc -> unsplit(esc._field._alias); case MemPhiNode phi -> unsplit(phi._alias); + case EscapeNode esc -> esc.pub()==this ? unsplit(esc.fld()._alias) : 0; default -> { - assert use instanceof ReturnNode || use instanceof CallNode || use instanceof ParmNode - : "Unexpected bulk-memory user "+use; + assert use instanceof ReturnNode || use instanceof CallNode || use instanceof CLInitNode + : "Unexpected bulk-memory user "+use.getClass().getSimpleName()+"#"+use._nid+": "+use; yield 0; } }; @@ -145,9 +159,9 @@ public class BulkMemPhiNode extends PhiNode { return 0; } - // Every explicit slot requests that alias, including a partial allocation - // input whose slot still points to this bulk Phi. - private int missingAlias(MemMergeNode mmm) { + // First explicit MemMerge slice still covered by this Phi. A user's slice + // may point to this Phi after a precise Phi folds; splitting is still valid. + private int missingAlias( MemMergeNode mmm ) { for( int alias=2; alias<mmm.nIns(); alias++ ) if( mmm.in(alias)!=null && mmm.alias(alias)!=mmm.in(1) && @@ -163,6 +177,7 @@ public class BulkMemPhiNode extends PhiNode { // parallel precise Phi already exists, reuse it instead of building a // duplicate for the same Region/alias pair. Node mem = precisePhi(alias); + assert ((TypeMem)mem._type)._alias==alias; BitSet aliases = ((BitSet)_aliases.clone()); aliases.set(alias); @@ -177,8 +192,7 @@ public class BulkMemPhiNode extends PhiNode { // Do not peephole bphi here, as BulkMemPhi can recursively start a // second bulk rewrite before this one has finished. Let the worklist // discover any further splits - bphi.setType(TypeMem.BOT); - Node bulk = bphi; + Node bulk = bphi.init(); CodeGen.CODE.add(bulk); CodeGen.CODE.add(mem); return aggregate(bulk,alias,mem); @@ -188,11 +202,19 @@ public class BulkMemPhiNode extends PhiNode { // which the bulk has already excluded. The named alias is newly supplied // (or replaces its old slice). MemMergeNode aggregate(Node bulk, int alias, Node precise) { - MemMergeNode mmm = new MemMergeNode(bulk); + MemMergeNode mmm = new MemMergeNode(null,bulk); for( int old = _aliases.nextSetBit(0); old >= 0; old = _aliases.nextSetBit(old+1) ) { assert old != alias; mmm.alias(old,precisePhi(old));} mmm.alias(alias,precise); assert checkMerge(mmm); + mmm.init(); + // Slicing changes representation, not the set of values in memory. + // Newly exposed precise inputs can carry conservative bulk escape + // summaries; do not let those invent escapes absent from this Phi. + TypeMem old = (TypeMem)_type; + TypeMem mem = (TypeMem) mmm._type; + mmm._type = TypeMem.make(mem._alias,mem._t,mem._one,mem._clz, + mem._final,old._escFs,old._escAs); return mmm; } @@ -204,14 +226,14 @@ public class BulkMemPhiNode extends PhiNode { mphi = new MemPhiNode("$"+alias,alias); mphi.addDef(region()); // Due to cycles, must set before calling peephole - mphi.setType(TypeMem.make(alias,Type.BOTTOM)); + mphi.setType(TypeMem.BOT.makeFrom(alias)); for( int i=1; i<nIns(); i++ ) mphi.addDef(CodeGen.CODE.add(preciseInput(in(i),alias))); return CodeGen.CODE.add(mphi); } MemPhiNode _findPhi(int alias) { - for( Node use : region()._outputs ) + for( Node use : region().outs() ) if( use instanceof MemPhiNode mphi && mphi._alias==alias ) return mphi; return null; diff --git a/src/main/java/com/seaofnodes/simple/node/CFGNode.java b/src/main/java/com/seaofnodes/simple/node/CFGNode.java index 6805c1ef..4f225fbe 100644 --- a/src/main/java/com/seaofnodes/simple/node/CFGNode.java +++ b/src/main/java/com/seaofnodes/simple/node/CFGNode.java @@ -1,13 +1,11 @@ package com.seaofnodes.simple.node; -import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.Ary; -import com.seaofnodes.simple.util.Utils; + import java.util.BitSet; -import java.util.HashMap; -import java.util.HashSet; +import java.util.IdentityHashMap; /** Control Flow Graph Nodes * <p> @@ -103,11 +101,79 @@ public abstract class CFGNode extends Node { return lhs; } + // Safe read of the control input of a data node. + public static CFGNode safeCFG(Node n) { + if( n==null ) return null; + if( !(n.in(0) instanceof CFGNode cfg) ) return null; + return cfg._safeCFG(); + } + private CFGNode _safeCFG() { + return switch(this) { + case CLInitNode init when init.bypass() -> init.cfg0()._safeCFG(); + case CallNode call when call.cend()._folding -> call.cfg0()._safeCFG(); + case CallEndNode cend when cend._folding -> cend.cfg(1)._safeCFG(); + case FunNode fun when fun._folding -> fun.cfg(1)._safeCFG(); + case ReturnNode ret when ret.fun()._folding -> ret.cfg0()._safeCFG(); + default -> this; + }; + } + + // True when this node dominates `sub` in the current idom tree. + public boolean dominates(CFGNode sub, Node dep) { + for( CFGNode cfg=sub; cfg!=null; cfg=cfg.idom() ) + if( cfg == this ) + return true; + else if( dep != null ) dep.addDep(cfg); + return false; + } + + public static CFGNode deepest(CFGNode a, CFGNode b) { + if( a == null ) return b; + if( b == null ) return a; + return b.idepth() > a.idepth() ? b : a; + } + + public static CFGNode earlyCFG(Node n) { + return earlyCFG(n,new IdentityHashMap<>(),new BitSet()); + } + + public static CFGNode earlyCFG(Node n, IdentityHashMap<Node,CFGNode> cache, BitSet active) { + if( n == null ) return null; + CFGNode cached = cache.get(n); + if( cached != null ) return cached; + if( active.get(n._nid) ) + return safeCFG(n); + active.set(n._nid); + CFGNode early = null; + for( Node x : n._inputs ) + early = deepest(early,defCFG(x,cache,active)); + active.clear(n._nid); + cache.put(n,early); + return early; + } + + private static CFGNode defCFG(Node n, IdentityHashMap<Node,CFGNode> cache, BitSet active) { + if( n == null || n.isConst() ) return null; // no impact on early cfg + return switch( n ) { + case PhiNode phi -> phi.region(); + case CFGNode cfg -> cfg; + case ProjNode projNode -> safeCFG( n ); + default -> earlyCFG( n, cache, active ); + }; + } + + public boolean sameFun(CFGNode that) { + FunNode fun = fun(); + return fun != null && fun == that.fun(); + } + // Find nearest enclosing FunNode public FunNode fun() { CFGNode cfg = this; - while( !(cfg instanceof FunNode fun) ) + while( !(cfg instanceof FunNode fun) ) { + if( cfg==null ) return null; cfg = cfg.idom(); + } return fun; } @@ -118,13 +184,13 @@ public abstract class CFGNode extends Node { public LoopTree _ltree; public int _pre; // Pre-order numbers for loop tree finding - static class LoopTree { - LoopTree _par; - final LoopNode _head; - int _depth; + public static class LoopTree { + public LoopTree _par; + public final LoopNode _head; + public int _depth; LoopTree(LoopNode head) { _head = head; } - @Override public String toString() { return "LOOP"+_head._nid; } - int depth() { + @Override public String toString() { return label(_head); } + public int depth() { return _depth==0 ? (_par==null ? 0 : (_depth = _par.depth()+1)) : _depth; } } @@ -133,23 +199,29 @@ public abstract class CFGNode extends Node { // Tag all CFG Nodes with their containing LoopNode; LoopNodes themselves // also refer to *their* containing LoopNode, as well as have their depth. // Start contains the function bodies at depth 0; real loops add depth. - public void buildLoopTree(StartNode start, StopNode stop) { - // Unlink all linked calls. This can remove RPC constants which - // shuffled the StartNode outputs so requires a while loop. - boolean done=false; - while(!done) { - done = true; - for( Node use : start._outputs ) - if( use instanceof FunNode fun ) - for( Node c : fun._inputs ) - if( c instanceof CallNode call ) - { call.unlink_all(); done=false; } + public void buildLoopTree( Ary<FunNode> funs, StopNode stop) { + _ltree = stop._ltree = CodeGen.CODE.XCTRL._ltree = new LoopTree((StartNode)this); + Ary<FunNode> work = new Ary<>(FunNode.class); + + BitSet post = new BitSet(); + int pre = 1; + _pre = pre++; + for( FunNode fun : funs ) { + if( fun!=null && !fun.isDead() && !fun._type.isHigh() ) { + work.push(fun); + if(fun.in(1) instanceof StartCUNode start && start._pre==0 ) { + start._pre = pre++; + start._ltree = start.cfg(1)._ltree = _ltree; + post.set(start._nid); + } + }; } - _ltree = stop._ltree = Parser.XCTRL._ltree = new LoopTree((StartNode)this); - _bltWalk(2,null,stop, new BitSet()); + for( int i=0; i<work._len; i++ ) + pre = work.at(i)._bltWalk(pre,work.at(i),stop, post, work); } - int _bltWalk( int pre, FunNode fun, StopNode stop, BitSet post ) { + + int _bltWalk( int pre, FunNode fun, StopNode stop, BitSet post, Ary<FunNode> work ) { // Pre-walked? if( _pre!=0 ) return pre; _pre = pre++; @@ -163,8 +235,12 @@ public abstract class CFGNode extends Node { } // Pre-walk for( Node use : _outputs ) - if( use instanceof CFGNode usecfg && !skip( usecfg ) ) - pre = usecfg._bltWalk( pre, use instanceof FunNode fuse ? fuse : fun, stop, post ); + if( use instanceof CFGNode usecfg ) { + if( skip( usecfg ) ) { + if( usecfg._pre==0 && usecfg instanceof FunNode fun2 ) work.add( fun2 ); + } else + pre = usecfg._bltWalk( pre, use instanceof FunNode fuse ? fuse : fun, stop, post, work ); + } // Post-order work: find innermost loop LoopTree inner = null, ltree; @@ -207,7 +283,7 @@ public abstract class CFGNode extends Node { return pre; } - private boolean skip(CFGNode usecfg) { + public boolean skip(CFGNode usecfg) { // Only walk control users that are alive. // Do not walk from a Call to linked Fun's. return usecfg instanceof XCtrlNode || @@ -215,8 +291,13 @@ public abstract class CFGNode extends Node { (this instanceof ReturnNode && usecfg instanceof CallEndNode); } - - public String label( CFGNode target ) { - return (target instanceof LoopNode ? "LOOP" : "L")+target._nid; + public static String label( CFGNode target ) { + return switch(target) { + case StartNode start -> start.label(); + case FunNode fun -> fun.label(); + case LoopNode loop -> "LOOP"+target._nid; + default -> "L"+target._nid; + }; } + } diff --git a/src/main/java/com/seaofnodes/simple/node/CLInitNode.java b/src/main/java/com/seaofnodes/simple/node/CLInitNode.java new file mode 100644 index 00000000..0a0de3cf --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/CLInitNode.java @@ -0,0 +1,165 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.print.ExprPrinter; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.CompUnit; +import com.seaofnodes.simple.type.*; +import java.util.*; + +// Track class initializers - the code just after this point expects the named +// class to be initialized. If this code path dies, then the dependency dies +// also - dead code does not force class init. +// +public class CLInitNode extends CFGNode implements MultiNode { + public Parser.Lexer _loc; // For error reporting + + // Inputs: control, whole memory, class pointer, loaded $init flag + // Results: control, whole memory, known-true flag alias memory. + // For field references, the ptr might be the dst clz ptr or not. + public CLInitNode(Parser.Lexer loc, Node ctrl, Node mem, Node ptr, Node ready) { + super(ctrl,mem,ptr,ready); + _loc = loc; + } + + + public CLInitNode(CLInitNode init) { + super(init); + _loc = init._loc; + } + + public Node mem() { return in(1); } + public Node ptr() { return in(2); } + public Node ready() { return in(3); } + + // The field/alias can be unresolved until forward references are loaded. + public Field flag() { + return ptr()._type instanceof TypeMemPtr tmp && Parser.startsClzPrefix(tmp._obj._name) + ? tmp._obj.field("$init") : null; + } + @Override public String label() { return "CLInit"; } + + // Best effort dst compunit, based on ptr - or null + public CompUnit cu() { + if( !(ptr()._type instanceof TypeMemPtr ptr && Parser.startsClzPrefix(ptr._obj._name)) ) + return null; + // Convert "class:sys.ary" into "sys/ary" and lookup CompUnit + String fname = ptr._obj._name.substring(6).replace(".","/"); + return CodeGen.CODE._compunits.get(fname); + } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + FunNode fun = fun(); + CompUnit cu = cu(); + return p.p("CLInit ").p(fun._name).p(" -> ").p(cu==null ? "---" : cu._cname); + } + + // Block head is Start, Region, CProj, but not e.g. If, Return, Stop + public boolean blockHead() { return true; } + + private boolean instance() { + return ptr()._type==Type.NIL || + (ptr()._type instanceof TypeMemPtr tmp && tmp._obj!=TypeStruct.BOT && + !tmp._obj.isHigh() && !tmp._obj._open && !tmp._obj._fref && + !Parser.startsClzPrefix(tmp._obj._name)); + } + + public boolean bypass() { return instance() || ready()._type==TypeInteger.TRUE; } + + @Override public Type compute() { + if( in(0)._type.isHigh() || ptr()._type.isHigh() ) + return TypeTuple.make(Type.XCONTROL,TypeMem.TOP,TypeMem.TOP); + if( instance() ) + return TypeTuple.make(in(0)._type,mem()._type,mem()._type); + Field flag = flag(); + Type slice = flag==null ? TypeMem.BOT : TypeMem.make(flag._alias,TypeInteger.TRUE); + // The initializer may write arbitrary public memory. Only its flag + // has a guaranteed value on the normal continuation. + Type bulk = ready()._type.isHigh() ? TypeMem.TOP : + ready()._type==TypeInteger.TRUE ? mem()._type : TypeMem.BOT; + return TypeTuple.make(in(0)._type,bulk,slice); + } + + @Override public Node pcopy(int idx, Node dep) { + dep.addDep(ptr()); + dep.addDep(ready()); + if( instance() ) return idx==0 ? in(0) : mem(); + if( ready()._type!=TypeInteger.TRUE ) return null; + if( idx==0 ) return in(0); + if( idx==1 ) return mem(); + // Already established on the incoming path; this is a memory fact, + // not a new store to the class object. + return CodeGen.CODE.con(((TypeTuple)_type)._types[2]); + } + + @Override public Node idealize() { + if( CodeGen.CODE._phase!=CodeGen.Phase.Lower || bypass() || in(0)._type.isHigh() ) return null; + if( CodeGen.CODE._midAssert ) return this; + // Until entry arguments become arrays, ordinary initializers receive zero. + return expand(CodeGen.CODE.con(TypeInteger.ZERO)); + } + + // Called only after cycle/type checking. The initialized arm skips the + // call; both arms rejoin with whole memory and a known-true flag slice. + private Node expand(Node arg) { + FunNode target = CodeGen.CODE.link(cu()); + assert target!=null && !target.isDead(); + keep(); + Node ctrl = in(0); + Node memory = mem(); + + // A child body may assume its parent has finished. Establish that + // requirement first, then test the child's flag in the resulting memory. + CompUnit parent = cu()._par; + if( parent!=null ) { + Node ptr = CodeGen.CODE.con(TypeMemPtr.make((byte)2,parent._clz,true)); + Field flag = parent._clz.field("$init"); + Node off = CodeGen.CODE.con(parent._clz.offset(parent._clz.find("$init"))); + Node parentReady = CodeGen.CODE.add(new LoadNode(_loc,"$init",flag._alias,ctrl,memory,ptr,off).init()); + CLInitNode init = CodeGen.CODE.add(new CLInitNode(_loc,ctrl,memory,ptr,parentReady).init()); + ctrl = CodeGen.CODE.add(new CProjNode(init,0,ScopeNode.CTRL).init()); + memory = CodeGen.CODE.add(new ProjNode(init,1,ScopeNode.MEM0).init()); + } + + Node childOff = CodeGen.CODE.con(cu()._clz.offset(cu()._clz.find("$init"))); + // Read the child's flag after establishing its parent's memory effects. + Node ready = CodeGen.CODE.add(new LoadNode(_loc,"$init",flag()._alias,ctrl,memory,ptr(),childOff).init()); + + IfNode iff = CodeGen.CODE.add(new IfNode(ctrl,ready).init()); + Node done = CodeGen.CODE.add(new CProjNode(iff,0,"True").init()); + Node cold = CodeGen.CODE.add(new CProjNode(iff,1,"False").init()); + + Node fptr = new FunPtrNode(target.sig(),CodeGen.CODE._start,target.ret()).peephole(); + CallNode call = CodeGen.CODE.add(new CallNode(_loc,cold,memory,ptr(),arg,fptr).init()); + CallEndNode end = new CallEndNode(call,TypeRPC.constant(CodeGen.CODE.rpc(_loc._cu._cname))); + // This is a lowered call, after interprocedural analysis. Do not add + // new parameter flows to a body already optimized by SCCP. + CodeGen.CODE.add(end.init()); + Node called = CodeGen.CODE.add(new CProjNode(end,0,ScopeNode.CTRL).init()); + Node after = CodeGen.CODE.add(new ProjNode(end,1,ScopeNode.MEM0).init()); + RegionNode region = CodeGen.CODE.add(new RegionNode(_loc,null,done,called).init()); + Node merged = CodeGen.CODE.add(new BulkMemPhiNode(ScopeNode.MEM0,region,memory,after).init()); + + // Replace the individual outputs; no tuple-pack node or second + // representation of CLInit is needed while the projections collapse. + for( Node use : _outputs.asAry() ) { + if( use==null ) continue; // Our temporary keep. + assert use instanceof Proj; + int idx = ((Proj)use).idx(); + use.subsume(idx==0 ? region : idx==1 ? merged : CodeGen.CODE.con(((TypeTuple)_type)._types[2])); + } + CodeGen.CODE.invalidateIDepthCaches(); + return unkeep(); // Iter removes this now-unused node. + } + + // Resolve a precise flag slice even when the parser did not yet know the + // class/alias and could only install the whole-memory projection. + static Node slice(Node mem, int alias, Node dep) { + if( !(mem instanceof ProjNode proj && proj._idx==1 && proj.in(0) instanceof CLInitNode init) ) + return mem; + dep.addDep(init.ptr()); + Field flag = init.flag(); + if( flag==null || flag._alias!=alias ) return mem; + return new ProjNode(init,2,Parser.memName(alias)).peephole(); + } +} diff --git a/src/main/java/com/seaofnodes/simple/node/CProjNode.java b/src/main/java/com/seaofnodes/simple/node/CProjNode.java index c62de840..2bd48d92 100644 --- a/src/main/java/com/seaofnodes/simple/node/CProjNode.java +++ b/src/main/java/com/seaofnodes/simple/node/CProjNode.java @@ -2,13 +2,14 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; -import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeMem; import com.seaofnodes.simple.type.TypeTuple; -import com.seaofnodes.simple.util.Utils; -public class CProjNode extends CFGNode { +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class CProjNode extends CFGNode implements Proj { // Which slice of the incoming multipart value public int _idx; @@ -23,6 +24,7 @@ public class CProjNode extends CFGNode { } public CProjNode(CProjNode c) { super(c); _idx = c._idx; _label = c._label; } + @Override public String label() { return _label; } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p(_label); } @@ -41,17 +43,17 @@ public class CProjNode extends CFGNode { public Node idealize() { if( ctrl()._type instanceof TypeTuple tt ) { if( tt._types[_idx]==Type.XCONTROL ) - return Parser.XCTRL; // We are dead + return CodeGen.CODE.XCTRL; // We are dead if( ctrl() instanceof IfNode && tt._types[1-_idx]==Type.XCONTROL ) // Only true for IfNodes return ctrl().in(0); // We become our input control } // Flip a negating if-test, to remove the not - if( ctrl() instanceof IfNode iff && addDep(iff.pred()) instanceof NotNode not ) + if( ctrl() instanceof IfNode iff && iff.pred() != null && addDep(iff.pred()) instanceof NotNode not ) return new CProjNode(new IfNode(iff.ctrl(),not.in(1)).peephole(),1-_idx,_idx==0 ? "False" : "True"); // Copy of some other input - return ((MultiNode)ctrl()).pcopy(_idx); + return ((MultiNode)ctrl()).pcopy(_idx,this); } // Only called during basic-block layout, inverts a T/F CProj @@ -66,4 +68,6 @@ public class CProjNode extends CFGNode { @Override int hash() { return _idx; } + @Override public int idx() { return _idx; } + } diff --git a/src/main/java/com/seaofnodes/simple/node/CallEndMach.java b/src/main/java/com/seaofnodes/simple/node/CallEndMach.java index d26ea33b..05707132 100644 --- a/src/main/java/com/seaofnodes/simple/node/CallEndMach.java +++ b/src/main/java/com/seaofnodes/simple/node/CallEndMach.java @@ -14,13 +14,15 @@ public class CallEndMach extends CallEndNode implements MachNode { _retMask = code._mach.retMask(call().tfp()); // Kill mask is all caller-saves, and any mirror stack slots for args // in registers. - RegMaskRW kills = code._callerSave.copy(); + RegMaskRW kills = (call().external() ? code._cCallerSave : code._callerSave).copy(); // Start of stack slots int maxReg = code._mach.regs().length; // Incoming function arg slots, all low numbered in the RA int fslot = fun()._maxArgSlot; // Killed slots for this calls outgoing args - int xslot = code._mach.maxArgSlot(call().tfp()); + int xslot = call().external() + ? code._mach.maxArgSlot(call().tfp(),code._cCallingConv) + : code._mach.maxArgSlot(call().tfp()); _xslot = (maxReg+fslot)+xslot; for( int i=0; i<xslot; i++ ) kills.set((maxReg+fslot)+i); diff --git a/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index 86c6c79d..4422265f 100644 --- a/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -2,27 +2,47 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; -import com.seaofnodes.simple.*; +import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.type.*; +import java.util.HashMap; +import java.util.IdentityHashMap; + /** * CallEnd */ public class CallEndNode extends CFGNode implements MultiNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined - private boolean _folding; - public boolean folding() { return _folding; } + boolean _folding; + // +2: must clone, so await all cleanup but OK + // +1: trivial now, so can inline without cloning and without waiting for cleanup + // 0: take a look, + // -1: somethings wrong, but might improve + // -2: never and stop asking, + private byte _inline; public final TypeRPC _rpc; - public CallEndNode(CallNode call) { super(new Node[]{call}); _rpc = TypeRPC.constant(_nid); } + public CallEndNode(CallNode call, TypeRPC rpc) { + super(new Node[]{call}); + _rpc = rpc; + } public CallEndNode(CallEndNode cend) { super(cend); _rpc = cend._rpc; } + public CallEndNode(TypeRPC rpc) { + super(new Node[0]); + _rpc = TypeRPC.constant(CodeGen.CODE._rpcs.next(rpc.rpc())); + } + private CallEndNode(int nIns, TypeRPC rpc) { + super(new Node[nIns]); + _rpc = rpc; + } + - @Override public String label() { return "CallEnd"; } @Override public boolean blockHead() { return true; } public CallNode call() { return (CallNode)in(0); } + public boolean folding() { return _folding; } @Override public CFGNode idom(Node dep) { // Folding the idom is the one inlining Return @@ -41,216 +61,261 @@ public class CallEndNode extends CFGNode implements MultiNode { @Override public Type compute() { if( !(in(0) instanceof CallNode call) || call._type != Type.CONTROL ) - return TypeTuple.RET.dual(); - // Mid-fold, just take the one single callers' return type - if( _folding ) return in(1)._type; + return TypeTuple.STATE.dual(); // Grab the TFP and use the functions declared return type. + // If the call.fptr() is a FRef, return will be BOTTOM. Type ftype = addDep(call.fptr())._type; - Type ret = ftype.isHigh() ? Type.TOP : Type.BOTTOM; - if( ftype instanceof TypeFunPtr tfp ) { - ret = tfp.ret(); - // Here, if I can figure out I've found *all* callers, then I can meet - // across the linked returns and join with the function return type. - if( 1+tfp.nfcns() == nIns() ) { // A linked function for every concrete function - ret = Type.TOP; - for( int i=1; i<nIns(); i++ ) { - Type tret = in(i)._type instanceof TypeTuple rtt ? rtt.ret() : in(i)._type; - ret = ret.meet(tret); - } - // Recursion is a loop in disguise. For f(x-1)+1, the result - // flows Return -> CallEnd -> Add -> Phi -> Return, without - // crossing ParmNode.compute's widening point. Widen here too, - // using the loop rule in PhiNode.compute. Keep constants and - // above-center ranges narrow, and stay within the return type. - Type oldret = _type instanceof TypeTuple tt ? tt.ret() : Type.TOP; - if( ret instanceof TypeInteger ti && ret != oldret && - !ti.isHigh() && !ti.isConstant() && - (!(oldret instanceof TypeInteger oldi) || ti._widen <= oldi._widen) ) - ret = ti.same_but_slightly_wider_than(TypeInteger.BOT).join(tfp.ret()); - } - } - TypeMem mem=TypeMem.TOP; - for( int i=1; i<nIns(); i++ ) { - Type state=in(i)._type; - if( state instanceof TypeTuple tt ) mem=(TypeMem)mem.meet(tt._types[1]); - else if( !state.isHigh() ) mem=TypeMem.BOT; + if( !(ftype instanceof TypeFunPtr tfp) ) + return ftype.isHigh() ? TypeTuple.STATE.dual() : TypeTuple.STATE; + // Mid-fold, just take the one single callers' return type + if( _folding ) { + TypeTuple tt = (TypeTuple)in(1)._type; + return tt.makeFrom(2,tt.ret()); } - boolean known=false, external=false; - if( ftype instanceof TypeFunPtr tfp && !XInt.isHigh(tfp.fidxs()) ) { - int locals=0; - for( int fidx=XInt.next(tfp.fidxs(),-1); fidx>=0; fidx=XInt.next(tfp.fidxs(),fidx) ) { - if( CodeGen.CODE.externFunc(tfp.makeFrom(fidx))!=null ) external=true; - else locals++; + + // Here, if I can figure out I've linked *all* callers, then I can meet + // across the linked returns and join with the function return type. + + // External functions have an exact FIDX but deliberately no local + // FunNode/Return edge. They conservatively crush public memory and + // produce their declared return type. A missing non-external target + // is still an unresolved/error call. + int externs = 0; + // The signature bounds widening (e.g. an i32 result stays i32). + // Use the callee signature, not FunPtrNode's live recursive range. + Type bound = tfp.ret(); + if( !XInt.isHigh(tfp.fidxs()) ) { + bound = Type.TOP; + for( int fidx = XInt.next(tfp.fidxs(),0); fidx >= 0; fidx = XInt.next(tfp.fidxs(),fidx) ) { + if( CodeGen.CODE.externFunc(fidx)!=null ) + externs++; + FunNode fun = CodeGen.CODE._link(fidx); + bound = bound.meet(fun==null ? tfp.ret() : addDep(fun).sig().ret()); } - known=locals==nIns()-1; } - if( external || (ftype instanceof TypeFunPtr tfp && XInt.isHigh(tfp.fidxs())) ) { - // Unknown code may retain arguments and all addresses already - // exposed to it. Heap values are arbitrary after this call; - // exposure itself still has a useful finite summary. - mem=(TypeMem)mem.meet(call.exposed(this)); - } else if( !known && (CodeGen.CODE._phase.ordinal()<CodeGen.Phase.Opto.ordinal() || - CodeGen.CODE._phase.ordinal()>CodeGen.Phase.FinalizeIR.ordinal()) ) mem=TypeMem.BOT; - return TypeTuple.make(call._type,mem,ret); + boolean hasExtern = externs != 0; + + // If we have an error-call, e.g. wrong args, then we never link. + // Pre-Opto this looks like a missing target fcn, and we assume it will + // appear later - meanwhile, we use a conservative approx of memory + // effects. + if( (nIns()-1)+externs < tfp.nfcns() ) { + // If during Opto, assume call won't be called and thus won't return anything. + // Lowered calls deliberately have no links into optimized bodies. + boolean opto = CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Opto.ordinal() && + CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Lower.ordinal(); + Type tmem = opto ? TypeMem.TOP : TypeMem.BOT; + Type ret = opto ? Type.TOP : tfp.ret(); + // Use the same widening before and after linking a callee. + return TypeTuple.make(Type.CONTROL,tmem,widenResult(ret,bound)); + } + + // A linked function for every concrete function + TypeTuple state = hasExtern + ? TypeTuple.make(Type.CONTROL,call.exposed(this),tfp.ret()) + : TypeTuple.STATE.dual(); + for( int i=1; i<nIns(); i++ ) + state = (TypeTuple)state.meet(in(i)._type); + // A reachable call whose presently linked exits are all unreachable + // keeps a conservative continuation. In particular, do not let a + // transient no-return result tear down the caller before/during SCCP. + // If the call remains out-of-line, its continuation can conservatively + // contribute class-init dependencies even though it never executes. + if( state.ctl()==Type.XCONTROL ) + return TypeTuple.make(Type.CONTROL,TypeMem.TOP,Type.TOP); + // Join before widening: FunPtrNode follows the live Return type, so + // joining it afterward would undo the widening. + return state.makeFrom(2,widenResult(state.ret().join(tfp.ret()),bound)); + } + + private static Type widenResult(Type ret, Type bound) { + // Recursion is a loop in disguise. For f(x-1)+1, the result + // flows Return -> CallEnd -> Add -> Phi -> Return, without + // crossing ParmNode.compute's widening point. Widen here too, + // using the loop rule in PhiNode.compute. Keep constants and + // above-center ranges narrow. + if( ret instanceof TypeInteger ti && !ti.isHigh() && !ti.isConstant() ) + ret = ti.same_but_slightly_wider_than().join(bound); + return ret; } @Override public Node idealize() { + return null; + } - // Trivial inlining: call site calls a single function; single function - // is only called by this call site. - if( !_folding && nIns()==2 && in(0) instanceof CallNode call ) { - Node fptr = call.fptr(); - if( fptr.nOuts() == 1 && // Only user is this call - fptr instanceof ConstantNode && // We have an immediate call - // Function is being called, and its not-null - fptr._type instanceof TypeFunPtr tfp && tfp.notNull() && - // Arguments are correct - call.err()==null ) { - ReturnNode ret = (ReturnNode)in(1); - FunNode fun = ret.fun(); - // Expecting Start, and the Call - if( fun.nIns()==3 ) { - assert fun.in(1) instanceof StartNode && fun.in(2)==call; - // Disallow self-recursive inlining (loop unrolling by another name). - // Disallow if still folding other things, as it makes other - // dependency checks carry long chains of half-folded calls. - // Dead control may end the walk before an enclosing function. - CFGNode idom = call; - while( idom!=null ) { - idom = idom.idom(); - if( idom instanceof FunNode ) break; - if( idom instanceof CallEndNode cend && cend._folding ) break; - } - // Inline? - if( idom instanceof FunNode fun2 && fun2 != fun && !fun2._folding ) { - // Trivial inline: rewrite - _folding = true; - // Rewrite Fun so the normal RegionNode ideal collapses - fun._folding = true; - fun.setDef(1,Parser.XCTRL); // No default/unknown StartNode caller - fun.setDef(2,call.ctrl()); // Bypass the Call; - fun.ret().setDef(3,null); // Return is folding also - CodeGen.CODE.addAll(fun._outputs); - // Repeat defs 1 layer down, for users of Parm (Phis) - for( Node parm : fun._outputs ) - if( parm instanceof ParmNode ) - CodeGen.CODE.addAll(parm._outputs); - - // Inlining immediately blows all cache idepth fields past the inline point. - // Bump the global version number invalidating them en-masse. - CodeGen.CODE.invalidateIDepthCaches(); - return this; - } else if( idom!=null ) { - addDep(idom); - } - } else { - addDep(fun); - } - } else { // Function ptr has multiple users (so maybe multiple call sites) - addDep(fptr); - } - } + public byte maybeInline() { + byte inline = inlineCandidate(); + if( CodeGen.CODE._midAssert ) + return inline; + _inline = inline; + if( _inline > 0 ) + doInline(); - return null; + return _inline; } - // Clone only tiny, straight-line boilerplate, after ordinary peeps settle. - // Larger bodies still use the existing single-caller, non-cloning inline. - public boolean inlineSmall() { - if( isDead() || _folding || nIns()!=2 || !(in(0) instanceof CallNode call) ) return false; - Node fptr = call.fptr(); - if( !(fptr instanceof ConstantNode) || - !(fptr._type instanceof TypeFunPtr tfp && tfp.notNull() && tfp.isConstant()) ) - { addDep(fptr); return false; } - if( call.err()!=null ) return false; + // Check if candidate right now, sets no flags + private byte inlineCandidate() { + if( _inline < -2 ) return -2; // Never + if( _folding ) return -2; // No, and will collapse anyways + if( !(in(0) instanceof CallNode call) ) return -2; // malformed because mid-death + + if( nIns()!=2 ) return -1; // Not exactly 1 link, but can change + + // Have exactly 1 linked function ReturnNode ret = (ReturnNode)in(1); FunNode fun = ret.fun(); - if( fun._folding ) return false; - int len = smallBody(fun); - if( len==0 ) return false; - // All callers consume the three normal return projections. - for( Node use : _outputs ) - if( !((use instanceof ProjNode p && p._idx>=0 && p._idx<3) || - (use instanceof CProjNode cp && cp._idx==0)) ) return false; - - // Substitute control and arguments directly; no new function identity, - // parameter Phis, or return address is needed for this straight line. - Node[] copy = new Node[len]; - for( int i=0; i<len; i++ ) { - Node n = TINYBODY[i]; - copy[i] = n==fun ? call.ctrl() : - n instanceof ParmNode parm ? (parm._idx==0 ? null : call.arg(parm._idx)) : - n==ret ? null : n.copyEmpty(); - } - for( int i=0; i<len; i++ ) { - Node n = TINYBODY[i]; - if( n instanceof StoreNode || n instanceof MemMergeNode || - n instanceof NewNode || n instanceof ProjNode || n instanceof EscapeNode ) { - for( Node in : n._inputs ) copy[i].addDef(mapped(in,copy)); - CodeGen.CODE.add(copy[i]); - } - } - Node[] result = new Node[3]; - for( int i=0; i<3; i++ ) result[i] = mapped(ret.in(i),copy).keep(); - keep(); - call.unlink_all(); - CodeGen.CODE.add(fun); - CodeGen.CODE.addAll(fun._outputs); - // subsume kills each projection; retain CallEnd until all are replaced. - while( nOuts()>1 ) { - Node use = out(0)==null ? out(1) : out(0); - int idx = use instanceof ProjNode p ? p._idx : ((CProjNode)use)._idx; - CodeGen.CODE.addAll(use._outputs); - use.subsume(result[idx]); - } - unkill(); - for( Node n : result ) n.unkill(); - CodeGen.CODE.invalidateIDepthCaches(); - return true; - } - private static Node mapped(Node n, Node[] copy) { - for( int i=0; i<copy.length; i++ ) - if( TINYBODY[i]==n ) return copy[i]; - return n; // Constants outside the body are shared. + // Preserve initializer boundaries until post-Opto cycle validation. + if( fun.isClz() ) return -2; + + // Heuristic forced inlining turned off via name + if( fun._name != null && + fun._name.endsWith("_noInline") ) + return -2; // Never, stop asking + + // Disallow self-recursive inlining (loop unrolling by another name). + for( int i=1; i < fun.nIns(); i++ ) + if( fun.cfg(i).fun()==fun ) // Check for linked call input inside "fun" + { addDep(fun); return -1; } // No, but can lose the recursive edge + + // Things that might improve, but not now a candidate + Node fptr = call.fptr(); + // Constant Function is being called, and its not-null + if( !(fptr._type instanceof TypeFunPtr tfp && tfp.notNull() && tfp.isConstant()) ) + { addDep(fptr); return -1; } + + // Arguments are correct + if( call.nargs()!=tfp.nargs() || !goodArgs(call,tfp) ) + { addDep(fptr); return -1; } + + // Encouraged inlining because small size and constructor. + int maxSize = fun.inlineLimit(); + // A large parse can leave only a tiny initializer. Recognize that + // and await cleanup to shrink the body. + if( fun._approxUIDs >= maxSize ) { + if( !smallBody(fun) ) return -1; + } else if( fun.body().cardinality() >= maxSize ) // Expensive size check + return -1; // Try later (if cleanup makes the real body small enough) + + // Can be cloned but not trivial + if( fun.nIns() > 2 ) { addDep(fun); return 2; } + + // A trivial candidate right now + return 1; } - // Bounded recognition of initializer boilerplate. Walk uses from the entry - // and stop at Return, never following callers or constant inputs. This also - // avoids scanning sparse MemMerge input arrays while a large body shrinks. - // Watch this small prefix, including an unsupported node that may fold. + // Bounded, conservative recognition of straight-line initializer boilerplate. + // Walk uses from the entry and stop at Return, never following callers or + // constant inputs. This also avoids scanning sparse MemMerge input arrays. + // Watch only this small prefix, including an unsupported node that may fold. private static final Node[] TINYBODY = new Node[15]; // Tiny worklist - private int smallBody(FunNode fun) { + private boolean smallBody(FunNode fun) { ReturnNode ret = fun.ret(); for( Node in : ret._inputs ) if( in!=null ) addDep(in); - if( ret.ctrl()!=fun ) return 0; + if( ret.ctrl()!=fun ) return false; TINYBODY[0] = fun; int len=1; for( int i=0; i<len; i++ ) { Node n = TINYBODY[i]; addDep(n); if( n==ret ) continue; - if( n instanceof ParmNode parm && parm.region()!=fun ) return 0; + if( n instanceof ParmNode parm && parm.region()!=fun ) return false; if( n!=fun && !(n instanceof ParmNode) && - !(n instanceof StoreNode) && !(n instanceof MemMergeNode) && - !(n instanceof NewNode) && !(n instanceof ProjNode) && !(n instanceof EscapeNode) ) - return 0; - if( n.nOuts()>TINYBODY.length ) return 0; + !(n instanceof StoreNode) && !(n instanceof MemMergeNode) ) + return false; // Something unexpected + if( n.nOuts()>TINYBODY.length ) return false; // Too big for( Node use : n._outputs ) { if( use==null ) continue; int j=0; while( j<len && TINYBODY[j]!=use ) j++; - if( j<len ) continue; - if( len==TINYBODY.length ) { addDep(use); return 0; } - TINYBODY[len++] = use; + if( j<len ) continue; // Already in TINYBODY + if( len==TINYBODY.length ) { addDep(use); return false; } // Too big + TINYBODY[len++] = use; // Add to worklist } } - return len; + return true; + } + + public void doInline() { + assert _inline > 0; // Only called when possible to inline + + ReturnNode ret = (ReturnNode)in(1); + FunNode fun = ret.fun(); + CallNode call = call(); + assert fun.sig().isa(call.fptr()._type); + + // Clone first + if( _inline==2 ) { + assert !CodeGen.CODE._midAssert; // Triggered inlining + // Remove the existing function linkage + call.unlink_all(); + // Clone the function body + FunNode fun2 = fun.copyBody(); + // Call uses the unique new function + Node fptr2 = new FunPtrNode(fun2.sig(),CodeGen.CODE._start,fun2.ret()).peephole(); + call.setDef(call.nIns()-1,fptr2); + // Link to the new function + call.link(fun2); + fun = fun2; + } + + // Trivial inlining: call site calls a single function; single function + // is only called by this call site. + doTrivialInlining(fun); + } + + boolean goodArgs( CallNode call, TypeFunPtr tfp ) { + for( int i=0; i<tfp.nargs(); i++ ) { + Node arg = call.arg(i+2); + // The graph's function-pointer type can still contain open forward + // references here. Compare against its recursively upgraded form, + // so deep mutability requirements participate before inlining. + Type formal = tfp.arg(i).upgradeType(Parser.TYPES); + if( !arg._type.isa(formal) || !arg._type.accessISA(formal) ) + { addDep(arg); return false; } + } + return true; } - @Override public Node pcopy(int idx) { + + // Do trivial inlining. Inlining does not need to clone code, merely + // triggers folding the Call/Fun and Return/CallEnd away. + private boolean doTrivialInlining( FunNode fun ) { + assert !CodeGen.CODE._midAssert; // Triggered inlining + // Trivial inline: rewrite + _folding = true; + // Rewrite Fun so the normal RegionNode ideal collapses + fun._folding = true; + fun.setDef(1,call().ctrl());// Bypass the Call; + fun.ret().setDef(3,null); // Return is folding also + + CodeGen.CODE.addAll(fun._outputs); + // Repeat defs 1 layer down, for users of Parm (Phis) + for( Node parm : fun._outputs ) + if( parm instanceof ParmNode ) + CodeGen.CODE.addAll(parm._outputs); + + // Inlining immediately blows all cache idepth fields past the inline point. + // Bump the global version number invalidating them en-masse. + CodeGen.CODE.invalidateIDepthCaches(); + // More peeps + CodeGen.CODE.add(fun); + CodeGen.CODE.add(this); + CodeGen.CODE.addAll(_outputs); + CodeGen.CODE.addAll(_inputs); + return true; // Always return true + } + + + @Override public Node pcopy(int idx, Node dep) { return _folding ? in(1).in(idx) : null; } + + @Override public Node copy() { + return _folding ? super.copy() : new CallEndNode(_rpc); + } + } diff --git a/src/main/java/com/seaofnodes/simple/node/CallNode.java b/src/main/java/com/seaofnodes/simple/node/CallNode.java index 44ad8e83..f791225a 100644 --- a/src/main/java/com/seaofnodes/simple/node/CallNode.java +++ b/src/main/java/com/seaofnodes/simple/node/CallNode.java @@ -2,14 +2,16 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; -import com.seaofnodes.simple.IterPeeps; import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.XInt; import com.seaofnodes.simple.type.TypeFunPtr; import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeRPC; +import com.seaofnodes.simple.type.XInt; import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; +import java.util.IdentityHashMap; /** * Call @@ -18,14 +20,10 @@ public class CallNode extends CFGNode { // Source location for late reported errors public final Parser.Lexer _loc; - // A forward field call records its receiver before its declaration tells - // us whether this is a method or an ordinary function stored in a field. - public String _method; - public CallNode(Parser.Lexer loc, Node... nodes) { super(nodes); _loc = loc; CodeGen.CODE._calls.add(this); } - public CallNode(CallNode call) { super(call); _loc = call._loc; _method=call._method; } + public CallNode(Parser.Lexer loc, Node... nodes) { super(nodes); _loc=loc; CodeGen.CODE._calls.add(this); } + public CallNode(CallNode call) { super(call); _loc = call._loc; } - @Override public String label() { return "Call"; } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { String fname = name(); @@ -40,13 +38,12 @@ public class CallNode extends CFGNode { if( fptr()._type instanceof TypeFunPtr tfp && tfp._isConstant() && tfp.notNull() ) { FunNode fun = CodeGen.CODE._link(tfp); if( fun !=null ) return fun._name; - return CodeGen.CODE.externFunc(tfp); + if( fptr() instanceof ExternNode ex ) return ex._extern; } return null; } - Node ctrl() { return in(0); } Node mem () { return in(1); } // Args mapped 1-to-1 on inputs, so conceptually start at 2 @@ -63,12 +60,12 @@ public class CallNode extends CFGNode { if( !(fptr()._type instanceof TypeFunPtr tfp) ) return false; if( XInt.isHigh(tfp.fidxs()) ) return true; for( int fidx=XInt.next(tfp.fidxs(),-1); fidx>=0; fidx=XInt.next(tfp.fidxs(),fidx) ) - if( CodeGen.CODE.externFunc(tfp.makeFrom(fidx))!=null ) return true; + if( CodeGen.CODE.externFunc(fidx)!=null ) return true; return false; } - // Unknown code can retain arguments without ever returning. Stop uses - // this summary directly as well as through the call's memory result. + // As in the escape chapter, native code may retain an argument even if + // this path never returns. StopCU reads this independently of CallEnd. TypeMem exposed(Node dep) { dep.addDepForwards(this); dep.addDepForwards(fptr()); @@ -76,7 +73,7 @@ public class CallNode extends CFGNode { Type input=dep.addDepForwards(mem())._type; TypeMem escaped=input instanceof TypeMem m ? m : input.isHigh() ? TypeMem.TOP : TypeMem.BOT; for( int i=2; i<nIns()-1; i++ ) escaped=escaped.escapes(dep.addDepForwards(arg(i))._type); - return escaped.withContents(1,Type.BOTTOM); + return TypeMem.make(1,Type.BOTTOM,false,false,false,escaped._escFs,escaped._escAs); } @@ -111,17 +108,11 @@ public class CallNode extends CFGNode { @Override public Node idealize() { + // Lowering may introduce calls after the call graph has been analyzed. + // These use the callee interface without reopening its parameter flow. + if( CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Lower.ordinal() ) return null; CallEndNode cend = cend(); if( cend==null ) return null; // Still building - if( _method!=null ) { - if( !(arg(2)._type instanceof com.seaofnodes.simple.type.TypeMemPtr ptr) ) return null; - var field=ptr._obj.field(_method); - if( field==null ) return null; - if( !(fptr()._type instanceof TypeFunPtr sig) ) return null; - if( !field._final || CodeGen.CODE.externFunc(sig)!=null ) delDef(2); - _method=null; - return this; - } // Link: call calls target function. Linking makes the target FunNode // point to this Call, and all his Parms point to the call arguments; @@ -131,11 +122,9 @@ public class CallNode extends CFGNode { // If fidxs is negative, then infinite unknown functions int[] fidxs = tfp.fidxs(); if( !XInt.isHigh(fidxs) ) { - // Wipe out the return which matching in the linker table - // Walk the function identities and link each possible callee. - for( int fidx=XInt.next(fidxs,-1); fidx>=0; fidx=XInt.next(fidxs,fidx) ) { - TypeFunPtr tfp0 = tfp.makeFrom(fidx); - FunNode fun = CodeGen.CODE.link(tfp0); + // Walk the bits and link + for( int fidx = XInt.next(fidxs,0); fidx > 0; fidx = XInt.next(fidxs,fidx) ) { + FunNode fun = CodeGen.CODE.link(fidx); if( fun!=null && !fun._folding && !linked(fun) ) progress = link(fun); } @@ -156,35 +145,58 @@ public class CallNode extends CFGNode { // Link so this calls fun public Node link( FunNode fun ) { assert !linked(fun); - fun.addDef(this); + CodeGen.CODE.add(fun).addDef(this); for( Node use : fun._outputs ) - if( use instanceof ParmNode parm ) - parm.addDef(parm._idx==0 ? new ConstantNode(cend()._rpc).peephole() : arg(parm._idx)); + if( use instanceof ParmNode parm ) { + Node def = parm._idx==0 ? ConstantNode.seed(cend()._rpc).peephole() : arg(parm._idx); + CodeGen.CODE.add(parm).addDef(def); + // Linking can happen during post-SCCP inlining. Keep an RPC + // Parm's cached finite set synchronized with its newly added + // input instead of waiting for the next worklist visit. + if( parm._idx==0 && parm._type instanceof TypeRPC ) + parm._type = parm._type.meet(def._type); + } // Call end points to function return CodeGen.CODE.add(cend()).addDef(fun.ret()); return this; } + // Unlink a single function + public void unlink( FunNode fun, int path ) { + assert linked(fun); + for( Node use : fun._outputs ) + if( use instanceof ParmNode ) + use.delDef(path); + fun.delDef(path); + CodeGen.CODE.add(fun); + CallEndNode cend = cend(); + ReturnNode ret = fun.ret(); + if( cend != null ) { + ret.keep(); + cend.delDef(cend._inputs.find(ret)); + CodeGen.CODE.add(cend); + ret.unkeep(); + } + assert !linked(fun); + } + + // Unlink all linked functions public void unlink_all() { for( int i=0; i<_outputs._len; i++ ) if( out(i) instanceof FunNode fun ) { - assert linked(fun); - int idx = fun._inputs.find(this); - for( Node use : fun._outputs ) - if( use instanceof ParmNode ) - use.delDef(idx); - fun.delDef(idx); - cend().delDef(cend()._inputs.find(fun.ret())); - assert !linked(fun); + int path = fun._inputs.find(this); + unlink(fun,path); i--; } } @Override public Parser.ParseException err() { + if( fptr()._type == Type.BOTTOM ) + return null; // Wrong, but failing earlier if( !(fptr()._type instanceof TypeFunPtr tfp) ) - throw Utils.TODO(); + return Parser.error("Expected a function, but found " + fptr()._type, _loc); if( !tfp.notNull() ) return Parser.error( "Might be null calling "+tfp, _loc); if( nargs() != tfp.nargs() ) @@ -193,11 +205,13 @@ public class CallNode extends CFGNode { // Check for args for( int i=0; i<tfp.nargs(); i++ ) if( !arg(i+2)._type.isa(tfp.arg(i)) || !arg(i+2)._type.accessISA(tfp.arg(i)) ) { - cend().addDep(arg(i+2)); // Upgrading arg type may allow inlining - return Parser.error( "Argument #" + i + " isa " + arg(i+2)._type + ", but must be a " + tfp.arg(i), _loc ); + // Constructors return private memory and carry hidden self and + // selfMem arguments ahead of the source-level arguments. + int argno = i - (tfp.ret() instanceof TypeMem ? 2 : 0); + return Parser.error( "Argument #" + argno + " isa " + arg(i+2)._type + ", but must be a " + tfp.arg(i), _loc ); } - if( XInt.isHigh(tfp.fidxs()) ) + if( tfp.isHigh() ) throw Utils.TODO(); // Infinite unknown TFPs? Should be fairly precise CG return null; diff --git a/src/main/java/com/seaofnodes/simple/node/CastMach.java b/src/main/java/com/seaofnodes/simple/node/CastMach.java deleted file mode 100644 index e45898f5..00000000 --- a/src/main/java/com/seaofnodes/simple/node/CastMach.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.codegen.Encoding; -import com.seaofnodes.simple.codegen.RegMask; -import com.seaofnodes.simple.util.SB; - -/** - * Cast a pointer to read-only - */ -public class CastMach extends CastNode implements MachNode { - public CastMach( CastNode n ) { super(n); } - @Override public String op() { return label(); } - @Override public RegMask regmap(int i) { assert i==1; return RegMask.FULL; } - @Override public RegMask outregmap() { return RegMask.FULL; } - @Override public int twoAddress( ) { return 1; } - @Override public void encoding( Encoding enc ) { } - @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(in(1))); } -} diff --git a/src/main/java/com/seaofnodes/simple/node/CastNode.java b/src/main/java/com/seaofnodes/simple/node/CastNode.java deleted file mode 100644 index fa7c207d..00000000 --- a/src/main/java/com/seaofnodes/simple/node/CastNode.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.print.ExprPrinter; - -import com.seaofnodes.simple.Parser; -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeInteger; -import com.seaofnodes.simple.type.TypeMemPtr; -import com.seaofnodes.simple.util.Utils; - -// Upcast (join) the input to a t. Used after guard test to lift an input. -// Can also be used to make a type-assertion if ctrl is null. -public class CastNode extends Node { - public Type _t; - public CastNode(Type t, Node ctrl, Node in) { - super(ctrl, in); - _t = t; - setType(compute()); - } - - public CastNode(CastNode c) { - super(c.in(0), c.in(1)); // Call parent constructor - this._t = c._t; // Copy the Type field - setType(compute()); // Ensure type is recomputed - } - - @Override public String label() { return "("+_t.str()+")"; } - - @Override boolean _upgradeType(java.util.HashMap<String,Type> types) { - Type t = _t.upgradeType(types); - if( t==_t ) return false; - unlock(); - _t = t; - return true; - } - - - @Override public boolean isConst() { return true; } - - @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { - return p.p(label()).n(in(1)); - } - - @Override - public Type compute() { - // Cast array to int - Type t1 = in(1)._type; - if( _t == TypeInteger.BOT && t1 instanceof TypeMemPtr tmp && tmp._obj.isAry() ) - return _t; - // Freeze if the join is high but both inputs are low - Type tj = t1.join(_t); - if( tj.isHigh() && !t1.isHigh() ) - return _type==null ? _t : _type; - - return tj; - } - - @Override - public Node idealize() { - return in(1)._type.isa(_t) && in(1)._type.accessISA(_t) ? in(1) : null; - } - - @Override - public boolean eq(Node n) { - CastNode cast = (CastNode)n; // Contract - return _t==cast._t; - } - - @Override - int hash() { return _t.hashCode(); } - - @Override - public Parser.ParseException err() { - // Has a condition to test, so OK - if( in(0) != null ) return null; - // No condition to test, so this must optimize away - return Parser.error( "Type " + in(1)._type.str() + " is not of declared type " + _t.str(), null ); - } -} diff --git a/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java b/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java index afdd29be..0b8053e2 100644 --- a/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java @@ -2,11 +2,14 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; +import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.type.TypeStruct; import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; /** * A field offset. This becomes a constant *after* other optimizations which @@ -15,27 +18,61 @@ import com.seaofnodes.simple.util.SB; */ public class ConFldOffNode extends ConstantNode { - public final String _name; // Struct name + public TypeStruct _ts; // Struct holding field public final String _fname; // Field name - public ConFldOffNode( String name, String fname ) { super(TypeInteger.BOT); _name = name; _fname = fname; } + public ConFldOffNode( TypeStruct ts, String fname ) { super(TypeInteger.BOT); _ts = ts; _fname = fname; } + @Override public String label() { - return _fname == " len" ? "sizeof("+_name+")" : "#"+_fname; + return _fname == " len" ? "sizeof("+_ts._name+")" : "#"+_fname; } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p(label()); } + @Override public Node idealize() { + // Some array offsets are known early + if( _ts.isAry() ) { + // Length always at offset zero + if( _fname==" len" ) + return CodeGen.CODE.ZERO; + // The shift depends on the element + if( _fname=="<<" ) + return Node.con(_ts.aryScale()); + // The base depends on the layout (alignment for large elements may + // involve padding) and MAY depend on the length if I go for + // smaller array lengths. + } + return null; + } + // Convert field offset to an integer - public Node asOffset(TypeStruct ts) { - return new ConstantNode(TypeInteger.constant(ts.offset(_fname==" len" ? ts._fields.length : ts.find(_fname)))).peephole(); + public Node asOffset() { + int fldx = _fname==" len" ? _ts._fields.length : _ts.find(_fname); + if( fldx == -1 ) return null; + // " len" selects the terminal size slot, not a Field. + if( fldx < _ts._fields.length && _ts._fields[fldx]._extern ) + return new ExternOffsetNode(_fname).peephole(); + return ConstantNode.make(TypeInteger.constant(_ts.offset(fldx))).peephole(); } + // Upgrade the internal type + @Override boolean _upgradeType( HashMap<String,Type> TYPES) { + boolean progress = super._upgradeType(TYPES); + TypeStruct ts = (TypeStruct)_ts.upgradeType(TYPES); + if( ts == _ts ) return progress; + _ts = ts; + return true; + } @Override public boolean eq(Node n) { ConFldOffNode off = (ConFldOffNode)n; // Invariant - return _name.equals(off._name) && _fname.equals(off._fname) && super.eq(n); + return _ts._name==off._ts._name && _fname.equals(off._fname) && super.eq(n); + } + @Override int hash() { return super.hash() * _ts._name.hashCode() * _fname.hashCode(); } + + @Override public Parser.ParseException err() { + return _ts._fref ? Parser.error("Unknown struct type '"+_ts._name+"'",null) : null; } - @Override int hash() { return super.hash() * _name.hashCode() * _fname.hashCode(); } } diff --git a/src/main/java/com/seaofnodes/simple/node/ConstantNode.java b/src/main/java/com/seaofnodes/simple/node/ConstantNode.java index 8d783be7..eabf2639 100644 --- a/src/main/java/com/seaofnodes/simple/node/ConstantNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ConstantNode.java @@ -5,7 +5,9 @@ import com.seaofnodes.print.ExprPrinter; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeFunPtr; -import com.seaofnodes.simple.util.SB; + +import java.util.HashMap; +import java.util.IdentityHashMap; /** * A Constant node represents a constant value. @@ -17,29 +19,56 @@ import com.seaofnodes.simple.util.SB; * The Constant's value is the value stored in it. */ -public class ConstantNode extends Node { - public Type _con; +public class ConstantNode extends TypeNode { public ConstantNode( Type type ) { - super(new Node[]{CodeGen.CODE._start}); - _con = _type = type; + this(type,false); + } + protected ConstantNode( Type type, boolean raw ) { + super(type,new Node[]{CodeGen.CODE._start}); + assert raw || !(type instanceof TypeFunPtr tfp && tfp.isConstant()) : + "Unique function identities require a Return-linked FunPtrNode"; + _type = type; } - public ConstantNode( ConstantNode con ) { super(con); _con = con._type; } + public ConstantNode( ConstantNode con ) { super(con); } + + // Semantic constant construction through Lower. A unique internal + // function identity carries a lifetime edge to the function's Return. public static Node make( Type type ) { if( type==Type. CONTROL ) return new CtrlNode(); if( type==Type.XCONTROL ) return new XCtrlNode(); - if( type instanceof TypeFunPtr tfp && tfp.isConstant() && tfp.notNull() ) { - FunNode fun = CodeGen.CODE.link(tfp); - if( fun!=null && !fun.isDead() ) return new FunPtrNode(tfp,fun.ret()); + if( CodeGen.CODE._phase != null && + CodeGen.CODE._phase.ordinal() > CodeGen.Phase.Lower.ordinal() ) + return raw(type); + if( type instanceof TypeFunPtr tfp && tfp.isConstant() ) { + FunNode fun = CodeGen.CODE._link(tfp); + if( fun != null ) + return new FunPtrNode(tfp,CodeGen.CODE._start,fun.ret()).init(); + // TODO: A singleton FIDX can also emerge after merging and + // guarding several function pointers. Preserve that identity + // even if its internal Return is no longer available. + return raw(type); } return new ConstantNode(type); } + // Exact construction for deserialization and graph-incomplete machine + // selection. No semantic classification, compute, idealize, or GVN. + public static ConstantNode raw( Type type ) { return new ConstantNode(type,true); } + public static ConstantNode raw( ConstantNode con ) { return new ConstantNode(con); } + + // Analysis input created from whole cloth, rather than a first-class + // runtime value. In particular, a constructor call descriptor does not + // own the constructor function; the class FunPtr does. + public static ConstantNode seed( Type type ) { return new ConstantNode(type,true); } + @Override protected String repeatName() { return _con==null || _con.toString().length()<=32 ? null : uniqueName(); } @Override public String label() { return "Con"; } + @Override public Node copy() { return raw(this); } + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { @@ -54,14 +83,4 @@ public class ConstantNode extends Node { @Override public boolean isConst() { return true; } @Override public Type compute() { return _con; } @Override public Node idealize() { return null; } - @Override public boolean eq(Node n) { return _con==((ConstantNode)n)._con; } - @Override int hash() { return _con.hashCode(); } - - @Override boolean _upgradeType(java.util.HashMap<String,Type> types) { - Type next = _con.upgradeType(types); - if( next==_con ) return false; - unlock(); - _con=next; - return true; - } } diff --git a/src/main/java/com/seaofnodes/simple/node/ConvertNode.java b/src/main/java/com/seaofnodes/simple/node/ConvertNode.java index c5be0123..cdc890a7 100644 --- a/src/main/java/com/seaofnodes/simple/node/ConvertNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ConvertNode.java @@ -77,7 +77,13 @@ public class ConvertNode extends Node { return null; } - + @Override boolean _upgradeType(HashMap<String,Type> TYPES) { + Type dst = _dst.upgradeType(TYPES); + if( dst==_dst ) return false; + unlock(); + _dst = dst; + return true; + } @Override public Parser.ParseException err() { if( val()._type==Type.BOTTOM || @@ -92,12 +98,4 @@ public class ConvertNode extends Node { @Override public boolean eq(Node n) { return _dst==((ConvertNode)n)._dst; } @Override int hash() { return _dst.hashCode(); } - - @Override boolean _upgradeType(java.util.HashMap<String,Type> types) { - Type next = _dst.upgradeType(types); - if( next==_dst ) return false; - unlock(); - _dst=next; - return true; - } } diff --git a/src/main/java/com/seaofnodes/simple/node/CtrlNode.java b/src/main/java/com/seaofnodes/simple/node/CtrlNode.java index cf630ad4..813e6fcb 100644 --- a/src/main/java/com/seaofnodes/simple/node/CtrlNode.java +++ b/src/main/java/com/seaofnodes/simple/node/CtrlNode.java @@ -8,6 +8,7 @@ import com.seaofnodes.simple.type.Type; public class CtrlNode extends CFGNode { public CtrlNode() { super(CodeGen.CODE._start); } + @Override public String label() { return "Ctrl"; } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("Cctrl"); } @Override public boolean isConst() { return true; } diff --git a/src/main/java/com/seaofnodes/simple/node/DivFNode.java b/src/main/java/com/seaofnodes/simple/node/DivFNode.java deleted file mode 100644 index df1dae4e..00000000 --- a/src/main/java/com/seaofnodes/simple/node/DivFNode.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeFloat; - -import java.util.BitSet; - -public class DivFNode extends ArithFNode { - public DivFNode(Node lhs, Node rhs) { super(lhs, rhs); } - public DivFNode(ArithNode n) { super(n); } - - @Override public String label() { return "DivF"; } - - @Override public String glabel() { return "/"; } - - @Override double doOp( double x, double y ) { return x/y; } - - @Override - public Node idealize() { - // Div of constant - if( in(2)._type instanceof TypeFloat f && f.isConstant() ) - return new MulFNode(in(1),new ConstantNode(TypeFloat.constant(1.0/f.value())).peephole()); - - return null; - } - @Override Node copy(Node lhs, Node rhs) { return new DivFNode(lhs,rhs); } -} diff --git a/src/main/java/com/seaofnodes/simple/node/DivNode.java b/src/main/java/com/seaofnodes/simple/node/DivNode.java index d973ffa7..0db2b7a2 100644 --- a/src/main/java/com/seaofnodes/simple/node/DivNode.java +++ b/src/main/java/com/seaofnodes/simple/node/DivNode.java @@ -10,6 +10,7 @@ public class DivNode extends ArithNode { public DivNode(Node lhs, Node rhs) { super(null, lhs, rhs); } public DivNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs, mode); } + @Override public String op() { return _mode==1 ? "//" : "/"; } @Override long doOp( long x, long y ) { return y==0 ? 0 : x / y; } diff --git a/src/main/java/com/seaofnodes/simple/node/EscapeMach.java b/src/main/java/com/seaofnodes/simple/node/EscapeMach.java deleted file mode 100644 index b6ec04e9..00000000 --- a/src/main/java/com/seaofnodes/simple/node/EscapeMach.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.codegen.*; - -/** Publication orders memory; it needs no runtime instruction or register. */ -public class EscapeMach extends EscapeNode implements MachNode { - public EscapeMach(EscapeNode esc) { super(esc); _inputs.set(3,null); } - @Override public String op() { return label(); } - @Override public RegMask regmap(int i) { return null; } - @Override public RegMask outregmap() { return null; } - @Override public void encoding(Encoding enc) { } -} diff --git a/src/main/java/com/seaofnodes/simple/node/EscapeNode.java b/src/main/java/com/seaofnodes/simple/node/EscapeNode.java index 849c552f..f7518184 100644 --- a/src/main/java/com/seaofnodes/simple/node/EscapeNode.java +++ b/src/main/java/com/seaofnodes/simple/node/EscapeNode.java @@ -1,60 +1,109 @@ package com.seaofnodes.simple.node; -import com.seaofnodes.simple.Parser; +import com.seaofnodes.print.ExprPrinter; + import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; +import java.util.HashMap; +import java.util.IdentityHashMap; -/** Publish one initialized field: {control, public memory, private memory, self}. - * This explicit construction boundary publishes a completed field. Stop and - * Start separately discover whether unknown code can reach that field. +/** + * Merge public and private aliases. + * 0 - control + * 1 - private pointer + * 2 - private alias + * 3 - public alias */ -public class EscapeNode extends Node { - public Field _field; - private final Parser.Lexer _loc; - public EscapeNode(Parser.Lexer loc, Field field, Node ctrl, Node pub, Node priv, Node self) { - super(ctrl,pub,priv,self); - _field=field; - _loc=loc; +public class EscapeNode extends TypeNode { + + public EscapeNode(Field fld, Node self, Node priv, Node pub ) { + super(fld,null,self,priv,pub); } - public EscapeNode(EscapeNode esc) { super(esc); _field=esc._field; _loc=esc._loc; } - public Node pub() { return in(1); } + public EscapeNode(EscapeNode esc ) { + super(esc); // Copy constructor + // Except we do not want/need self ptr past Opto. + // *NOTE* the weird direct stomp; this is correct because this + // constructor does NOT set the backedges, because it is used to + // copy-construct an entire graph and ALL edges will get rewritten. + _inputs.set(1,null); + _con = esc._con; + } + + // Pointer to some private (unescaped) memory from a NewNode + public Node self() { return in(1); } public Node priv() { return in(2); } - public Node self() { return in(3); } - @Override public boolean isMem() { return true; } - @Override public String label() { return "Escape_"+_field._fname; } + public Node pub () { return in(3); } + public Field fld() { return (Field)_con; } - private Type value() { - return MemMergeNode.contents(priv(),_field._alias,this); + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.p("Esc#").p(fld()._alias).p(" {"); + if( self()==null ) p.p("---"); + else p.n(self()).p(","); + p.n(priv()).p("}, "); + p.n(pub ()); + return p; } - @Override public Type compute() { - if( in(0)._type.isHigh() || priv()._type.isHigh() || pub()._type.isHigh() ) return TypeMem.TOP; - Type t=value().meet(MemMergeNode.contents(pub(),_field._alias,this)).join(_field._t); - TypeMem prior=pub()._type instanceof TypeMem m ? m : TypeMem.BOT; - return prior.withContents(_field._alias,t).escapes(value()); + + @Override + public Type compute() { + if( priv()._type.isHigh() ) return TypeMem.TOP; + if( pub ()._type.isHigh() ) return TypeMem.TOP; + TypeMem mpriv = (TypeMem)priv()._type; + TypeMem mpub = (TypeMem)pub ()._type; + // Private memory comes from an allocation or other non-aliased source. + // Public memory comes from anywhere, might be bulk memory with a very weak mem type. + Field fld = fld(); + assert mpriv._alias==1 || mpriv._alias==fld._alias; + assert mpub ._alias==1 || mpub ._alias==fld._alias; + Type tpriv = mpriv._t instanceof TypeStruct ts && ts.findAlias(fld._alias)!=-1 + ? ts.at(ts.findAlias(fld._alias))._t + : mpriv._t; + assert !(mpub._t instanceof TypeStruct); // Never expect this, but if it starts to happen, need to optimize + Type tpub = mpub._t; + + // t is meet of pub & priv. Pub memory can be very weak (e.g. alias 1) + // and we're no worse than the declared field type + Type t = tpriv.meet(tpub).join(fld._t); + int[] fidxes = XInt.meet(mpriv._escFs, mpub._escFs); + int[] aliases = XInt.meet(mpriv._escAs, mpub._escAs); + return TypeMem.make(fld._alias, t, false, false, fld._final, fidxes, aliases).escapesFrom(t); } + @Override public Node idealize() { - if( priv() instanceof MemMergeNode merge ) { - setDef(2,merge.alias(_field._alias)); + // Sharpen private memory input + if( priv() instanceof MemMergeNode mmm ) { + setDef(2,mmm.alias(fld()._alias)); return this; } - if( pub() instanceof MemMergeNode merge ) { - setDef(1,merge.alias(_field._alias)); + // Sharpen public memory input + if( pub() instanceof MemMergeNode mmm ) { + setDef(3,mmm.alias(fld()._alias)); return this; } + // New allocation is dead + if( self()._type.isHigh() ) + if( pub()._type.isa(_type) ) return pub(); + else addDep(pub()); return null; } - @Override public Parser.ParseException err() { - if( in(0)._type.isHigh() ) return null; - if( value()==Type.BOTTOM ) { - String name = self()._type instanceof TypeMemPtr p ? p._obj._name : "Object"; - return Parser.error("'"+name+"' is not fully initialized, field '"+_field._fname+"' needs to be set in a constructor",_loc); - } - return null; - } - @Override boolean _upgradeType(java.util.HashMap<String,Type> types) { - Field next = (Field)_field.upgradeType(types); - if( next==_field ) return false; - unlock(); _field=next; return true; + +// Upgrade the internal type +@Override boolean _upgradeType( HashMap<String,Type> TYPES) { + Type t = _con.upgradeType(TYPES); + if( t == _con ) return false; + unlock(); // Unlock before changing _con + _con = t; + return true; +} + + @Override public boolean eq(Node n) { + if( fld()._alias!=((EscapeNode)n).fld()._alias ) + return false; + assert _con == ((EscapeNode)n)._con; + return true; } - @Override public boolean eq(Node n) { return _field==((EscapeNode)n)._field; } - @Override int hash() { return _field.hashCode(); } + + @Override int hash() { return _con.hashCode(); } } diff --git a/src/main/java/com/seaofnodes/simple/node/ExternNode.java b/src/main/java/com/seaofnodes/simple/node/ExternNode.java index 7a26e2e9..2d1dc29f 100644 --- a/src/main/java/com/seaofnodes/simple/node/ExternNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ExternNode.java @@ -1,25 +1,27 @@ package com.seaofnodes.simple.node; -import com.seaofnodes.simple.Parser; -import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeFunPtr; import com.seaofnodes.simple.util.SB; + import java.util.BitSet; +import java.util.HashMap; +import java.util.IdentityHashMap; /** - A constant with external linkage. + A constant with external linkage. */ public class ExternNode extends ConstantNode { public final String _extern; - public ExternNode(Type t, String ex) { - super(t); - _extern = ex; - if( t instanceof TypeFunPtr tfp ) CodeGen.CODE.externFunc(tfp,ex); - } + // An exact external function identity has no local FunNode/Return to own. + // Its lifetime and eventual relocation are carried by the extern symbol + // and CodeGen's FIDX-to-extern mapping instead. + public ExternNode(Type t, String ex) { super(t,true); _extern = ex; } + @Override public String label() { return "#"+_con+":"+_extern; } @Override public boolean eq(Node n) { return this==n; } + + } diff --git a/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java b/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java new file mode 100644 index 00000000..23ed5468 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/ExternOffsetNode.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeInteger; +import java.util.HashMap; +import java.util.IdentityHashMap; + +// A field address supplied by the C linker, not a displacement from the base. +// The class base remains available for ideal type/alias analysis. Instruction +// selection replaces it with this symbol's address and a zero displacement. +public class ExternOffsetNode extends ExternNode { + public ExternOffsetNode(String name) { super(TypeInteger.BOT,name); } + + @Override public Node copy() { return new ExternOffsetNode(_extern); } + +} diff --git a/src/main/java/com/seaofnodes/simple/node/FRefNode.java b/src/main/java/com/seaofnodes/simple/node/FRefNode.java index e7333c23..644f1434 100644 --- a/src/main/java/com/seaofnodes/simple/node/FRefNode.java +++ b/src/main/java/com/seaofnodes/simple/node/FRefNode.java @@ -10,17 +10,32 @@ import com.seaofnodes.simple.type.Type; * A Forward Reference. Its any final constant, including functions. When * the Def finally appears its plugged into the forward reference, which then * peepholes to the Def. + + * FRefs can be external references, to other Simple defined values in ELF + * files, or external FFI/C libs. If external, a file system lookup needs to + * find it in another file, and the name (one time) change to match the found + * location. + * + * Example, a ambiguous name "foo" is looked up externally; it might resolve + * to "A.B.foo" or "libc.foo" or the like; the name is changed to match. */ public class FRefNode extends ConstantNode { public static final Type FREF_TYPE = Type.BOTTOM; - public final Var _n; - public FRefNode( Var n ) { super(FREF_TYPE); _n = n; } + public String _name; + public final Parser.Lexer _loc; // Source location + + public FRefNode( String name, Parser.Lexer loc ) { this(name,loc,FREF_TYPE); } + public FRefNode( String name, Parser.Lexer loc, Type type ) { + super(type); + _name = name; + _loc = loc; + } - @Override public String label() { return "FRef"+_n; } + @Override public String label() { return "FRef"+_name; } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { - return p.p("FRef_").p(_n); + return p.p("FRef_").p(_name); } @Override public Node idealize() { @@ -28,8 +43,8 @@ public class FRefNode extends ConstantNode { return nIns()==1 ? null : in(1); } - public Parser.ParseException err() { return Parser.error("Undefined name '"+_n._name+"'",_n._loc); } + public Parser.ParseException err() { return Parser.error("Undefined name '"+_name+"'",_loc); } @Override public boolean eq(Node n) { return this==n; } - @Override int hash() { return _n._idx; } + @Override int hash() { return _name.hashCode(); } } diff --git a/src/main/java/com/seaofnodes/simple/node/FldOffNode.java b/src/main/java/com/seaofnodes/simple/node/FldOffNode.java index 7605d74f..535241a6 100644 --- a/src/main/java/com/seaofnodes/simple/node/FldOffNode.java +++ b/src/main/java/com/seaofnodes/simple/node/FldOffNode.java @@ -1,24 +1,54 @@ package com.seaofnodes.simple.node; +import com.seaofnodes.print.ExprPrinter; + import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; + +/** + * A field offset. This becomes a constant *after* other optimizations which + * determine field sizes - some fields might be dead, or hold only zeros or + * small values. + */ + +public class FldOffNode extends TypeNode { + public final String _fname; // Field name + public final Parser.Lexer _loc; // + public FldOffNode( Node base, String fname, Parser.Lexer loc ) { super(TypeInteger.BOT,base); _fname = fname; _loc=loc; } + + + @Override public String label() { + return _fname == " len" ? "sizeof()" : "#"+_fname; + } + + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p(label()); + } + + @Override public Type compute() { + return Type.BOTTOM; + } -/** A field name whose receiver's shape is not known while parsing. */ -public class FldOffNode extends Node { - public final String _name; - public FldOffNode(Node ptr, String name) { super(null,ptr); _name=name; } - @Override public String label() { return "#"+_name; } - @Override public Type compute() { return TypeInteger.BOT; } @Override public Node idealize() { - if( in(1)._type instanceof TypeMemPtr ptr && !ptr._obj._open ) { - if( _name.equals("<<") && ptr._obj.isAry() ) return con(ptr._obj.aryScale()); - if( ptr._obj.field(_name)!=null ) return new ConFldOffNode(ptr._obj._name,_name); - } + // When base() gets typed, this xforms into a ConFldOffNode + if( in(0)._type instanceof TypeMemPtr tmp ) + return new ConFldOffNode(tmp._obj,_fname); return null; } + + @Override public boolean eq(Node n) { + FldOffNode off = (FldOffNode)n; // Invariant + return _fname.equals(off._fname) && super.eq(n); + } + @Override int hash() { return super.hash() * _fname.hashCode(); } + @Override public Parser.ParseException err() { - return Parser.error("Cannot resolve field '"+_name+"'",null); + if( in(0)._type instanceof TypeMemPtr tmp && tmp._obj._fref ) + return Parser.error("Unknown struct type '"+tmp._obj._name+"'",null); + return null; } - @Override public boolean eq(Node n) { return _name.equals(((FldOffNode)n)._name); } - @Override int hash() { return _name.hashCode(); } } diff --git a/src/main/java/com/seaofnodes/simple/node/FunNode.java b/src/main/java/com/seaofnodes/simple/node/FunNode.java index 2850ba92..ad79130e 100644 --- a/src/main/java/com/seaofnodes/simple/node/FunNode.java +++ b/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -1,25 +1,20 @@ package com.seaofnodes.simple.node; import com.seaofnodes.simple.Parser; -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.codegen.Encoding; -import com.seaofnodes.simple.codegen.RegMask; -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeMem; -import com.seaofnodes.simple.type.TypeFunPtr; -import com.seaofnodes.simple.type.TypeTuple; +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; +import java.util.Arrays; import java.util.BitSet; +import java.util.HashMap; +import java.util.IdentityHashMap; import static com.seaofnodes.simple.codegen.CodeGen.CODE; public class FunNode extends RegionNode { - // Generated allocation/initializer entry points cannot be called by users. - public boolean _constructor; - // When set true, this Call/CallEnd/Fun/Return is being trivially inlined - boolean _folding; + public boolean _folding; public boolean folding() { return _folding; } private TypeFunPtr _sig; // Initial signature @@ -27,21 +22,28 @@ public class FunNode extends RegionNode { public String _name; // Debug name - public FunNode( Parser.Lexer loc, TypeFunPtr sig, String name, Node... nodes ) { super(loc,nodes); _name=name; _sig = sig; } + public int _approxUIDs; // Approximate function size, used as a inlining heuristic + public CompUnit _compunit; // Defined in source file + public final boolean _extern; // External function; no code, name only + + private FunNode( Parser.Lexer loc, Node[] nodes, TypeFunPtr sig, String name, CompUnit compunit, boolean ext ) { + super(loc,nodes); + _name = name; + _sig = sig; + _extern = ext; + _compunit = compunit; + } + public FunNode( Parser.Lexer loc, TypeFunPtr sig, String name, CompUnit compunit, Node... nodes ) { this(loc,nodes,sig,name,compunit,false); } public FunNode( FunNode fun ) { - super( fun, fun==null ? null : fun._loc ); - if( fun!=null ) { - _sig = fun.sig(); - _name = fun._name; - _constructor = fun._constructor; - } else { - _sig = TypeFunPtr.BOT; - _name = ""; - } + super( fun, fun._loc ); + _sig = fun.sig(); + _name = fun._name; + _compunit = fun._compunit; + _extern = fun._extern; } - @Override - public String label() { return _name == null ? "$fun"+_sig.fidx() : _name; } + + @Override public String label() { return _name == null ? "$fun"+_sig.fidx() : _name; } // Find the one CFG user from Fun. It's not always the Return, but always // the Return *is* a CFG user of Fun. @@ -53,49 +55,63 @@ public class FunNode extends RegionNode { return null; } - public ParmNode rpc() { - ParmNode rpc = null; + public ParmNode parm(int idx) { + ParmNode pidx = null; for( Node n : _outputs ) - if( n instanceof ParmNode parm && parm._idx==0 ) - { assert rpc==null; rpc=parm; } - return rpc; + if( n instanceof ParmNode parm && parm._idx==idx ) + { assert pidx==null; pidx=parm; } + return pidx; } + public ParmNode rpc() { return parm(0); } // Cannot create the Return and Fun at the same time; one has to be first. // So setting the return requires a second step. public void setRet(ReturnNode ret) { _ret=ret; } public ReturnNode ret() { assert _ret!=null; return _ret; } - // Direct calls are known; any other use of an address may have unknown callers. - public boolean hasPtr() { - for( Node use : ret()._outputs ) - if( use instanceof FunPtrNode ) - for( Node user : use._outputs ) - if( !(user instanceof CallNode call) || call.fptr()!=use || - call._inputs.find(use)!=call.nIns()-1 ) return true; - return false; - } - // Signature can improve over time public TypeFunPtr sig() { return _sig; } - // Hidden initializer captures are discovered while parsing its body, - // before any call or function address has been made for this function. - public void addParseArg(Type type) { - Type[] args=java.util.Arrays.copyOf(_sig._sig,_sig.nargs()+1); - args[args.length-1]=type; - _sig=TypeFunPtr.make((byte)2,false,args,_sig.ret(),_sig.fidxs()); - } public void setSig( TypeFunPtr sig ) { - assert sig.isa(_sig); + assert sig.isa(_sig) || resolvesForwardSelf(sig,_sig); if( _sig != sig ) { CODE.add(this); + // Changing the signature can allow more inlining + if( _ret != null ) + for( Node use : ret().outs() ) + if( use instanceof CallEndNode || use instanceof FunPtrNode ) + CODE.add(use); + unlock(); _sig = sig; } } + // A class initializer starts parsing with an open singleton receiver and + // closes that same receiver after discovering its fields. Function + // arguments are contravariant, so ordinary isa does not describe this + // parser-time forward-reference resolution even though every other part + // of the signature sharpens normally. + private static boolean resolvesForwardSelf(TypeFunPtr sig, TypeFunPtr old) { + if( !(sig.arg(0) instanceof TypeMemPtr now) || + !(old.arg(0) instanceof TypeMemPtr prior) || + now._obj._name != prior._obj._name || + !prior._obj._open || now._obj._open ) + return false; + return sig.makeFrom(old.arg(0),0).isa(old); + } + + @Override boolean _upgradeType( HashMap<String,Type> TYPES) { + TypeFunPtr sig = (TypeFunPtr)_sig.upgradeType(TYPES); + if( sig == _sig ) return false; + unlock(); + _sig = sig; + return true; + } + public void setName( String name ) { - if( _name==null ) _name=name; - else _name += "."+name; + if( _name==null ) + _name = _compunit==null || _compunit._cname==null || _compunit._cname.indexOf('.')==-1 + ? name + : (_compunit._cname+"."+name).intern(); } @Override @@ -104,10 +120,10 @@ public class FunNode extends RegionNode { if( unknownCallers() ) return Type.CONTROL; Type t = Type.XCONTROL; - for (int i = 1; i < nIns(); i++) { - // Since no unknown callers, we are not main and the Start input - // will be a Tuple with XControl, so ignore it. Need to be called - // from someplace other than Start + for( int i = 1; i < nIns(); i++) { + // Since there are no unknown callers, the Start input is only a + // fake hook and will be a Tuple with XControl, so ignore it. Need + // to be called from someplace other than Start. if( !(in(i) instanceof StartNode) ) t = t.meet(in(i)._type); } @@ -117,29 +133,38 @@ public class FunNode extends RegionNode { @Override public Node idealize() { - // Some linked path dies, except main never kills Start + // Some linked path dies, except unknown caller hooks never kill Start. Node progress = deadPath(unknownCallers()); if( progress!=null ) { - if( nIns()==3 && in(2) instanceof CallNode call ) - CODE.add(call.cend()); // If Start and one call, check for inline + if( nIns()==2 && in(1) instanceof CallNode call ) + CODE.add(call.cend()); // If no Start and one call, check for inline return progress; } // Upgrade inferred or user-written return type to actual if( _ret!=null && _ret._type instanceof TypeTuple tt && - !(tt.ret() instanceof TypeMem) && tt.ret() != _sig.ret() ) { + tt.ret() != _sig.ret() && tt.ret().isa(_sig.ret()) ) { setSig(_sig.makeFrom(tt.ret())); return this; } + // Attempt to get rid of the unknown caller. + // - Must be past pessimistic Iter, which discovers and links calls + // through class-field loads and other initially imprecise values. + // - FIDX is not in the escape set + // - Not called anywhere else; post Opto called-not-escaped functions need to still be hooked to start + // Opaque CLInit obligations need their bodies even without Call edges. + if( !isClz() && in(1) instanceof StartCUNode start ) { + boolean preOpto = CodeGen.CODE._phase == null || + CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal(); + if( !preOpto && !start.escapedFIDX(_sig.fidx()) && nIns()<=2 && _ret!=null ) { + _compunit._stop.delDef(_compunit._stop._inputs.find(ret())); + return removeDeadPath(1); + } else { + addDep(start); + } + } - // When can we assume no callers? Or no other callers (except main)? - // In a partial compilation, we assume Start gets access to any/all - // top-level public structures and recursively what they point to. - // This in turn is valid arguments to every callable function. - // - // In a total compilation, we can start from Start and keep things - // more contained. // If no default/unknown caller, use the normal RegionNode ideal rules // to collapse @@ -158,62 +183,230 @@ public class FunNode extends RegionNode { } // Bypass Region idom, always assume depth == 1, one more than Start, - // unless folding then just a ID on input#1 + // unless folding @Override public int idepth() { return _folding ? super.idepth() : 1; } + // Bypass Region idom, always assume idom is Start - @Override public CFGNode idom(Node dep) { return _folding && nIns()==3 ? cfg(2) : (nIns()>1 ? cfg(1) : null); } + @Override public CFGNode idom(Node dep) { + return _folding ? cfg(1) : null; + } // Always in-progress until we run out of unknown callers public boolean unknownCallers() { return nIns()>=2 && in(1) instanceof StartNode; } - @Override public boolean inProgress() { return unknownCallers(); } + public boolean isModInit( ) { + // The one top-level <clinit> with no internal dots: + // "sys.<clinit>" is a module, but "sys.io.<clinit>" is not. + return isClz() && _name.indexOf('.')==_name.lastIndexOf('.'); + } + public boolean isClz ( ) { return isClz (_name); } + public boolean isInit( ) { return isInit(_name); } + public boolean isInstance() { return isInstance(_name); } + public static boolean isClz (String name ) { return name!=null && name.endsWith(".<clinit>"); } + public static boolean isInit(String name ) { return name!=null && name.endsWith("init>"); } + public static boolean isInstance(String name ) { return name!=null && name.endsWith(".<init>"); } + + // Allocation factories and instance initializers carry extra memory boilerplate. + public int inlineLimit() { + return isInstance() || (_name!=null && _name.endsWith(".<new>")) ? 200 : 100; + } - // Add a new function exit point. - public void addReturn(Node ctrl, Node mem, Node rez) { _ret.addReturn(ctrl,mem,rez); } + // Function is public (callable from Start directly). + public boolean isPublic( ) { + // Never true for anonymous functions + if( _name == null ) return false; + // Only public for <clinit> and <init>, all others + // are found via field loads. The <init> Start hook also supplies its + // conservative pre-Opto construction inputs; StopCU separately keeps + // its private-memory return from escaping as a public result. + if( !isInit() ) return false; + // Private class, has to be found via program text, not linker + if( _name.charAt(0)=='_' ) return false; + + // No instances of "...clz1._clz2..." where the class name path + // includes a private class. + if( _name.indexOf("._") != -1 ) return false; + + // Public class <cl/init> + return true; + } - // Build the function body - public BitSet body() { + @Override public boolean inProgress() { + return unknownCallers() && CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal(); + } - // Reverse up (stop to start) CFG only, collect bitmap. + // Build the function body set + public BitSet body() { + // Iter drains ordinary peeps between inlines; body discovery precedes + // this inline's folding, so function boundaries are stable. + assert !_folding; + // Forward control includes paths that never reach Return. BitSet cfgs = new BitSet(); - cfgs.set(_nid); - walkUp(ret(),cfgs ); + walkCFG(this,cfgs); + cfgs.set(ret()._nid); // Top down (start to stop) all flavors. CFG limit to bitmap. // If data use bottoms out in wrong CFG, returns false - but tries all outputs. // If any output hits an in-CFG use (e.g. phi), then keep node. BitSet body = new BitSet(); - walkDown(this, cfgs, body, new BitSet()); + // The function owns its Return even when it has no executable exit. + body.set(ret()._nid); + BitSet visit = new BitSet(); + int old; + do { + old = body.cardinality(); + visit.clear(); + walkDown(this, cfgs, body, visit); + } while( old != body.cardinality() ); return body; } - private static void walkUp(CFGNode n, BitSet cfgs) { + private static void walkCFG(CFGNode n, BitSet cfgs) { if( cfgs.get(n._nid) ) return; cfgs.set(n._nid); - if( n instanceof RegionNode r ) - for( int i=1; i<n.nIns(); i++ ) - walkUp(n.cfg(i),cfgs); - else walkUp(n.cfg0(),cfgs); + if( n instanceof ReturnNode ) return; + for( Node use : n._outputs ) { + if( !(use instanceof CFGNode cfg) || cfg instanceof FunNode ) continue; + if( cfg instanceof RegionNode ? cfg._inputs.find(n)>0 : cfg.in(0)==n ) + walkCFG(cfg,cfgs); + } } private static boolean walkDown( Node n, BitSet cfgs, BitSet body, BitSet visit ) { + if( n==null ) return false; if( visit.get(n._nid) ) return body.get(n._nid); visit.set(n._nid); - + // Visit self as CFG outside the function if( n instanceof CFGNode && !cfgs.get(n._nid) ) return false; + // Pretend the NewNode is a CFG, so its projections stay with it in loops + if( n instanceof NewNode nnn ) cfgs.set(n._nid); + // Visit n.cfg() outside of function if( n.in(0)!=null && !cfgs.get(n.in(0)._nid) ) return false; + // Phis inside the function must have their body flag set BEFORE + // recursion walks around a loop and finds them again. + if( n instanceof PhiNode phi && cfgs.get(phi.in(0)._nid) ) + body.set(phi._nid); // Find data cycles boolean in = n.in(0)!=null || n instanceof CFGNode; - for( Node use : n._outputs ) + for( Node use : n._outputs ) { + // Will hit a backedge, so 'n' MUST be in the function, and must be + // set before the cyclic walk finds 'n' again. + if( use instanceof PhiNode puse && puse.in(0) instanceof LoopNode loop && puse.in(2)==n ) + body.set(n._nid); // Set before walk in |= walkDown(use,cfgs,body,visit); + } if( in ) body.set(n._nid); return in; } - // FunNodes must match signature (equivalent: no 2 FunNodes are ever GVN'able) + // Clone function body. Give function a new FIDX. + FunNode copyBody() { + // Build the function body BitSet + BitSet body = body(); + assert body.cardinality() < inlineLimit(); + // Walk the body, cloning + IdentityHashMap<Node,Node> map = new IdentityHashMap<>(); + BitSet visit = CodeGen.CODE.visit(); + bodyCopy( visit, body, map, this ); + bodyCopy( visit, body, map, _ret ); // May be disconnected from executable control. + visit.clear(); + bodyEdge( visit, body, map, this ); + bodyEdge( visit, body, map, _ret ); + visit.clear(); + assert map.size()==body.cardinality(); + + // New function/return cross-link each other + FunNode fun2 = (FunNode )map.get(this); + ReturnNode ret2 = (ReturnNode)map.get(_ret); + fun2._ret = ret2; + ret2._fun = fun2; + BitSet body2 = new BitSet(); + for( Node n : map.values() ) + body2.set(n._nid); + + // Remove non-body callers. Body-local call edges were cloned along + // with their CallEnds and must stay linked so their cloned types remain + // monotonic during the post-Opto Iter. + for( int i=1; i<fun2.nIns(); i++ ) + if( !(fun2.in(i) instanceof CallNode call) || !body2.get(call._nid) ) + fun2.removeDeadPath(i--); + + // Linking adds graph edges and worklist entries; identity-map order + // would make inlining depend on the host JVM's object hashes. + Node[] olds = map.keySet().toArray(new Node[0]); + Arrays.sort(olds, (x,y) -> Integer.compare(x._nid,y._nid)); + for( Node old : olds ) + if( old instanceof CallNode oldCall ) + relinkClonedCall(oldCall,(CallNode)map.get(oldCall),map); + + // Flip to a new FIDX to avoid confusion with the old one. + // This is a non-monotonic (sideways) type move, only applicable + // because fun2 is new. The fidx is also a purely local fiction, + // since this body will inline and the fidx goes dead. + fun2._sig = _sig.makeFrom(CodeGen.CODE._fidxs.nextInline()); + return fun2; + } + + private static void relinkClonedCall( CallNode oldCall, CallNode call, IdentityHashMap<Node,Node> map ) { + CallEndNode oldCend = oldCall.cend(); + if( oldCend==null || oldCend._folding ) + return; + for( int i=1; i<oldCend.nIns(); i++ ) { + ReturnNode oldRet = (ReturnNode)oldCend.in(i); + FunNode oldFun = oldRet.fun(); + FunNode fun = (FunNode)map.get(oldFun); + if( fun==null ) fun = oldFun; + if( !call.linked(fun) ) + call.link(fun); + } + CallEndNode cend = call.cend(); + if( cend.nIns() > 1 ) + cend._type = cend.compute(); + } + + // Clone small function + private void bodyCopy( BitSet visit, BitSet body, IdentityHashMap<Node,Node> map, Node n ) { + if( n==null ) return; + if( !body.get(n._nid) ) return; // Not part of the body + if( visit.get(n._nid) ) return; // Been there, done that + visit.set(n._nid); + Node m = n.copy(); + CodeGen.CODE.add(m); + map.put(n,m); + m._type = n._type; + for( Node x : n._outputs ) + bodyCopy(visit,body,map,x); + } + private void bodyEdge( BitSet visit, BitSet body, IdentityHashMap<Node,Node> map, Node n ) { + if( n==null ) return; + if( !body.get(n._nid) ) return; // Not part of the body + if( visit.get(n._nid) ) return; // Been there, done that + visit.set(n._nid); + Node m = map.get(n); + for( Node e : n._inputs ) { + Node x = e; + if( e != null ) { + if( n instanceof CallEndNode cend && e != cend.call() && + (!(e instanceof ReturnNode ret) || map.get(ret.fun()) == null) ) + continue; + x = map.get(e); + if( x == null ) { + if( n instanceof CallEndNode cend && e != cend.call() ) + continue; + x = e; + } + } + m.addDef(x); + } + for( Node x : n._outputs ) + bodyEdge(visit,body,map,x); + } + + + // no 2 FunNodes are ever GVN'able @Override public boolean eq( Node n ) { - return _sig == ((FunNode)n)._sig; + return this==n; } @Override public int hash() { @@ -267,12 +460,4 @@ public class FunNode extends RegionNode { return slotRotate*8; } - - @Override boolean _upgradeType(java.util.HashMap<String,Type> types) { - TypeFunPtr next = (TypeFunPtr)_sig.upgradeType(types); - if( next==_sig ) return false; - unlock(); - _sig=next; - return true; - } } diff --git a/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java b/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java index 43242c32..3d6031b8 100644 --- a/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java +++ b/src/main/java/com/seaofnodes/simple/node/FunPtrNode.java @@ -1,12 +1,62 @@ package com.seaofnodes.simple.node; -import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.print.ExprPrinter; -// A function address keeps its body alive, even without a linked Call. -public class FunPtrNode extends ConstantNode { - public FunPtrNode(TypeFunPtr tfp, ReturnNode ret) { - super(tfp); - addDef(ret); +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// Constant function pointer whose type follows the callee signature. +public class FunPtrNode extends TypeNode { + public FunPtrNode(TypeFunPtr tfp, StartNode start, ReturnNode ret) { + super(tfp,start,ret); } + public FunPtrNode(FunPtrNode fptr) { super(fptr); } + + public ReturnNode ret() { return (ReturnNode)in(1); } + public FunNode fun() { return ret().fun(); } + + @Override public String label() { return "#"+_con; } + @Override public Node copy() { + FunPtrNode fptr = new FunPtrNode((TypeFunPtr)_con,null,null); + fptr._type = _type; + return fptr; + } + + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + FunNode fun = CodeGen.CODE._link(((TypeFunPtr)_con).fidx()); + return fun!=null && fun._name!=null + ? p.p("{ ").p(fun._name).p("}") + : p.p(_con); + } + + @Override public boolean isConst() { return true; } + @Override public Type compute() { + ReturnNode ret = ret(); + if( ret == null ) return _con; + if( !(ret._type instanceof TypeTuple tt) ) return ret._type.oob(); + TypeFunPtr tfp = ret.fun().sig(); + // Both the sig and the return value must be true, so JOIN both + Type tret = tfp._ret.join(tt.ret()); + return tfp.makeFrom(tret); + } + @Override public Node idealize() { + ReturnNode ret = ret(); + if( ret==null ) return null; + // Function died (never executed), but fcn ptr is alive. + // Can be checked for null, or for unequals another FunPtr. + FunNode fun = addDep(ret.fun()); + TypeFunPtr sig = fun.sig(); + if( sig != _con ) + { liftType(sig); return this; } + // TODO: If the function body dies but this value remains live only as + // a null/equality sentinel, preserve the identity without the Return. + if( fun.isDead() ) + { setDef(1,null); liftType(sig.makeFrom(Type.TOP)); return this; } + return null; + } } diff --git a/src/main/java/com/seaofnodes/simple/node/GuardNode.java b/src/main/java/com/seaofnodes/simple/node/GuardNode.java index f808c276..e985da72 100644 --- a/src/main/java/com/seaofnodes/simple/node/GuardNode.java +++ b/src/main/java/com/seaofnodes/simple/node/GuardNode.java @@ -25,7 +25,6 @@ public class GuardNode extends Node { } - @Override public String label() { return _nonZero ? "(!=0)" : "(==0)"; } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { @@ -46,7 +45,6 @@ public class GuardNode extends Node { // choosing generic ptr here can be weaker than a later concrete type. if( t==TypeScalar.BOT || t==TypeScalar.TOP ) return t; Type t2 = _nonZero ? t.nonZero() : t.makeZero(); - if( t2!=null ) t2 = t.join(t2); return t2==null ? (_type==null ? Type.TOP : _type) : t2; // No sane answer for e.g. not-zero of a zero } @@ -54,8 +52,6 @@ public class GuardNode extends Node { if( in(0)._type != Type.CONTROL && in(0)._type != Type.BOTTOM ) return null; if( in(1)._type==Type.BOTTOM || in(1)._type==Type.TOP ) return null; - Type t = in(1)._type; - if( _nonZero ? !t.makeZero().isa(t) : t==t.makeZero() ) return in(1); if( _type.isHighOrConst() && in(1)._type.isa(_type) ) return in(1); return null; diff --git a/src/main/java/com/seaofnodes/simple/node/IfNode.java b/src/main/java/com/seaofnodes/simple/node/IfNode.java index f1b76dc2..471a28fe 100644 --- a/src/main/java/com/seaofnodes/simple/node/IfNode.java +++ b/src/main/java/com/seaofnodes/simple/node/IfNode.java @@ -14,8 +14,6 @@ public class IfNode extends CFGNode implements MultiNode { } public IfNode(IfNode iff) { super(iff); } - @Override - public String label() { return "If"; } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { @@ -46,7 +44,7 @@ public class IfNode extends CFGNode implements MultiNode { // If constant is 0 then false branch is reachable // Else true branch is reachable if( t.isConstant() ) - return (t==Type.NIL || t==TypeInteger.ZERO || (t instanceof TypeFunPtr tfp && tfp._fidxs==com.seaofnodes.simple.type.XInt.EMPTY) ) ? TypeTuple.IF_FALSE : TypeTuple.IF_TRUE; + return (t==Type.NIL || t==TypeInteger.ZERO || (t instanceof TypeFunPtr tfp && tfp._fidxs== XInt.EMPTY) ) ? TypeTuple.IF_FALSE : TypeTuple.IF_TRUE; // If adding a zero makes a difference, the predicate must not have a zero/null if( !t.makeZero().isa(t) ) return TypeTuple.IF_TRUE; @@ -59,17 +57,14 @@ public class IfNode extends CFGNode implements MultiNode { // Hunt up the immediate dominator tree. If we find an identical if // test on either the true or false branch, that side wins. if( !pred()._type.isHighOrConst() ) - for( CFGNode dom = idom(), prior=this; dom!=null; prior = dom, dom = dom.idom() ) { - if( addDep(dom) instanceof IfNode iff && prior instanceof CProjNode prj && + for( CFGNode dom = idom(), prior=this; dom!=null; prior = dom, dom = dom.idom() ) + if( addDep(dom) instanceof IfNode iff && + prior instanceof CProjNode prj && (addDep(iff.pred())==pred() || - (pred() instanceof GuardNode guard && guard._nonZero && iff.pred()==guard.in(1))) ) { + pred() instanceof GuardNode cast && cast._nonZero && iff.pred()==cast.in(1) ) ) { setDef(1,con( prj._idx==0 ? 1 : 0 )); return this; } - if( dom instanceof RegionNode r ) - for( Node c : r._inputs ) - if( c!= null ) addDep(c); // If region loses input, idom changes - } return null; } diff --git a/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/src/main/java/com/seaofnodes/simple/node/LoadNode.java index b9dd2af6..e1a495ad 100644 --- a/src/main/java/com/seaofnodes/simple/node/LoadNode.java +++ b/src/main/java/com/seaofnodes/simple/node/LoadNode.java @@ -2,8 +2,8 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; -import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.Utils; import java.util.BitSet; @@ -13,7 +13,6 @@ import java.util.BitSet; * in chapter 10 this means Struct fields. */ public class LoadNode extends MemOpNode { - private final boolean _private; /** * Load a value from a ptr.field. @@ -23,16 +22,11 @@ public class LoadNode extends MemOpNode { * @param ptr The ptr to the struct base from where we load a field * @param off The offset inside the struct base */ - public LoadNode(Parser.Lexer loc, String name, int alias, Node mem, Node ptr, Node off) { - this(loc,name,alias,mem,ptr,off,false); - } - public LoadNode(Parser.Lexer loc, String name, int alias, Node mem, Node ptr, Node off, boolean priv) { - // The field declaration will supply storage width once the receiver - // is known. A parser guess must not bound the value of this Load. - super(loc, name, alias, true, Type.BOTTOM, mem, ptr, off); - _private=priv; + public LoadNode(Parser.Lexer loc, String name, int alias, Node ctrl, Node mem, Node ptr, Node off) { + super(loc, name, alias, true, Type.BOTTOM, ctrl, mem, ptr, off); } + @Override boolean canDrop(MemOpNode other, Node dep) { return super.canDrop(other,dep) && !clobbered(dep); } @@ -47,7 +41,8 @@ public class LoadNode extends MemOpNode { dep.addDepForwards(use); if( use instanceof StoreNode || use instanceof PhiNode || use instanceof MemMergeNode || - use instanceof NewNode || use instanceof CallNode ) return true; + use instanceof NewNode || use instanceof EscapeNode || use instanceof CallNode || + use instanceof CLInitNode ) return true; } return false; } @@ -59,73 +54,105 @@ public class LoadNode extends MemOpNode { @Override public Type compute() { - Type p = ptr()._type; - Type m = mem()._type; - if( p.isHigh() || m.isHigh() ) return TypeScalar.TOP; - if( p==Type.NIL || p==Type.BOTTOM ) return Type.BOTTOM; - if( !(p instanceof TypeMemPtr ptr) ) return TypeScalar.BOT; - if( ptr.nullable() ) return Type.BOTTOM; - Field field = ptr._obj.field(_name); - if( field==null ) return TypeScalar.BOT; - if( field._t instanceof TypeFunPtr ctor && ctor.ret() instanceof TypeMem ) return TypeScalar.BOT; - Type t = field._t; - if( t instanceof TypeConAry ary ) t=ary.elem(); - if( _alias!=1 ) t=t.join(MemMergeNode.contents(mem(),_alias,this)); - // An unknown helper result does not mean the field is unwritten. - // Keep its declared bound until private memory proves otherwise; - // this Load can be forwarded into an ordinary typed value Phi. - if( uninitialized() ) t=Type.BOTTOM; - if( m instanceof TypeMem pm && pm._t instanceof TypeStruct shape ) { - Field value = shape.field(_name); - t = value==null ? Type.BOTTOM : value._t; - } - if( ptr.isFinal() ) t=t.makeRO(); + Type mem0 = mem()._type; + Type ptr0 = ptr()._type; + // Validate argument types + if( ptr0.isHigh() ) + return TypeScalar.TOP; + if( !(mem0 instanceof TypeMem mem) ) + return mem0.isHigh() ? TypeScalar.TOP : TypeScalar.BOT; + if( mem._t == Type.TOP ) + return TypeScalar.TOP; + if( ptr0 == Type.NIL ) + return Type.BOTTOM; // Load from nil is error + if( ptr0 == Type.BOTTOM ) + return Type.BOTTOM; // Error in is error out + if( !(ptr0 instanceof TypeMemPtr tmp) ) + return scalar(mem._t); + if( tmp.nullable() ) + return Type.BOTTOM; // Load from nil-able is error + + // Load field from object + Field pfld = tmp._obj.field(_name); + // No field? Open objects might yet get the field when falling; + // closed objects with missing field are an error. + if( pfld == null ) + return tmp.isHigh() || tmp._obj.isHigh() ? TypeScalar.TOP : TypeScalar.BOT; + + Type t = pfld._t; + boolean readonly = t instanceof TypeMemPtr view && view._ro; + // Load member of constant array + if( t instanceof TypeConAry ary ) + t = ary.elem(); // TODO: if offset is known, can peek the constant + if( mem._alias==_alias ) + t = t.join(mem._t); + + // A deeply read-only base produces a deeply read-only value. The + // generic declared field type must not cast this information away. + if( ptr0.isFinal() || readonly ) + t = t.makeRO(); + return scalar(t); + } + + // Loads produce ordinary scalar values. Field/memory joins can still + // expose the enclosing global lattice bounds through an uninitialized + // Type.TOP/BOTTOM field; keep those results inside the scalar envelope. + private static Type scalar(Type t) { return t==Type.TOP ? TypeScalar.TOP : t==Type.BOTTOM ? TypeScalar.BOT : t; } @Override public Node idealize() { - if( ptr()._type instanceof TypeMemPtr ptr && !ptr._obj._open ) { - Field field = ptr._obj.field(_name); - // Storage width comes from the declaration, not from a constant - // or range inferred for this particular object's current value. - Type declared = Parser.TYPES.get(ptr._obj._name); - if( declared instanceof TypeMemPtr shape && shape._obj.field(_name)!=null ) - field=shape._obj.field(_name); - Type storage = field==null ? Type.BOTTOM : field._t instanceof TypeConAry ary ? ary.elem() : field._t; - if( field!=null && (_alias!=field._alias || _declaredType!=storage) ) { - unlock(); - _alias=field._alias; - _declaredType=storage; - return this; - } - } - if( _alias==1 ) return null; - // A required field has no value yet. In particular, do not turn a - // read of its calloc bytes into a legal null/zero value. - if( uninitialized() ) return null; + Node mem = mem(); + Node ptr = ptr(); - if( mem() instanceof CastNode cast ) { - setDef(1,cast.in(1)); + // Null checks are handled by types. Array elements retain control for + // range checks, but reading an array's length has no index to check. + if( in(0)!=null && ptr._type instanceof TypeMemPtr tmp && + (!tmp._obj.isAry() || _name.equals("#")) ) { + setDef(0,null); return this; } - if( mem() instanceof MemMergeNode merge ) { - setDef(1,CodeGen.CODE.add(merge.alias(_alias))); + + // Forward-ref loads eventually sharpen to a declared type + Field fld; + if( ptr._type instanceof TypeMemPtr tmp && + (fld=tmp._obj.field(_name)) != null && + _alias != fld._alias) { + assert _alias==1 || _alias == fld._alias; + assert !tmp._obj._open && !tmp._obj._fref; + unlock(); // Alias participates in GVN semantics + _alias = fld._alias; return this; } - Node ptr = ptr(); - Node mem = mem(); + // Must sharpen alias first + if( _alias == 1 ) + return null; - Node win = find(mem,ptr,null,null); - if( win!=null ) return folded(win); + Node slice = CLInitNode.slice(mem,_alias,this); + if( slice!=mem ) { setDef(1,slice); return this; } + + if( mem instanceof MemMergeNode merge ) { + setDef(1,merge.alias(_alias)); + // In support of uplifting control to a prior dominating load, if + // we move our memory, recheck other loads to see if they can hoist. + for( Node use : in(1).outs() ) + if( use instanceof LoadNode ld && use != this && ptr == ld.ptr() ) + CodeGen.CODE.add(ld); + + return this; + } // Uplift control to a prior dominating load. for( Node memuse : mem._outputs ) // Find a prior load, has same mem,ptr,off but higher ctrl if( memuse != this && memuse instanceof LoadNode ld && ptr==ld.ptr() && off()==ld.off() && - cfg0()!=null && cfg0().domLCA(ld.cfg0(),this) == ld.cfg0() ) // Higher control means load is legal earlier + cfg0()!=null && cfg0().domLCA(ld.cfg0(),this) == ld.cfg0() && // Higher control means load is legal earlier + addDep(ld)._type.isa(_type) ) // and not rolling backwards return ld; + Node win = find(mem,ptr,null,null); + if( win!=null ) return folded(win); if( ptr instanceof ReadOnlyNode ro ) ptr=ro.in(1); // Push a Load up through a Phi, as long as it collapses on at least @@ -150,41 +177,26 @@ public class LoadNode extends MemOpNode { !(ptr instanceof PhiNode p && p.region()==memphi.region()) ? memphi : null; if( find(memphi.in(2),ptr,stop,new BitSet())!=null || (!(memphi.region() instanceof LoopNode) && find(memphi.in(1),ptr,null,new BitSet())!=null) ) { + // Integer loop Phis widen to i64. Keep the load's declared + // width at the result, as for ordinary typed loop variables. + if( _type instanceof TypeInteger && !declaredType().isa(_type) ) return null; PhiNode phi = new PhiNode(_name,memphi.region()).keep(); - phi.setType(_type); // Backedge searches may return this value Phi. + phi.setType(_type instanceof TypeInteger ? TypeInteger.BOT : _type); for( int i=1; i<memphi.nIns(); i++ ) { Node p = ptr instanceof PhiNode pp && pp.region()==memphi.region() ? pp.in(i) : ptr; phi.addDef(load(memphi.in(i),p,stop,phi)); } - return CodeGen.CODE.add(phi.unkeep()); + CodeGen.CODE.add(phi.unkeep()); + return _type instanceof TypeInteger ? new ConvertNode(declaredType(),phi) : phi; } } return null; } - private boolean uninitialized() { - if( !_private || _alias==1 ) return false; - Node mem=mem(); - if( mem instanceof MemMergeNode merge ) mem=merge.alias(_alias); - return mem._type instanceof TypeMem m && - ((m._alias==_alias && m._t==Type.BOTTOM) || - (m._t instanceof TypeStruct s && s.field(_name)!=null && s.field(_name)._t==Type.BOTTOM)); - } - - @Override public Parser.ParseException err() { - if( ptr()._type instanceof TypeMemPtr ptr ) { - Field f=ptr._obj.field(_name); - if( f!=null && f._t instanceof TypeFunPtr ctor && ctor.ret() instanceof TypeMem ) - return Parser.error("A constructor is only called by new",_loc); - } - return uninitialized() ? Parser.error("Cannot read uninitialized field '"+_name+"'",_loc) : super.err(); - } - // Load a flavored zero from a New private Node zero(NewNode nnn) { - TypeStruct ts = nnn._ptr._obj; - Type zero = ts._fields[ts.findAlias(_alias)]._t.makeZero(); + Type zero = declaredType().makeZero(); assert zero.isa(_type); // Catch an uninitialized non-null field return castRO(new ConstantNode(zero).peephole()); } @@ -193,36 +205,40 @@ public class LoadNode extends MemOpNode { // the unchanged loop memory, or a merge whose every arm folds. Null fails. // Register dependencies so a later pointer/offset rewrite retries the query. private Node find(Node mem, Node ptr, Node stop, BitSet visit) { - ptr = address(ptr); + if( ptr instanceof ReadOnlyNode ro ) ptr=ro.in(1); + Field fld; + boolean external = ptr._type instanceof TypeMemPtr tmp && + (fld=tmp._obj.field(_name))!=null && fld._extern; while( mem!=null ) { addDep(mem); if( mem==stop ) return mem; switch( mem ) { case MemMergeNode merge: mem=merge.alias(_alias); break; - case CastNode cast: mem=cast.in(1); break; - case EscapeNode esc: - mem = ptr==address(esc.self()) ? esc.priv() : esc.pub(); - break; case StoreNode st: - // A symbolic Store may write this field. Wait for its alias; - // skipping it can make a compound assignment read the old value. + // Alias 1 is still unresolved, not a proven different field. if( st._alias==1 ) return null; addDep(st.ptr()); - if( _alias==st._alias && ptr==address(st.ptr()) && off()==st.off() ) return st; - if( _alias==st._alias && !neverAlias(ptr,address(st.ptr())) && + if( _alias==st._alias && (ptr==st.ptr() || ptr==st.nnptr()) && off()==st.off() ) { + // A full-width load cannot narrow the forwarded value. + if( declaredType() instanceof TypeInteger ti && ti.log_size()==3 && + !addDep(st.val())._type.isa(_type) ) return null; + return st; + } + if( (external || _alias==st._alias) && (external || !neverAlias(ptr,st.ptr())) && !addDep(off())._type.join(addDep(st.off())._type).isHigh() ) return null; mem=st.mem(); break; case ProjNode proj: - if( !(proj.in(0) instanceof NewNode nnn) ) return null; - if( ptr instanceof ProjNode p && p.in(0)==nnn ) { - if( nnn.mem()==null && proj._type instanceof TypeMem m && - m._t instanceof TypeStruct shape && shape.field(_name)._t==Type.BOTTOM ) return null; - return nnn; - } - if( !(ptr instanceof ProjNode p && p.in(0) instanceof NewNode) ) return null; - mem=nnn.mem(); // Distinct allocations cannot overlap. - break; + // New's memory is private; public memory is joined at Escape. + if( external || !(proj.in(0) instanceof NewNode nnn) ) return null; + if( declaredType()==Type.BOTTOM ) return null; + return nnn; + case EscapeNode esc: + if( external ) return null; + addDep(esc.self()); + if( esc.self()==ptr ) { mem=esc.priv(); break; } + if( neverAlias(ptr,esc.self()) ) { mem=esc.pub(); break; } + return null; case MemPhiNode phi: addDep(phi.region()); // Only the original loop closes a successful cycle. Another @@ -240,13 +256,6 @@ public class LoadNode extends MemOpNode { return null; } - // Guards and read-only views keep the same address. Retain the Load's - // guarded input for its null check, but compare actual addresses here. - private static Node address(Node ptr) { - while( ptr instanceof ReadOnlyNode || ptr instanceof GuardNode ) ptr=ptr.in(1); - return ptr; - } - private Node folded(Node win) { return win instanceof StoreNode st ? extend(castRO(st.val())) : zero((NewNode)win); } @@ -260,12 +269,12 @@ public class LoadNode extends MemOpNode { if( win instanceof MemPhiNode mp ) { PhiNode phi=new PhiNode(_name,mp.region()).keep(); for( int i=1; i<mp.nIns(); i++ ) phi.addDef(load(mp.in(i),ptr,stop,value)); - return phi.unkeep().peephole(); + return CodeGen.CODE.add(phi.unkeep().peephole()); } - return folded(win).peephole(); + Node fold=folded(win); + if( fold!=null ) return fold.peephole(); } - LoadNode ld=new LoadNode(_loc,_name,_alias,mem,ptr,off(),_private); - ld.setDef(0,in(0)); + LoadNode ld=new LoadNode(_loc,_name,_alias,in(0),mem,ptr,off()); return ld.peephole(); } @@ -297,26 +306,26 @@ public class LoadNode extends MemOpNode { // Read-Only is a deep property, and cannot be cast-away private Node castRO(Node rez) { - if( (ptr()._type.isFinal() || (_declaredType instanceof TypeMemPtr p && p._ro)) && !rez._type.isFinal() ) + if( (ptr()._type.isFinal() || declaredType().isFinal()) && !rez._type.isFinal() ) return new ReadOnlyNode(rez).peephole(); return rez; } - @Override public boolean eq(Node n) { return _private==((LoadNode)n)._private && super.eq(n); } - // When a load bypasses a store, the store might truncate bits - and the // load will need to zero/sign-extend. private Node extend(Node val) { - if( !(_declaredType instanceof TypeInteger ti) ) return val; + if( !(declaredType() instanceof TypeInteger ti) ) return val; if( ti._min==0 ) // Unsigned return new AndNode(null,val,con(ti._max)); // Signed extension int shift = Long.numberOfLeadingZeros(ti._max)-1; Node shf = con(shift); - if( shf._type==TypeInteger.ZERO ) + if( shift==0 ) { + if( !val._type.isa(_type) ) + { addDep(val); return null; } return val; + } Node shl = new ShlNode(null,val,shf.keep()).peephole(); return new SarNode(null,shl,shf.unkeep()); } - } diff --git a/src/main/java/com/seaofnodes/simple/node/LoopNode.java b/src/main/java/com/seaofnodes/simple/node/LoopNode.java index 1d493db7..005a6624 100644 --- a/src/main/java/com/seaofnodes/simple/node/LoopNode.java +++ b/src/main/java/com/seaofnodes/simple/node/LoopNode.java @@ -3,23 +3,20 @@ package com.seaofnodes.simple.node; import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.type.TypeMem; -import com.seaofnodes.simple.util.Utils; -import java.util.BitSet; -import java.util.HashSet; + +import java.util.HashMap; +import java.util.IdentityHashMap; public class LoopNode extends RegionNode { - public LoopNode( Parser.Lexer loc, Node entry ) { super(loc,null,entry,null); } + public LoopNode( Parser.Lexer loc, Node xctrl, Node entry ) { super(loc,xctrl,entry,null); } public LoopNode( LoopNode loop ) { super(loop); } + public CFGNode entry() { return cfg(1); } public CFGNode back () { return cfg(2); } @Override - public String label() { return "Loop"; } - - @Override public Type compute() { if( inProgress() ) return Type.CONTROL; return entry()._type; @@ -46,7 +43,7 @@ public class LoopNode extends RegionNode { x = x.idom(); } // Found a no-exit loop. Insert an exit - NeverNode iff = (NeverNode)new NeverNode(back()).peephole(); // Ideal never-branch + NeverNode iff = (NeverNode)new NeverNode(1.2f,back()).peephole(); // Ideal never-branch CProjNode t = new CProjNode(iff,0,"True" ).init(); CProjNode f = new CProjNode(iff,1,"False").init(); setDef(2,t); // True continues loop, False (never) exits loop @@ -56,12 +53,13 @@ public class LoopNode extends RegionNode { // Now fold control into the exit. Might have 1 valid exit, or an // XCtrl or a bunch of prior NeverNode exits. - Node top = new ConstantNode(Type.TOP).peephole(); - MemMergeNode memout = new MemMergeNode(new ConstantNode(TypeMem.BOT).peephole()); - memout.setDef(0,f); + Node top = ConstantNode.make(Type.TOP).peephole(); + Node memout = new MemMergeNode(); + memout.addDef(f); // placeholder for control + memout.addDef(top); for( Node u : _outputs ) - if( u instanceof BulkMemPhiNode ) memout.setDef(1,u); - else if( u instanceof MemPhiNode phi ) memout.alias(phi._alias,phi); + if( u instanceof PhiNode phi && phi._type.isa(TypeMem.BOT) ) + memout.addDef(phi); memout.init(); Node ctrl = ret.ctrl(), mem = ret.mem(), expr = ret.expr(); @@ -73,7 +71,7 @@ public class LoopNode extends RegionNode { // Nope, insert an aligned exit layer RegionNode r = new RegionNode(_loc,null,ctrl).init(); ctrl = r; r._ltree = fun._ltree; - mem = new BulkMemPhiNode(ScopeNode.MEM0,r,mem).init(); + mem = new BulkMemPhiNode(r,mem).init(); expr = new PhiNode(r,expr).init(); } // Append new Never exit @@ -88,8 +86,12 @@ public class LoopNode extends RegionNode { ret.setDef(0,ctrl.unkeep()); ret.setDef(1,mem .unkeep()); ret.setDef(2,expr.unkeep()); - ret._type = null; // This may LIFT the Return type, from [XCtrl,mem,expr] to [Ctrl,mem,expr] - ret.setType(ret.compute()); + // Force sane types to exit; might locally break monotonicity + ret.init(); + ret.out(0).init(); + stop.init(); + CodeGen.CODE.add(CodeGen.CODE._start); + CodeGen.CODE.add(mem); return fun; } diff --git a/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java b/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java index 9c0755b9..b25fcaaf 100644 --- a/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java +++ b/src/main/java/com/seaofnodes/simple/node/MachConcreteNode.java @@ -3,7 +3,6 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.util.Utils; import java.io.ByteArrayOutputStream; // Generic machine-specific class, has a few Node implementations that have to @@ -14,8 +13,8 @@ public abstract class MachConcreteNode extends Node implements MachNode { public MachConcreteNode(Node[]nodes) { super(nodes); } @Override public String label() { return op(); } - @Override public Type compute () { throw Utils.TODO(); } - @Override public Node idealize() { throw Utils.TODO(); } + @Override public Type compute () { throw new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { p.open().p(op()).p(","); diff --git a/src/main/java/com/seaofnodes/simple/node/MachNode.java b/src/main/java/com/seaofnodes/simple/node/MachNode.java index c0b3af66..025b5dc4 100644 --- a/src/main/java/com/seaofnodes/simple/node/MachNode.java +++ b/src/main/java/com/seaofnodes/simple/node/MachNode.java @@ -55,6 +55,6 @@ public interface MachNode { // General form: "dst=src+src". default void asm(CodeGen code, SB sb) { throw Utils.TODO(); } - // Human readable opcode; e.g. "ld4" or "call". + // Human-readable opcode; e.g. "ld4" or "call". default String op() { throw Utils.TODO(); } } diff --git a/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java index a43667ff..cbf19c55 100644 --- a/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java +++ b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java @@ -5,117 +5,150 @@ import com.seaofnodes.print.ExprPrinter; import com.seaofnodes.simple.*; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; + import java.util.*; -/** A default slice and explicit alias overrides. A null default means partial - * memory: absent aliases are not covered, rather than having unknown contents. +/** + * Memory Merge - a merge of many aliases into a "fat memory". All aliases + * are here, but most will be lazy - take the default fat memory. */ public class MemMergeNode extends Node { + + public MemMergeNode( Node ...nodes) { super(nodes); _type = TypeMem.BOT; } public MemMergeNode(MemMergeNode mem) { super(mem); } - public MemMergeNode(Node bulk) { super(null,bulk); _type = TypeMem.BOT; } - public MemMergeNode(Node bulk, int alias, Node precise) { - this(bulk); - alias(alias,precise); - } - public Node alias(int alias) { - Node n = alias < nIns() ? in(alias) : null; - assert n!=null || !(in(1) instanceof BulkMemPhiNode bulk) || !bulk.isSplit(alias); - n = n==null ? in(1) : n; - assert n!=null : "Alias not covered: "+alias; - return n; - } - public void alias(int alias, Node mem) { - assert alias > 1; - while( nIns()<=alias ) addDef(null); - setDef(alias,mem); - } + @Override public String label() { return "ALLMEM"; } + @Override public boolean isMem() { return true; } - // Scalar contents of one alias. New zeroes the allocated fields; - // join it with the incoming contents for all previously allocated objects. - // Phis use their cached types, so this query does not recurse around loops. - static Type contents(Node mem, int alias, Node dep) { - // A loop memory Phi can also be a user of the querying Store. - // Its type changing still needs to revisit this query. - dep.addDepForwards(mem); - if( mem instanceof CastNode cast ) return contents(cast.in(1),alias,dep); - if( mem instanceof MemMergeNode merge ) - return contents(merge.alias(alias),alias,dep); - if( mem._type instanceof TypeMem mt && mt._t instanceof TypeStruct shape ) { - int index = shape.findAlias(alias); - return index==-1 ? Type.BOTTOM : shape._fields[index]._t; - } - if( mem instanceof ProjNode proj && proj.in(0) instanceof NewNode nnn ) { - assert proj._idx==1 && nnn.field(alias)!=null; - dep.addDep(nnn); - if( dep.addDep(nnn.in(0))._type.isHigh() ) return Type.TOP; - return contents(nnn.mem(),alias,dep).meet(nnn.field(alias)._t.makeZero()); + @Override + protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + p.p("MEM[ "); + for( int j=2; j<nIns(); j++ ) { + p.p(j); + p.p(":"); + Node n = in(j); + while( n instanceof ScopeNode loop ) { + p.p("Lazy_"); + n = loop.mem(); + } + if( n==null ) p.p("___ "); + else p.n(n).p(" "); } - // Function parameters and call results may contain arbitrary heap values. - // Their bulk memory types do not imply empty or zero-filled storage. - if( mem._type==Type.TOP || mem._type==TypeMem.TOP ) return Type.TOP; - if( mem._type instanceof TypeMem mt && mt._alias==alias ) return mt._t; - return Type.BOTTOM; + return p.unchar(' ').p("]"); } - @Override public String label() { return "MemMerge"; } - @Override public boolean isMem() { return true; } - @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { - p.p("MEM["); - for( int i=1; i<nIns(); i++ ) - if( in(i)!=null ) { - p.p(i==1 ? "default:" : i+":"); - p.n(in(i)).p(" "); + + @Override public Type compute() { + Type tmem = in(1)._type; + if( !(tmem instanceof TypeMem defmem) ) + // A MemMerge is structurally memory even while its default input + // is a weak, not-yet-specialized Phi. + return tmem.isHigh() ? TypeMem.TOP : TypeMem.BOT; + // Is this a single private instance memory? + if( defmem._one ) { + if( !(defmem._t instanceof TypeStruct ts) ) + return defmem; + // Perfect singleton memory, so all updates are parallel and + // independent and stack. + for( int i=2; i<nIns(); i++ ) { + Node in = in(i); + if( in !=null && in._type.isHigh() ) + return TypeMem.TOP; + if( in instanceof StoreNode st ) { + Type val = ((TypeMem)st._type)._t; + Field old = ts.field(st._name); + // Lazy add an expected field for open structs. + if( old==null ) { assert ts._open; + old = Field.make(st._name,Type.BOTTOM,st._alias,st._init); + ts = ts.add(old); + } + Field fld = old.makeFrom(val); + // TODO: val leaks into perfect singleton memory, along with its fidxs and aliases + ts = ts.replace(fld); + } } - return p.p("]"); - } + return TypeMem.makePrivate(ts); + } - @Override public Type compute() { - if( in(1)!=null && in(1)._type instanceof TypeMem base && base._t instanceof TypeStruct shape ) { - // At a helper boundary, package the independent field slices for - // the call result. Constructor loops themselves carry local values. - Field[] fields=shape._fields.clone(); - for( int i=0; i<fields.length; i++ ) { - Field f=fields[i]; - Node slice=f._alias<nIns() ? in(f._alias) : null; - if( slice!=null ) fields[i]=f.makeFrom(contents(slice,f._alias,this)); + // Normal public memory; meet across escaped inputs + TypeMem mem = defmem; + for( int i=2; i<nIns(); i++ ) { + // Has this alias escaped? + if( XInt.bit(mem._escAs,i) && + in(i) != null && !in(i)._type.isHigh() ) { + mem = (TypeMem)mem.meet(in(i)._type); } - return TypeMem.makePrivate(TypeStruct.make(shape._name,shape._open,fields)); } - TypeMem mem=in(1)==null ? TypeMem.START : in(1)._type instanceof TypeMem m ? m : - in(1)._type.isHigh() ? TypeMem.TOP : TypeMem.BOT; - // Only memory visible outside this graph contributes to the external - // summary. Newly exposed aliases feed back through Start on the next - // SCCP visit, including chains of pointers stored in other objects. - for( int i=2; i<nIns(); i++ ) - if( in(i)!=null && XInt.bit(mem._escAs,i) ) mem=mem.escapes(in(i)._type); return mem; } @Override public Node idealize() { + assert nIns() != 0 && in(1)!=null; // Always have a default. if( nIns()==0 ) return null; + + // Fold defaults into the default boolean progress=false, allDefault=true; for( int i=2; i<nIns(); i++ ) { - if( in(i) instanceof CastNode cast ) { setDef(i,cast.in(1)); progress=true; } - if( in(i)!=null && in(i)==in(1) ) { setDef(i,null); progress=true; } - if( in(i) instanceof MemMergeNode mem ) { - setDef(i,CodeGen.CODE.add(mem.alias(i))); - progress=true; - } - if( in(i)!=null ) allDefault=false; + if( in(1) == in(i) ) { setDef(i,null); progress=true; } + else { allDefault=false; } } - if( allDefault && in(1)!=null ) return in(1); + // If not merging any memory (all memory is just the default) + if( allDefault ) + return in(1); // Become default memory + + // Collapse stacked merged-mem if( in(1) instanceof MemMergeNode mem ) { - for( int i=2; i<mem.nIns(); i++ ) - if( mem.in(i)!=null && (i>=nIns() || in(i)==null) ) - alias(i,mem.in(i)); + // A dead memory cycle can lose its last use when we bypass mem. + // Keep the peephole's result alive until the worklist sees it. + keep(); + // Goal is to swap my default mem with mem's default mem + for( int i=2; i<mem.nIns(); i++ ) { + if( mem.in(i) != null ) { + // deeper default mem has a non-default + if( i>=nIns() || in(i)==null ) + setDefX(i,mem.in(i)); + } + } CodeGen.CODE.add(mem.in(1)); setDef(1,mem.in(1)); - return this; + return unkeep(); } - assert BulkMemPhiNode.checkMerge(this); + return progress ? this : null; } + + + public Node in( Var v ) { return in(v._idx); } + + public Node alias( int alias ) { + assert !(in(1) instanceof BulkMemPhiNode bulk && bulk.isSplit(alias) && + (alias >= nIns() || in(alias)==null)); + return alias < nIns() && in(alias)!=null ? in(alias) : in(1); + } + + public Node alias( int alias, Node st ) { return setDefX(alias,st); } + + // Now one-time do a useless-phi removal + void _useless( ) { + for( int i=1; i<nIns(); i++ ) { + if( in(i) instanceof PhiNode phi ) { + // Do an eager useless-phi removal + Node in = phi.peephole(); + CodeGen.CODE.addAll(phi._outputs); + phi.moveDepsToWorklist(); + if( in != phi ) { + if( !phi.iskeep() ) // Keeping phi around for parser elsewhere + phi.subsume(in); + setDef(i,in); // Set the update back into Scope + } + } + } + } + + @Override public boolean eq( Node n ) { + return this==n; + } } diff --git a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java index df1585ff..fa399e1d 100644 --- a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java +++ b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java @@ -3,12 +3,11 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; import com.seaofnodes.simple.Parser; -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeMemPtr; -import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; import java.lang.StringBuilder; -import java.util.BitSet; -import java.util.ArrayList; +import java.util.*; /** * Convenience common base for Load and Store. @@ -19,93 +18,84 @@ import java.util.ArrayList; * 3 - Offset, integer types * 4 - Value, for Stores only */ -public abstract class MemOpNode extends Node { +public abstract class MemOpNode extends TypeNode { // The equivalence alias class public int _alias; // True if load-like, false if store-like. + // Used on CPU-specific combined memory+arithmetic ops. // // Stores produce memory (maybe as part of a tuple with other things), // loads do not. // - // Loads might pick up anti-dependences on prior Stores, and never cause an + // Loads might pick up anti-dependencies on prior Stores, and never cause an // anti-dependence themselves. // // Stores must maximally sink to the least dominator use. Loads can be // opportunistically hoisted. public final boolean _isLoad; - // Declared type; not final because it might be a forward-reference - // which will be lazily improved when the reference is declared. - public Type _declaredType; - - // A debug name, no semantic meaning + // Field name. public final String _name; // Source location for late reported errors public final Parser.Lexer _loc; - public MemOpNode(Parser.Lexer loc, String name, int alias, boolean isLoad, Type glb, Node mem, Node ptr, Node off) { - super(null, mem, ptr, off); + public MemOpNode(Parser.Lexer loc, String name, int alias, boolean isLoad, Type decl, Node ctrl, Node mem, Node ptr, Node off) { + super(decl, ctrl, mem, ptr, off); _name = name; _alias = alias; - _declaredType = glb; _loc = loc; _isLoad = isLoad; } - public MemOpNode(Parser.Lexer loc, String name, int alias, boolean isLoad, Type glb, Node mem, Node ptr, Node off, Node value) { - this(loc,name, alias, isLoad, glb, mem, ptr, off); + public MemOpNode(Parser.Lexer loc, String name, int alias, boolean isLoad, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value) { + this(loc, name, alias, isLoad, decl, ctrl, mem, ptr, off); addDef(value); } - public MemOpNode( Node mach, MemOpNode mop ) { - super(mach); - _name = mop==null ? null : mop._name; - _alias = mop==null ? 0 : mop._alias; - _loc = mop==null ? null : mop._loc; - _isLoad= mop==null ? true : mop._isLoad; - _declaredType = mop==null ? Type.BOTTOM : mop._declaredType; - if( mop==null ) - throw Utils.TODO("Load or not"); + public MemOpNode( Node ideal, MemOpNode mop ) { + super(ideal,mop instanceof LoadNode ld ? ld.declaredType() : mop._con); + _name = mop._name; + _alias = mop._alias; + _loc = mop._loc; + _isLoad= mop._isLoad; } - // Used by M2 when translating graph to Simple - public MemOpNode( boolean isLoad ) { - super((Node)null); - _name = null; - _alias = 0; - _loc = null; - _isLoad = isLoad; - _declaredType = Type.BOTTOM; - } - static String mlabel(String name) { return "[]".equals(name) ? "ary" : ("#".equals(name) ? "len" : name); } + static String mlabel(String name) { return "[]"==name ? "ary" : ("#"==name ? "len" : name); } String mlabel() { return mlabel(_name); } - @Override public boolean isMem() { return !_isLoad; } - public Node mem() { return in(1); } public Node ptr() { return in(2); } public Node off() { return in(3); } + // Current declared field type. This is deliberately a live lookup: + // cyclic/forward types can sharpen by replacing the pointer's struct. + public Type declaredType() { + if( !(ptr()._type instanceof TypeMemPtr tmp) ) + return Type.BOTTOM; + Field fld = tmp._obj.field(_name); + return fld == null || fld._t == null ? Type.BOTTOM : fld._t; + } + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return _printMach(p); } protected ExprPrinter<Node> _printMach(ExprPrinter<Node> p) { throw Utils.TODO(); } - public int log_size() { return _declaredType.log_size(); } + // Extra conditions for factoring matching memory operations through a Phi. // The caller has already checked the opcode, input shape and control. boolean canDrop(MemOpNode other, Node dep) { dep.addDep(ptr()); - return _alias==other._alias && _declaredType==other._declaredType && err()==null; + return _alias>1 && _alias==other._alias && declaredType()==other.declaredType() && err()==null; } @Override public boolean eq(Node n) { MemOpNode mem = (MemOpNode)n; // Invariant - return _alias==mem._alias; // When comparing types error to use "equals"; always use "==" + return _alias==mem._alias && super.eq(mem); } @Override - int hash() { return _alias; } + int hash() { return _alias ^ super.hash(); } @Override public Parser.ParseException err() { @@ -114,43 +104,37 @@ public abstract class MemOpNode extends Node { if( ptr == Type.BOTTOM ) return null; if( ptr.isHigh() ) return null; // Assume it will fall to not-null // Better be a not-nil TMP - if( ptr instanceof TypeMemPtr tmp && tmp.notNull() ) - return null; - return Parser.error( "Might be null accessing '" + _name + "'",_loc); + if( !(ptr instanceof TypeMemPtr tmp && tmp.notNull()) ) + return Parser.error( "Might be null accessing '" + _name + "'",_loc); + if( tmp._obj.field(_name)==null && + CodeGen.CODE._phase.ordinal() > CodeGen.Phase.FinalizeIR.ordinal() ) + return Parser.error("Accessing unknown field '"+_name+"' from '*"+tmp._obj._name+"'",_loc); + // Sane field + if( off() instanceof ConFldOffNode && CodeGen.CODE._phase.ordinal() > CodeGen.Phase.FinalizeIR.ordinal() ) + return Parser.error("Accessing unknown field '"+_name+"' from '*"+tmp._obj._name+"'",_loc); + return null; } + // Memory writers which must follow this read. A packaging node is not a - // write, but can hide New's input, so follow this alias through aggregates. - public ArrayList<Node> antiDeps() { - ArrayList<Node> deps = new ArrayList<>(); - antiDeps(mem(),deps,new BitSet()); - return deps; + // write; follow this alias through aggregates to its writers. + public Ary<Node> antiDeps( Ary<Node> antideps ) { + antideps.clear(); + return antiDeps(mem(),antideps); } - private void antiDeps(Node mem, ArrayList<Node> deps, BitSet visit) { - if( visit.get(mem._nid) ) return; - visit.set(mem._nid); + private Ary<Node> antiDeps( Node mem, Ary<Node> antideps ) { for( Node use : mem._outputs ) { - if( use instanceof EscapeNode esc && esc._field._alias==_alias ) { - antiDeps(esc,deps,visit); - } else if( use instanceof MemMergeNode merge ) { - Node slice = _alias<merge.nIns() ? merge.in(_alias) : null; - if( (slice==mem) || (slice==null && merge.in(1)==mem) ) - antiDeps(merge,deps,visit); - } else if( use instanceof CallNode || + if( use instanceof MemMergeNode merge ) { + if( merge.alias(_alias)==mem ) + antiDeps(merge,antideps); + } else if( use instanceof CallNode || use instanceof CLInitNode || (use instanceof MemOpNode st && !st._isLoad && st._alias==_alias) || - (use instanceof NewNode nnn && nnn.field(_alias)!=null) || + (use instanceof EscapeNode esc && esc.fld()._alias==_alias) || (use instanceof MemPhiNode phi && phi._alias==_alias) || (use instanceof BulkMemPhiNode phi && !phi.isSplit(_alias)) ) { - if( !deps.contains(use) ) deps.add(use); + if( !antideps.contains(use) ) antideps.add(use); } } - } - - @Override boolean _upgradeType(java.util.HashMap<String,Type> types) { - Type next = _declaredType.upgradeType(types); - if( next==_declaredType ) return false; - unlock(); - _declaredType=next; - return true; + return antideps; } } diff --git a/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java b/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java index bca85e0d..5ae716a7 100644 --- a/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java +++ b/src/main/java/com/seaofnodes/simple/node/MemPhiNode.java @@ -1,45 +1,51 @@ package com.seaofnodes.simple.node; -import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeMem; -/** A Phi for one field alias, parallel to a bulk Phi at the same Region. */ +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** A Phi for one precise memory alias. */ public class MemPhiNode extends PhiNode { + public final int _alias; public MemPhiNode(String label, int alias, Node... inputs) { - super(label,inputs); + super(label, inputs); assert alias > 1; _alias = alias; } - public MemPhiNode(MemPhiNode phi) { super(phi); _alias = phi._alias; } + public MemPhiNode(MemPhiNode phi) { + super(phi,phi._label); + _alias = phi._alias; + } - @Override public boolean isMem() { return true; } - @Override public String label() { return "MemPhi_"+_alias; } - @Override public Type compute() { + @Override + public Type compute() { + Type t = super.compute(); assert BulkMemPhiNode.checkMem(this,_alias); - if( !(region() instanceof RegionNode r) ) - return (region()._type==Type.XCONTROL || region()._type==Type.TOP) ? TypeMem.TOP : _type; - if( r.inProgress() ) return r instanceof LoopNode ? TypeMem.make(_alias,Type.BOTTOM) : TypeMem.BOT; - Type t = Type.TOP; - TypeMem escapes=TypeMem.START; - for( int i=1; i<nIns(); i++ ) - if( addDep(r.in(i))._type!=Type.XCONTROL && r.in(i)._type!=Type.TOP ) { - t = t.meet(MemMergeNode.contents(in(i),_alias,this)); - escapes=escapes.escapes(in(i)._type); - } - return escapes.withContents(_alias,t); + if( !(t instanceof TypeMem tmem) ) return t; + assert tmem._alias==1 || tmem._alias==_alias; + // This Phi structurally knows its alias, but not whether the field is + // final. Do not infer slice finality from evolving input types. + return TypeMem.make(_alias,tmem._t,tmem._one,tmem._clz,false,tmem._escFs,tmem._escAs); } - @Override public Node idealize() { - for( int i=1; i<nIns(); i++ ) + @Override + public Node idealize() { + // A precise memory Phi can always bypass a MemMerge and select its + // matching slice. + for( int i=1; i<nIns(); i++ ) { if( in(i) instanceof MemMergeNode mem ) { - setDef(i,CodeGen.CODE.add(mem.alias(_alias))); + setDef(i,mem.alias(_alias)); return this; } + Node slice = CLInitNode.slice(in(i),_alias,this); + if( slice!=in(i) ) { setDef(i,slice); return this; } + } if( nIns()==3 && region() instanceof LoopNode loop && !loop.inProgress() && in(2) instanceof StoreNode st && st.mem()==this ) { Node sink = st.sink(this); @@ -48,6 +54,9 @@ public class MemPhiNode extends PhiNode { return super.idealize(); } - @Override public boolean eq(Node n) { return _alias==((MemPhiNode)n)._alias && super.eq(n); } + @Override public boolean eq(Node n) { + return _alias==((MemPhiNode)n)._alias && super.eq(n); + } + @Override int hash() { return _alias; } } diff --git a/src/main/java/com/seaofnodes/simple/node/MinusFNode.java b/src/main/java/com/seaofnodes/simple/node/MinusFNode.java deleted file mode 100644 index c3c27883..00000000 --- a/src/main/java/com/seaofnodes/simple/node/MinusFNode.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.seaofnodes.simple.node; - - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeFloat; - - -public class MinusFNode extends Node { - public MinusFNode(Node in) { super(null, in); } - public MinusFNode(MinusNode n) { super(n); } - - @Override public String label() { return "MinusF"; } - - @Override protected String format() { return "(-%1)"; } - - @Override - public Type compute() { - if( in(1)._type.isHigh() ) return TypeFloat.TOP; - if( in(1)._type instanceof TypeFloat i0) - return i0.isConstant() ? TypeFloat.constant(-i0.value()) : i0; - return TypeFloat.F64; - } - - @Override - public Node idealize() { - // -(-x) is x - if( in(1) instanceof MinusFNode minus ) - return minus.in(1); - - return null; - } -} diff --git a/src/main/java/com/seaofnodes/simple/node/MulFNode.java b/src/main/java/com/seaofnodes/simple/node/MulFNode.java deleted file mode 100644 index 1126319b..00000000 --- a/src/main/java/com/seaofnodes/simple/node/MulFNode.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeFloat; - -import java.util.BitSet; - -public class MulFNode extends ArithFNode { - public MulFNode(Node lhs, Node rhs) { super(lhs, rhs); } - public MulFNode(ArithNode n) { super(n); } - - @Override public String label() { return "MulF"; } - - @Override public String glabel() { return "*"; } - - @Override double doOp( double x, double y ) { return x*y; } - - @Override - public Node idealize() { - Node lhs = in(1); - Node rhs = in(2); - Type t1 = lhs._type; - Type t2 = rhs._type; - - // Mul of 1. We do not check for (1*x) because this will already - // canonicalize to (x*1) - if ( t2.isConstant() && t2 instanceof TypeFloat i && i.value()==1 ) - return lhs; - - // Move constants to RHS: con*arg becomes arg*con - if ( t1.isConstant() && !t2.isConstant() ) - return swap12(); - - return null; - } - @Override Node copy(Node lhs, Node rhs) { return new MulFNode(lhs,rhs); } -} diff --git a/src/main/java/com/seaofnodes/simple/node/MulNode.java b/src/main/java/com/seaofnodes/simple/node/MulNode.java index 37d6a2bc..49c6432a 100644 --- a/src/main/java/com/seaofnodes/simple/node/MulNode.java +++ b/src/main/java/com/seaofnodes/simple/node/MulNode.java @@ -10,6 +10,7 @@ public class MulNode extends ArithNode { public MulNode(Node lhs, Node rhs) { super(null, lhs, rhs); } public MulNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs, mode); } + @Override public String op() { return "*"; } @Override long doOp( long x, long y ) { return x * y; } @@ -35,7 +36,7 @@ public class MulNode extends ArithNode { // canonicalize to (x*1) long c = i.value(); if( c==1 ) return lhs._type.isa(compute()) ? lhs : super.idealize(); - if( c==0 ) return com.seaofnodes.simple.Parser.ZERO; + if( c==0 ) return CodeGen.CODE.ZERO; // Mul by a power of 2, +/-1. Bit patterns more complex than this // are unlikely to win on an X86 vs the normal "imul", and so // become machine-specific. diff --git a/src/main/java/com/seaofnodes/simple/node/MultiNode.java b/src/main/java/com/seaofnodes/simple/node/MultiNode.java index b4d6d78e..152d525a 100644 --- a/src/main/java/com/seaofnodes/simple/node/MultiNode.java +++ b/src/main/java/com/seaofnodes/simple/node/MultiNode.java @@ -25,5 +25,5 @@ public interface MultiNode { // Return not-null if this projection index is a ideal copy. // Called by ProjNode ideal and used to collapse Multis. - default Node pcopy(int idx) { return null; } + default Node pcopy(int idx, Node dep) { return null; } } diff --git a/src/main/java/com/seaofnodes/simple/node/NeverNode.java b/src/main/java/com/seaofnodes/simple/node/NeverNode.java index 66879b66..a7b4cf65 100644 --- a/src/main/java/com/seaofnodes/simple/node/NeverNode.java +++ b/src/main/java/com/seaofnodes/simple/node/NeverNode.java @@ -11,7 +11,8 @@ import com.seaofnodes.simple.type.TypeTuple; // "Never true" for infinite loop exits public class NeverNode extends IfNode { public NeverNode(NeverNode ctrl) { super(ctrl); } - public NeverNode(Node ctrl) { super(ctrl,null); } + public NeverNode(float ignore, Node ctrl) { super(ctrl,null); } + @Override public String label() { return "Never"; } diff --git a/src/main/java/com/seaofnodes/simple/node/NewNode.java b/src/main/java/com/seaofnodes/simple/node/NewNode.java index a43efba2..5cbe1b61 100644 --- a/src/main/java/com/seaofnodes/simple/node/NewNode.java +++ b/src/main/java/com/seaofnodes/simple/node/NewNode.java @@ -1,80 +1,65 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; - +import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; - - -/** Allocate a zeroed object. Inputs {ctrl, null, size}; results {ptr, private memory}. - * It starts outside the public heap. Helper exits store the field values, and - * Escape publishes the completed fields. Slot 1 remains reserved for memory; - * a non-null input supports the preceding chapter's graph fixtures. - */ +import java.util.HashMap; +import java.util.IdentityHashMap; + +/** + * Allocation! Allocate a chunk of memory, no init. + * Takes in ctrl and size, and TypeStruct. + * Produces a ptr and private memory. +*/ public class NewNode extends Node implements MultiNode { + public TypeStruct _ts; + public NewNode( TypeStruct ts, Node ctrl, Node size ) { super(ctrl,size); _ts = ts; } - public TypeMemPtr _ptr; + public Node ctrl() { return in(0); } + public Node size() { return in(1); } - public NewNode(TypeMemPtr ptr, Node... nodes) { - super(nodes); - assert !ptr.nullable(); - _ptr = ptr; - assert nodes.length==3; - assert nodes[0]._type==Type.CONTROL || nodes[0]._type==Type.XCONTROL; - assert nodes[1]==null || nodes[1]._type instanceof TypeMem; - assert nodes[2]._type instanceof TypeInteger || nodes[2]._type==Type.NIL; - } + // --- Serialization + public NewNode(NewNode nnn) { super(nnn); _ts = nnn._ts; } - public Node mem() { return in(1); } - public Node size() { return in(2); } - @Override boolean _upgradeType(java.util.HashMap<String,Type> types) { - TypeMemPtr ptr = (TypeMemPtr)_ptr.upgradeType(types); - if( ptr==_ptr ) return false; - unlock(); - _ptr = ptr; - return true; - } + @Override public String label() { return "new_"+(_ts.isAry() ? "ary_"+_ts._fields[1]._t.str() : _ts.str()); } - public Field field(int alias) { - for( Field f : _ptr._obj._fields ) - if( !f._one && f._alias==alias ) return f; - return null; + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { + return p.p("new ").p(_ts.str()); } - public NewNode(NewNode nnn) { super(nnn); _ptr = nnn._ptr; } + @Override public TypeTuple compute() { + if( ctrl()._type.isHigh() ) + return TypeTuple.DEAD_NEW; + TypeStruct ts = _ts.isAry() ? _ts : _ts.makeInit(); + return TypeTuple.make(TypeMemPtr.make(_ts), + // Arrays get pre-zeroed by constructor. + // Objects start with default-zero memory for + // fields that can be zero-initialized. This is + // still private memory, so no aliases escape yet. + TypeMem.make(1,ts,true,false,false,null,null)); + } + @Override public Node idealize() { return null; } - @Override public String label() { return "new_"+(_ptr._obj.isAry() ? "ary_"+_ptr._obj._fields[1]._t.str() : _ptr._obj.str()); } - @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { - return p.p("new ").p(_ptr._obj.str()); + @Override boolean _upgradeType( HashMap<String,Type> TYPES) { + Type old = _ts; _ts = (TypeStruct)_ts.upgradeType(TYPES); return old != _ts; } + // Two NewNodes are never equal; NewNodes always make independent pointers + // so they are not functional - they internally bump-ptr allocate always a + // new pointer. + @Override public boolean eq(Node n) { return this==n; } - @Override - public TypeTuple compute() { - return in(0)._type.isHigh() - ? TypeTuple.make(_ptr.makeHigh(),TypeMem.TOP) - : TypeTuple.make(_ptr,mem()==null ? TypeMem.makePrivate(TypeMem.zeroed(_ptr._obj)) : TypeMem.BOT); - } + @Override int hash() { return _ts.hashCode(); } - @Override - public Node idealize() { - Node progress=null; - for( int i=1; i<nIns(); i++ ) { - if( in(i) instanceof CastNode cast ) - progress = setDef(i,cast.in(1)); - } - return progress == null ? null : this; - } - @Override - public boolean eq(Node n) { return this == n; } + @Override public Parser.ParseException err() { + return _ts._fref ? Parser.error("Unknown struct type '"+_ts._name+"'",null) : null; + } - @Override - int hash() { return _ptr.hashCode(); } // ------------ // MachNode specifics, shared across all CPUs @@ -83,30 +68,31 @@ public class NewNode extends Node implements MultiNode { private RegMask _retMask; private RegMask _kills; public void cacheRegs(CodeGen code) { - _arg2Reg = code._mach.callArgMask(TypeFunPtr.CALLOC,2,0).firstReg(); - _arg3Mask = code._mach.callArgMask(TypeFunPtr.CALLOC,3,0); + _arg2Reg = code._mach.callArgMask(TypeFunPtr.CALLOC,2,0,code._cCallingConv).firstReg(); + _arg3Mask = code._mach.callArgMask(TypeFunPtr.CALLOC,3,0,code._cCallingConv); // Return mask depends on TFP (either GPR or FPR) _retMask = code._mach.retMask(TypeFunPtr.CALLOC); // Kill mask is all caller-saves, and any mirror stack slots for args // in registers. - RegMaskRW kills = code._callerSave.copy(); + RegMaskRW kills = code._cCallerSave.copy(); // Start of stack slots int maxReg = code._mach.regs().length; // Incoming function arg slots, all low numbered in the RA int fslot = cfg0().fun()._maxArgSlot; // Killed slots for this calls outgoing args - int xslot = code._mach.maxArgSlot(TypeFunPtr.CALLOC); + int xslot = code._mach.maxArgSlot(TypeFunPtr.CALLOC,code._cCallingConv); _xslot = (maxReg+fslot)+xslot; for( int i=0; i<xslot; i++ ) kills.set((maxReg+fslot)+i); _kills = kills; } public String op() { return "alloc"; } - public RegMask regmap(int i) { return i==2 ? _arg3Mask : null; } + public RegMask regmap(int i) { return i==1 ? _arg3Mask : null; } public RegMask outregmap() { return null; } public RegMask outregmap(int idx) { return idx==0 ? _retMask : null; } public RegMask killmap() { return _kills; } public void asm(CodeGen code, SB sb) { - sb.p("#calloc, ").p(code.reg(size())); + sb.p("#malloc, ").p(code.reg(size())); } + } diff --git a/src/main/java/com/seaofnodes/simple/node/Node.java b/src/main/java/com/seaofnodes/simple/node/Node.java index 230593a8..de39ca80 100644 --- a/src/main/java/com/seaofnodes/simple/node/Node.java +++ b/src/main/java/com/seaofnodes/simple/node/Node.java @@ -4,13 +4,10 @@ import com.seaofnodes.print.BaseNode; import com.seaofnodes.simple.IterPeeps; import com.seaofnodes.simple.Parser; -import com.seaofnodes.simple.util.Ary; -import com.seaofnodes.simple.util.Utils; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.print.IRPrinter; -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeFloat; -import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.*; import java.util.*; import java.util.function.Function; import static com.seaofnodes.simple.codegen.CodeGen.CODE; @@ -178,7 +175,8 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { } // Breaks the edge invariants, used temporarily - protected <N extends Node> void addUse(N n) { _outputs.add(n); } + @SuppressWarnings("unchecked") + protected <N extends Node> N addUse(Node n) { _outputs.add(n); return (N)this; } // Remove node 'use' from 'def's (i.e. our) output list, by compressing the list in-place. // Return true if the output list is empty afterward. @@ -200,6 +198,12 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { old_def.kill(); // Kill old def } } + public Node removeLast() { + Node old_def = _inputs.pop(); + if( old_def != null ) + old_def.delUse(this); + return old_def; + } /** * Kill a Node with no <em>uses</em>, by setting all of its <em>defs</em> @@ -236,11 +240,13 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { // Shortcuts to stop DCE mid-parse // Add bogus null use to keep node alive - @SuppressWarnings("unchecked") - public <N extends Node> N keep() { addUse(null); return (N)this; } + public <N extends Node> N keep() { return addUse(null); } // Remove bogus null. @SuppressWarnings("unchecked") - public <N extends Node> N unkeep() { delUse(null); return (N)this; } + public <N extends Node> N unkeep() { + delUse(null); + return (N)this; + } // Test "keep" status public boolean iskeep() { return _outputs.find(null) != -1; } public void unkill() { @@ -260,7 +266,6 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { n.unlock(); int idx = n._inputs.find(this); n._inputs.set(idx,nnn); - n.moveDepsToWorklist(); // Rewiring can change a dependent query without changing type. nnn.addUse(n); CODE.add(n); CODE.addAll(n._outputs); @@ -331,13 +336,13 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { * Try to peephole at this node and return a better replacement Node. * Always returns some not-null Node (often this). */ - public final Node peephole() { - var obs = CodeGen.CODE._midAssert ? null : CodeGen.CODE._obs; + public final Node peephole( ) { + var obs = CODE._midAssert ? null : CODE._obs; if( obs != null ) obs.before(this); Node n = peepholeOpt(); - Node rez = n == null ? this : deadCodeElim(n._nid >= _nid ? n.peephole() : n); - if( obs != null ) obs.after(this, n == null ? null : rez, false); - return rez; + Node rez = n==null ? this : deadCodeElim(n._nid >= _nid ? n.peephole() : n); + if( obs != null ) obs.after(this, n==null ? null : rez, false); + return rez; // Cannot return null for no-progress } /** @@ -363,11 +368,9 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { // Compute initial or improved Type Type old = setType(compute()); - // Replace constant computations from non-constants with a constant - // node. If peeps are disabled, still allow high Phis to collapse; - // they typically come from dead Regions, and we want the Region to - // collapse, which requires the Phis to die first. - if( !isConst() && _type.isHighOrConst() ) + // Replace constant computations from non-constants with a constant node. + if( !isConst() && !(this instanceof ParmNode parm && parm.inProgress()) && + _type.isHighOrConst() ) return ConstantNode.make(_type).peephole(); // Global Value Numbering @@ -435,7 +438,8 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { // If changing, add users to worklist. public Type setType(Type type) { Type old = _type; - assert old == null || type.isa(old) : "Monotonicity test failed"; + assert old == null || type.isa(old) : "Monotonicity test failed: "+this+"#"+_nid+" old="+old+" new="+type; + //assert !type.hasClosedBuilder0(); // New types should not have closed builders if( old == type ) return old; _type = type; // Set _type late for easier assert debugging CODE.addAll(_outputs); @@ -500,28 +504,33 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { // revisit them if `this` changes. Ary<Node> _deps; public int nDeps() { return _deps == null ? 0 : _deps.size(); } - public Node dep(int idx) { return _deps.get(idx); } + public Node dep(int idx) { return _deps.at(idx); } + + private boolean addDepImpl( Node dep ) { + // Running peepholes during the big assert cannot have side effects + // like adding dependencies. + if( CODE._midAssert ) return false; + if( CODE._obs != null ) CODE._obs.dep(this, dep); + if( dep._deps==null ) dep._deps = new Ary<>(Node.class); + if( dep._deps .find(this) != -1 ) return false; // Already on list + if( dep._outputs.find(this) != -1 ) return false; + return true; + } /** * Add a node to the list of dependencies. Only add it if it's not an input * or output of this node, that is, it is at least one step away. The node * being added must benefit from this node being peepholed. */ - <N extends Node> N addDep( N dep ) { return addDep(dep,false); } - - // Keep an explicit dependency when inspecting an immediate user's shape. - <N extends Node> N addDepForwards(N dep) { return addDep(dep,true); } + public <N extends Node> N addDep( N dep ) { + if( !addDepImpl(dep) ) return dep; + if( dep._inputs .find(this) != -1 ) return dep; // No need for deps on immediate neighbors + dep._deps.add(this); + return dep; + } - private <N extends Node> N addDep(N dep, boolean forwards) { - // Running peepholes during the big assert cannot have side effects - // like adding dependencies. - if( CODE._midAssert ) return dep; - var obs = CodeGen.CODE._midAssert ? null : CodeGen.CODE._obs; - if( obs != null ) obs.dep(this, dep); - if( dep._deps==null ) dep._deps = new Ary<>(Node.class); - if( dep._deps .find(this) != -1 ) return dep; // Already on list - if( !forwards && dep._inputs .find(this) != -1 ) return dep; // No need for deps on immediate neighbors - if( !forwards && dep._outputs.find(this) != -1 ) return dep; + public <N extends Node> N addDepForwards( N dep ) { + if( !addDepImpl(dep) ) return dep; dep._deps.add(this); return dep; } @@ -585,26 +594,6 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { /** Is this Node Memory related */ public boolean isMem() { return false; } - // Semantic change to the graph (so NOT a peephole), used by the Parser. - // If any input is a float, flip to a float-flavored opcode and widen any - // non-float input. - public final Node widen() { - if( !hasFloatInput() ) return this; - Node flt = copyF(); - if( flt==null ) return this; - for( int i=1; i<nIns(); i++ ) - flt.setDef(i, in(i)._type instanceof TypeFloat ? in(i) : new ToFloatNode(in(i)).peephole()); - kill(); - return flt; - } - private boolean hasFloatInput() { - for( int i=1; i<nIns(); i++ ) - if( in(i)._type instanceof TypeFloat ) - return true; - return false; - } - Node copyF() { return null; } - // ------------------------------------------------------------------------ // Peephole utilities @@ -649,6 +638,7 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { n._outputs = new Ary<>(Node.class); n._deps = null; n._hash = 0; + if( n instanceof CallNode call ) CODE._calls.add(call); return n; } @@ -656,7 +646,7 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { public Parser.ParseException err() { return null; } // Common integer constants - public static ConstantNode con(long x) { return (ConstantNode)(new ConstantNode(TypeInteger.constant(x)).peephole()); } + public static ConstantNode con(long x) { return (ConstantNode)ConstantNode.make(TypeInteger.constant(x)).peephole(); } // Utility to walk the entire graph applying a function; return the first // not-null result. @@ -677,6 +667,19 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { return null; } + + // If a Type upgrade happens, put on worklist + boolean _upgradeType( HashMap<String,Type> TYPES ) { return false; } + public final void upgradeType( HashMap<String,Type> TYPES ) { + Type old = _type; _type = _type.upgradeType(TYPES); + boolean progress = _upgradeType(TYPES); + if( progress || old != _type ) { + CODE.add(this); + CODE.addAll(_outputs); + if( _deps!=null ) CODE.addAll(_deps); + } + } + /** * Debugging utility to find a Node by index */ @@ -690,16 +693,4 @@ public abstract class Node extends BaseNode<Node> implements Cloneable { for( Node use : _outputs ) if( use != null && (x = use._find(nid,bs)) != null ) return x; return null; } - - boolean _upgradeType(java.util.HashMap<String,Type> types) { return false; } - public final void upgradeType(java.util.HashMap<String,Type> types) { - Type old = _type; - _type = _type.upgradeType(types); - boolean progress = _upgradeType(types); - if( progress || old!=_type ) { - CODE.add(this); - CODE.addAll(_outputs); - moveDepsToWorklist(); - } - } } diff --git a/src/main/java/com/seaofnodes/simple/node/NotNode.java b/src/main/java/com/seaofnodes/simple/node/NotNode.java index 775b069b..e22d16b6 100644 --- a/src/main/java/com/seaofnodes/simple/node/NotNode.java +++ b/src/main/java/com/seaofnodes/simple/node/NotNode.java @@ -7,7 +7,6 @@ import com.seaofnodes.simple.util.Utils; public class NotNode extends Node { public NotNode(Node in) { super(null, in); } - @Override public String label() { return "Not"; } @Override protected String format() { return "(!%1)"; } diff --git a/src/main/java/com/seaofnodes/simple/node/OrNode.java b/src/main/java/com/seaofnodes/simple/node/OrNode.java index 618c5340..263c3e87 100644 --- a/src/main/java/com/seaofnodes/simple/node/OrNode.java +++ b/src/main/java/com/seaofnodes/simple/node/OrNode.java @@ -7,6 +7,7 @@ import com.seaofnodes.simple.type.TypeInteger; public class OrNode extends ArithNode { public OrNode( Parser.Lexer loc, Node lhs, Node rhs) { super(loc,lhs, rhs, (byte)1); } + @Override public String label() { return "Or"; } @Override public String op() { return "|"; } @Override long doOp( long x, long y ) { return x | y; } diff --git a/src/main/java/com/seaofnodes/simple/node/ParmNode.java b/src/main/java/com/seaofnodes/simple/node/ParmNode.java index 37f049e5..316efb93 100644 --- a/src/main/java/com/seaofnodes/simple/node/ParmNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ParmNode.java @@ -2,31 +2,41 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; -import com.seaofnodes.simple.Parser; -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeInteger; -import com.seaofnodes.simple.type.TypeMemPtr; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; public class ParmNode extends PhiNode { // Argument indices are mapped one-to-one on CallNode inputs - public Type _declaredType; public final int _idx; // Argument index + public Type _declaredType; public ParmNode(String label, int idx, Type declaredType, Node... inputs) { - super(label,inputs); + super(label, inputs); _idx = idx; _declaredType = declaredType; + _type = declaredType; } - public ParmNode(ParmNode parm) { super(parm, parm._label, parm._declaredType ); _idx = parm._idx; _declaredType = parm._declaredType; } + public ParmNode(ParmNode parm) { super(parm, parm._label ); _idx = parm._idx; _declaredType = parm._declaredType; } + @Override public String label() { return MemOpNode.mlabel(_label); } + @Override boolean _upgradeType( HashMap<String,Type> TYPES) { + Type t = _declaredType.upgradeType(TYPES); + if( t == _declaredType ) return false; + unlock(); + _declaredType = t; + return true; + } + public FunNode fun() { return (FunNode)in(0); } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { - if( "main".equals(fun()._name) && _label.equals("arg") ) + if( fun()._name!=null && fun()._name.endsWith("<clinit>") && _label.equals("arg") ) return p.p("arg"); p.p("Parm_").p(_label).open(); for( Node in : _inputs ) p.n(in).p(","); @@ -39,42 +49,42 @@ public class ParmNode extends PhiNode { @Override public Type compute() { - Type t; - if( _idx==1 && region() instanceof FunNode fun ) { - // Even an unknown caller supplies Start's discovered escape set. - // Its heap contents are arbitrary, but its addresses are not. - t=Type.TOP; - for( int i=1; i<nIns(); i++ ) - if( !addDep(fun.in(i))._type.isHigh() ) t=t.meet(in(i)._type); - t=t.join(_declaredType); - } else t = inProgress() ? _declaredType : super.compute().join(_declaredType); + if( !(region() instanceof RegionNode r) ) + return region()._type==Type.XCONTROL || region()._type==Type.TOP ? (_type instanceof TypeMem ? TypeMem.TOP : Type.TOP) : _type; + // During parsing Phis have to be computed type pessimistically. + if( r.inProgress() || in(nIns()-1)==null ) + return _declaredType; + // Set type to local top of the starting type + Type t = Type.TOP; + for( int i = 1; i < nIns(); i++ ) { + // If the region's control input is live, add this as a dependency + // to the control because we can be peeped should it become dead. + Type ctrl = addDep(r.in(i))._type; + if( ctrl != Type.XCONTROL && ctrl != Type.TOP ) { + if( in(i)._type==Type.BOTTOM ) + return Type.BOTTOM; + t = t.meet(in(i)._type); + } + } // Recursion is a loop in disguise. A Parm merges the first argument // with those from recursive calls, just as a loop Phi merges its entry // and backedge. Use the widening rule in PhiNode.compute here too. // CallEndNode.compute handles the separate cycle through return values; // widening this Parm alone cannot stop f(x-1)+1 from growing its result. - if( t instanceof TypeInteger ti && t != _type && - !ti.isHigh() && !ti.isConstant() && - (!(_type instanceof TypeInteger oldi) || ti._widen <= oldi._widen) ) - return ti.same_but_slightly_wider_than(_declaredType); - return t; + if( t instanceof TypeInteger ti && !ti.isHigh() && !ti.isConstant() ) + t = ti.same_but_slightly_wider_than(); + return t.join(_declaredType); } @Override public Node idealize() { if( !(region() instanceof FunNode) ) - return in(1); // Input has collapse to e.g. starting control. + // A dead function can lose every caller before the Parm itself is + // removed; there is no replacement input in that transient form. + return nIns()==1 ? null : in(1); // Input has collapse to e.g. starting control. // If function is folding, do all possible peeps if( fun()._folding ) return super.idealize(); - // Can upgrade minType even while in-progress - if( _declaredType instanceof TypeMemPtr tmp && _declaredType.isFRef() ) { - TypeMemPtr tmp2 = (TypeMemPtr) Parser.TYPES.get(tmp._obj._name); - if( tmp2!=null && tmp2 != _declaredType ) { - _declaredType = tmp2; - return this; - } - } // Skip most phi optimizations on parms return null; } @@ -82,12 +92,4 @@ public class ParmNode extends PhiNode { @Override public boolean eq( Node n ) { return ((ParmNode)n)._idx==_idx && super.eq(n); } - - @Override boolean _upgradeType(java.util.HashMap<String,Type> types) { - Type next = _declaredType.upgradeType(types); - if( next==_declaredType ) return false; - unlock(); - _declaredType=next; - return true; - } } diff --git a/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/src/main/java/com/seaofnodes/simple/node/PhiNode.java index 064852e4..61e4b468 100644 --- a/src/main/java/com/seaofnodes/simple/node/PhiNode.java +++ b/src/main/java/com/seaofnodes/simple/node/PhiNode.java @@ -3,24 +3,30 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; import com.seaofnodes.simple.*; -import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.*; -import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.util.*; +import java.util.HashMap; +import java.util.IdentityHashMap; public class PhiNode extends Node { public final String _label; - int lattice_drop; - - public PhiNode(String label, Node... inputs) { + public PhiNode( String label, Node... inputs) { super(inputs); _label = label; } + public static PhiNode make(String label, Type minType, Node... inputs) { + return minType instanceof TypeMem mem + ? mem._alias==1 + ? new BulkMemPhiNode(label,inputs) + : new MemPhiNode(label,mem._alias,inputs) + : new PhiNode(label, inputs); + } // Used by ParmNode - public PhiNode(PhiNode phi, String label, Type type) { super(phi); _label = label; _type = type; } + public PhiNode(PhiNode phi, String label) { super(phi); _label = label; } // Used by instruction Selection - public PhiNode(PhiNode phi) { this(phi,phi._label,phi._type ); } + public PhiNode(PhiNode phi) { this(phi,phi._label); } // Used by the infinite-loop exit breaker public PhiNode(RegionNode r, Node sample) { super(new Node[]{r}); @@ -30,9 +36,6 @@ public class PhiNode extends Node { addDef(sample); } - public static PhiNode make(String label, Type type, Node... inputs) { - return type instanceof TypeMem ? new BulkMemPhiNode(label,inputs) : new PhiNode(label,inputs); - } @Override public String label() { return "Phi_"+MemOpNode.mlabel(_label); } @@ -47,20 +50,17 @@ public class PhiNode extends Node { public CFGNode region() { return (CFGNode)in(0); } @Override public boolean isMem() { return _type instanceof TypeMem; } - boolean isRPC() { return false; } @Override public Type compute() { if( !(region() instanceof RegionNode r) ) - return region()._type==Type.XCONTROL || region()._type==Type.TOP ? (_type instanceof TypeMem ? TypeMem.TOP : Type.TOP) : _type; + return Type.TOP; // During parsing Phis have to be computed type pessimistically. if( r.inProgress() || in(nIns()-1)==null ) - // The backedge has not been parsed. Declarations belong on - // Convert/Parm nodes; an unfinished value Phi knows no bound. return Type.BOTTOM; // Set type to local top of the starting type Type t = Type.TOP; - for (int i = 1; i < nIns(); i++) { + for( int i = 1; i < nIns(); i++ ) { // If the region's control input is live, add this as a dependency // to the control because we can be peeped should it become dead. Type ctrl = addDep(r.in(i))._type; @@ -70,7 +70,6 @@ public class PhiNode extends Node { t = t.meet(in(i)._type); } } - Type newt = t; // Widen loop Phis so integer ranges cannot grow one step per trip forever. // Recursion is a loop in disguise: ParmNode.compute widens arguments, @@ -78,98 +77,75 @@ public class PhiNode extends Node { // recursive result can feed itself without passing through a Parm. // Above-center ranges may still fall to unwidened constants. // Widen only below-center ranges to preserve monotonicity. - if( region() instanceof LoopNode && // Only around loops - newt instanceof TypeInteger newi && - // Types changed and are falling (the optimistic case, expected to fall forever) - newi != _type ) { - if( !newi.isHigh() && !newi.isConstant() && (!(_type instanceof TypeInteger oldi) || newi._widen <= oldi._widen) ) - return newi.same_but_slightly_wider_than(TypeInteger.BOT); + if( r instanceof LoopNode && // Only around loops + t instanceof TypeInteger ti && // Only widen integers + !ti.isHigh() && !ti.isConstant() ) { // Only widen live ranges + // Widen, to prevent infinite falling of TypeIntegers + return ti.same_but_slightly_wider_than(); } - return newt; + return t; } @Override public Node idealize() { - if( !(region() instanceof RegionNode r ) ) - return in(1); // Input has collapse to e.g. starting control. - if( r.inProgress() || inProgress() || r.nIns()<=1 ) + RegionNode r = (RegionNode)region(); + if( r.inProgress() || nOuts()==0 ) return null; // Input is in-progress // If we have only a single unique input, become it. Node live = singleUniqueInput(); - if( live != null ) return live; + if( live != null ) + return live; // No bother if region is going to fold dead paths soon for( int i=1; i<nIns(); i++ ) if( r.in(i)._type == Type.XCONTROL ) return null; - // Phi(op(a,b,...),op(c,d,...)) -> op(Phi(a,c),Phi(b,d),...). - Node down; - if( same_op() && (down=drop_same_op())!=null ) return down; + // Generic "pull down op" + Node progress; + if( same_op() && (progress = drop_same_op()) != null ) + return progress; - if( nIns()==3 ) { - Node merge = matchingGuardMerge(r,1); - if( merge!=null ) return merge; - merge = matchingGuardMerge(r,2); - if( merge!=null ) return merge; - } - - // If merging Phi(N, cast(N)) - we are losing the cast JOIN effects, so just remove. - if( nIns()==3 ) { - if( in(1) instanceof CastNode cast && addDep(cast.in(1))==in(2) ) return in(2); - if( in(2) instanceof CastNode cast && addDep(cast.in(1))==in(1) ) return in(1); - } - // If merging a null-checked null and the checked value, just use the value. - // if( val ) ..; phi(Region,False=0/null,True=val); - // then replace with plain val. - if( nIns()==3 ) { - int nullx = -1; - if( in(1)._type == in(1)._type.makeZero() ) nullx = 1; - if( in(2)._type == in(2)._type.makeZero() ) nullx = 2; - if( nullx != -1 ) { - Node val = in(3-nullx); - if( val instanceof CastNode cast ) - val = cast.in(1); - Node ridom = r.idom(this); - if( ridom instanceof IfNode iff && addDep(iff.pred())==val ) { - // Must walk the idom on the null side to make sure we hit False. - CFGNode idom = (CFGNode)r.in(nullx); - while( idom != null && idom.nIns() > 0 && idom.in(0) != iff ) idom = idom.idom(); - if( idom instanceof CProjNode proj && proj._idx==1 ) - return val; - } else if( ridom != null ) addDep(ridom); - } - } + // If merging Phi(ZERO, guardNZ(N)) at the matching `if(N)` join, the + // Phi is exactly N. The guard arm proves N is non-zero, and the other + // arm contributes N's zero value. This is only legal if N is already + // available at the Phi region; otherwise a return-scope/live-on-exit + // Phi can lose the zero arm and export a branch-local value upward. + Node unguard; + if( (unguard=matchingGuardMerge(r,1)) != null ) return unguard; + if( (unguard=matchingGuardMerge(r,2)) != null ) return unguard; return null; } private Node matchingGuardMerge(RegionNode r, int nzIdx) { Node zero = in(3-nzIdx); - if( !(in(nzIdx) instanceof GuardNode guard && guard._nonZero) || - zero==null || zero._type.makeZero()!=zero._type || guard.in(1)==this ) + if( !(in(nzIdx) instanceof GuardNode cast && cast._nonZero) || + zero._type.makeZero()!=zero._type || + cast.in(1)==this ) return null; if( !(r.in(nzIdx) instanceof CProjNode nz && nz._idx==0 && nz.ctrl() instanceof IfNode iff) ) return null; if( !(r.in(3-nzIdx) instanceof CProjNode z && z._idx==1 && z.ctrl()==iff) ) return null; - Node n=guard.in(1), tested=null; + // Predicate was guarded? + Node n = cast.in(1), tested = null; while( true ) { - if( iff.pred()==n ) tested=n; - if( !(n instanceof GuardNode g && g._nonZero) ) break; - n=g.in(1); + if( iff.pred()==n ) tested = n; + if( !(n instanceof GuardNode guard && guard._nonZero) ) break; + n = guard.in(1); } - // The source must exist before the join, not just on its guarded arm. - return tested!=null && ScopeNode.availableAt(n,r,new java.util.BitSet()) ? n : null; + CFGNode early = CFGNode.earlyCFG(n); + return tested!=null && (early==null || early.dominates(r,this)) ? n : null; } // Same op on all Phi paths; all ops have only the Phi as a use. // None have a control input. private boolean same_op() { Node op = in(1); - if( op instanceof CFGNode || op instanceof ConstantNode || op instanceof PhiNode || + if( op instanceof EscapeNode || op instanceof CFGNode || op instanceof ConstantNode || op instanceof PhiNode || op instanceof ProjNode || op instanceof NewNode || op instanceof ScopeNode || op instanceof MemMergeNode ) return false; // A bulk Phi must split its aliases before factoring precise Stores. if( this instanceof BulkMemPhiNode ) return false; @@ -221,7 +197,7 @@ public class PhiNode extends Node { ins[i] = in(i).in(j); t = t.meet(ins[i]._type); } - // Not accepts both pointers and integers, but a Phi cannot mix them. + // Do not introduce an operand Phi mixing incompatible type families. if( t==Type.BOTTOM ) { for( int i=1; i<ins.length; i++ ) addDep(ins[i]); cp.kill(); @@ -229,7 +205,7 @@ public class PhiNode extends Node { } PhiNode phi = j==1 && op instanceof MemOpNode mem ? new MemPhiNode(_label,mem._alias,ins) - : PhiNode.make(_label,t.glb(false),ins); + : PhiNode.make(_label,t,ins); x = phi.peephole(); } cp.addDef(x); @@ -276,31 +252,24 @@ public class PhiNode extends Node { } // Never equal if inProgress. - // Also, joins - @Override public boolean eq(Node n) { return !inProgress(); } + @Override public boolean eq( Node n ) { + return !inProgress() && super.eq(n); + } @Override public Parser.ParseException err() { - if( inProgress() ) return null; - // A scalar union is useful for truth tests, e.g. !(ptr || integer). - // A typed consumer reports an invalid use of that union itself. if( _type != Type.BOTTOM ) return null; - // BOTTOM means we mixed e.g. int and ptr + // Global BOTTOM retains the same role for non-scalar families. for( int i=1; i<nIns(); i++ ) // Already an error, but better error messages come from elsewhere if( in(i)._type == Type.BOTTOM ) return null; - // Gather a minimal set of types that "cover" all the rest - boolean ti=false, tf=false, tp=false, tn=false; - for( int i=1; i<nIns(); i++ ) { - Type t = in(i)._type; - ti |= t instanceof TypeInteger x; - tf |= t instanceof TypeFloat x; - tp |= t instanceof TypeMemPtr x; - tn |= t==Type.NIL; - } - return ReturnNode.mixerr(ti,tf,tp,tn, ((RegionNode)region())._loc); + SB sb = new SB().p("No common type amongst "); + for( int i=1; i<nIns(); i++ ) + sb.p(in(i)._type.toString()).p(" and "); + return Parser.error(sb.unchar(5).toString(),null); } + } diff --git a/src/main/java/com/seaofnodes/simple/node/Proj.java b/src/main/java/com/seaofnodes/simple/node/Proj.java new file mode 100644 index 00000000..c8b9378f --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/Proj.java @@ -0,0 +1,2 @@ +package com.seaofnodes.simple.node; +public interface Proj { int idx(); } diff --git a/src/main/java/com/seaofnodes/simple/node/ProjNode.java b/src/main/java/com/seaofnodes/simple/node/ProjNode.java index 92c6c2be..a0142379 100644 --- a/src/main/java/com/seaofnodes/simple/node/ProjNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ProjNode.java @@ -5,9 +5,10 @@ import com.seaofnodes.print.ExprPrinter; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeMem; import com.seaofnodes.simple.type.TypeTuple; +import java.util.HashMap; +import java.util.IdentityHashMap; - -public class ProjNode extends Node { +public class ProjNode extends Node implements Proj { // Which slice of the incoming multipart value public final int _idx; @@ -22,6 +23,7 @@ public class ProjNode extends Node { } public ProjNode(ProjNode p) { super(p); _idx = p._idx; _label = p._label; } + @Override public String label() { return _label; } @Override @@ -44,11 +46,14 @@ public class ProjNode extends Node { return t instanceof TypeTuple tt ? tt._types[_idx] : t.oob(); } - @Override public Node idealize() { return ((MultiNode)in(0)).pcopy(_idx); } + @Override public Node idealize() { return ((MultiNode)in(0)).pcopy(_idx,this); } @Override public boolean eq( Node n ) { return _idx == ((ProjNode)n)._idx; } @Override int hash() { return _idx; } + + @Override public int idx() { return _idx; } + } diff --git a/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java b/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java index 55c0fc02..094734fa 100644 --- a/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java +++ b/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java @@ -18,6 +18,7 @@ public class PtrToIntNode extends Node { public PtrToIntNode(Node ptr) { super(null,ptr); } public PtrToIntNode(PtrToIntNode ptr) { super(ptr); } + @Override protected String format() { return "(i64*)%1"; } @Override public Type compute() { diff --git a/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java b/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java index 94c1b6f6..ab7e0ae0 100644 --- a/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ReadOnlyNode.java @@ -10,7 +10,7 @@ import com.seaofnodes.simple.type.TypeMemPtr; public class ReadOnlyNode extends Node { public ReadOnlyNode( Node n ) { super(null,n); } public ReadOnlyNode( ReadOnlyNode n ) { super(n); } - @Override public String label() { return "ReadOnly"; } + @Override protected String format() { return "(const)%1"; } @Override public Type compute() { diff --git a/src/main/java/com/seaofnodes/simple/node/RegionNode.java b/src/main/java/com/seaofnodes/simple/node/RegionNode.java index 59030dee..923773c4 100644 --- a/src/main/java/com/seaofnodes/simple/node/RegionNode.java +++ b/src/main/java/com/seaofnodes/simple/node/RegionNode.java @@ -2,7 +2,6 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; -import com.seaofnodes.simple.*; import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.Type; @@ -16,8 +15,6 @@ public class RegionNode extends CFGNode { public RegionNode(RegionNode r, Parser.Lexer loc) { super(r); _loc = loc; } public RegionNode(RegionNode r) { super(r); if( r!=null ) _loc = r._loc; } - @Override - public String label() { return "Region"; } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p(label()); @@ -47,10 +44,10 @@ public class RegionNode extends CFGNode { // If a CFG diamond with no merging, delete: "if( pred ) {} else {};" if( !hasPhi() && nIns()>3 && // No Phi users, just a control user - in(1) instanceof CProjNode p1 && - in(2) instanceof CProjNode p2 && - addDepForwards(p1).in(0)==addDepForwards(p2).in(0) && - p1.in(0) instanceof IfNode iff ) { + in(1) instanceof CProjNode p1 && + in(2) instanceof CProjNode p2 && + addDepForwards(p1).in(0)==addDepForwards(p2).in(0) && + p1.in(0) instanceof IfNode iff ) { // Replace with the iff.ctrl directly if( nIns()==3 ) return iff.ctrl(); // Just delete the path for fat Regions @@ -76,7 +73,7 @@ public class RegionNode extends CFGNode { // Fold Region for( int j=1; j<region.nIns(); j++ ) addDef(region.in(j)); - setDef(i,Parser.XCTRL); + setDef(i,CodeGen.CODE.XCTRL); return this; } } @@ -112,7 +109,8 @@ public class RegionNode extends CFGNode { CodeGen.CODE.addAll(phi.delDef(path)._outputs); } } - return isDead() ? Parser.XCTRL : delDef(path); + if( !isDead() ) delDef(path); + return isDead() ? CodeGen.CODE.XCTRL : this; } private int findDeadInput(int startIdx) { diff --git a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java index ecfd2cde..0bb090ee 100644 --- a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java @@ -10,7 +10,15 @@ import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; -/** Keeps all preceding memory effects and the return value alive: {ctrl, $mem, value, rpc}. */ +/** + * The Return node has two inputs. The first input is a control node and the + * second is the data node that supplies the return value. + * <p> + * In this presentation, Return functions as a Stop node, since multiple <code>return</code> statements are not possible. + * The Stop node will be introduced in Chapter 6 when we implement <code>if</code> statements. + * <p> + * The Return's output is the value from the data node. + */ public class ReturnNode extends CFGNode { public FunNode _fun; @@ -21,6 +29,7 @@ public class ReturnNode extends CFGNode { } public ReturnNode( ReturnNode ret, FunNode fun ) { super(ret); _fun = fun; } + public Node ctrl() { return in(0); } public Node mem () { return in(1); } public Node expr() { return in(2); } @@ -28,9 +37,6 @@ public class ReturnNode extends CFGNode { @Override public FunNode fun() { return _fun; } @Override - public String label() { return "Return"; } - - @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { p.p("return "); if( expr()==null ) p.p("----"); @@ -43,123 +49,59 @@ public class ReturnNode extends CFGNode { @Override public Type compute() { - if( inProgress () ) return TypeTuple.RET; // In progress - // Inlining can delete the entry while callers still use this return. return TypeTuple.make(ctrl()._type,mem()._type,expr()._type); } @Override public Node idealize() { - if( inProgress () ) return null; if( _fun.isDead() ) return null; // Upgrade signature based on return type Type ret = expr()._type; TypeFunPtr fcn = _fun.sig(); - // Private field contents flow on the linked Return/CallEnd edges. - // Keep the helper's interface at MemBot; a current memory snapshot - // is not a new declaration for its self type or its methods. - if( !(ret instanceof TypeMem) && ret != fcn.ret() && ret.isa(fcn.ret()) ) + if( ret != fcn.ret() && ret.isa(fcn.ret()) ) _fun.setSig(fcn.makeFrom(ret)); - // If dead (cannot be reached; infinite loop), kill the exit values - if( ctrl()._type==Type.XCONTROL && - !(mem() instanceof ConstantNode && expr() instanceof ConstantNode) ) { - Node top = new ConstantNode(Type.TOP).peephole(); - setDef(1,top); - setDef(2,top); - return this; - } - return null; } - public boolean inProgress() { - return ctrl().getClass() == RegionNode.class && ((RegionNode)ctrl()).inProgress(); - } - - // Remember parsed type kinds for diagnostics; the optimized expression decides validity. - private boolean ti=false, tf=false, tp=false, tn=false; - - // Add a return exit to the current parsing function - void addReturn( Node ctrl, Node rmem, Node expr ) { - assert inProgress(); - - // Gather parse-time return types for error reporting - Type t = expr._type; - ti |= t instanceof TypeInteger x; - tf |= t instanceof TypeFloat x; - tp |= t instanceof TypeMemPtr x; - tn |= t==Type.NIL; - - // Merge path into the One True Return - RegionNode r = (RegionNode)ctrl(); - // Assert that the Phis are in particular outputs; not reordered or shuffled - PhiNode mem = (PhiNode)r.out(0); assert mem.isMem(); - PhiNode rez = (PhiNode)r.out(1); assert !rez.isMem() || fun().sig().ret() instanceof TypeMem; - // Pop "inProgress" null off - r ._inputs.pop(); - mem._inputs.pop(); - rez._inputs.pop(); - // Add new return point - r .addDef(ctrl); - mem.addDef(rmem); - rez.addDef(expr); - // Back to being inProgress - r .addDef(null); - mem.addDef(null); - rez.addDef(null); - } - @Override public Parser.ParseException err() { - if( expr()._type instanceof TypeMem ) return null; // Hidden initializer result. - if( ctrl()._type != Type.CONTROL ) return null; // Exit path is dead - if( expr()._type==TypeScalar.BOT ) { - int kinds = returnKinds(expr(),new java.util.BitSet()); - return mixerr((kinds&1)!=0,(kinds&2)!=0,(kinds&4)!=0,(kinds&8)!=0,_fun._loc); + if( ctrl()._type == Type.CONTROL && + expr()._type == Type.TOP ) + return Parser.error("No defined return type",null); + if( ctrl()._type == Type.CONTROL && + expr()._type == TypeScalar.BOT ) + return Parser.error("Cannot return generic scalar",null); + // With no user constructor, <init> doubles as the public no-arg + // constructor. Required fields remain TOP in its private memory: a + // typed parser poison which deliberately emitted no Store or code. + if( ctrl()._type == Type.CONTROL && _fun.isInstance() ) { + TypeStruct self = ((TypeMemPtr)_fun.sig().arg(0))._obj; + if( !CodeGen.hasUserConstructor(self) && expr()._type instanceof TypeMem mem ) + for( Field fld : self._fields ) { + if( !(fld._t instanceof TypeNil tn && tn.notNull()) ) continue; + Type actual = mem._alias==1 && mem._t instanceof TypeStruct ts + ? ts.field(fld._fname)._t + : mem._alias==fld._alias ? mem._t : Type.TOP; + if( actual==Type.TOP || actual instanceof TypeNil atn && atn.nullable() ) + return Parser.error("'"+self._name+"' is not fully initialized, field '"+ + fld._fname+"' is only partially set in the constructor",null); + } } - return expr()._type==Type.BOTTOM || expr()._type==Type.TOP || expr()._type==TypeScalar.BOT ? mixerr(ti,tf,tp,tn,_fun._loc) : null; - } - - private static int returnKinds(Node n, java.util.BitSet visit) { - if( n==null || visit.get(n._nid) ) return 0; - visit.set(n._nid); - Type t=n._type; - if( t instanceof TypeInteger ) return 1; - if( t instanceof TypeFloat ) return 2; - if( t instanceof TypeMemPtr || t instanceof TypeFunPtr ) return 4; - if( t==Type.NIL ) return 8; - int kinds=0; - if( n instanceof PhiNode phi ) - for( int i=1; i<n.nIns(); i++ ) - if( phi.region().in(i)._type==Type.CONTROL ) kinds |= returnKinds(n.in(i),visit); - return kinds; + return null; } - static Parser.ParseException mixerr( boolean ti, boolean tf, boolean tp, boolean tn, Parser.Lexer loc ) { - if( !ti && !tf && !tp && !tn ) - // Hit when the function never returns. e.g. `f = { -> return f() };` - return Parser.error("No defined return type", loc); - // Rather ugly way to print conflicting return types - int cnt = (ti?1:0) + (tf?1:0) + (tp||tn?1:0); - if( cnt==1 ) return null; // Uniform return type, something else errored and will report - SB sb = new SB().p("No common type amongst "); - if( ti ) sb.p("int and "); - if( tf ) sb.p("f64 and "); - if( tp || tn ) sb.p("reference and "); - return Parser.error(sb.unchar(5).toString(),loc); + @Override public boolean eq( Node n ) { + return !_fun.isDead() && !((ReturnNode)n)._fun.isDead(); } - // ------------ // MachNode specifics, shared across all CPUs public String op() { - return _fun._frameAdjust > 0 ? "addi" : "ret"; + return _fun._frameAdjust > 0 ? "epilog" : "ret "; } // Correct Nodes outside the normal edges public void postSelect(CodeGen code) { - FunNode fun = (FunNode)rpc().in(0); - _fun = fun; - fun.setRet(this); + _fun.setRet(this); } public RegMask regmap(int i) { return i==2 @@ -171,13 +113,14 @@ public class ReturnNode extends CFGNode { public void asm(CodeGen code, SB sb) { int frameAdjust = fun()._frameAdjust; if( frameAdjust>0 ) - sb.p("rsp += #").p(frameAdjust).p("\nret"); + sb.p("\n").p("rsp += #").p(frameAdjust).p("\nret "); // Post code-gen, just print the "ret" - if( code._phase.ordinal() <= CodeGen.Phase.RegAlloc.ordinal() ) + if( code._phase.ordinal() < CodeGen.Phase.RegAlloc.ordinal() || + (code._phase.ordinal() == CodeGen.Phase.RegAlloc.ordinal() && !code._regAlloc.done()) ) // Prints return reg (either RAX or XMM0), RPC and then the // callee-save registers. for( int i=2; i<nIns(); i++ ) - sb.p(code.reg(in(i),fun())).p(" "); + sb.p(code.reg(in(i))).p(" "); // If we did not get the expected rpc, print which one we got else if( code._regAlloc.regnum(rpc()) != code._mach.rpc() ) sb.p("[").p(code.reg(rpc())).p("]"); diff --git a/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java b/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java index 10a0e4aa..02d3bbaa 100644 --- a/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java +++ b/src/main/java/com/seaofnodes/simple/node/RoundF32Node.java @@ -8,7 +8,6 @@ import com.seaofnodes.simple.type.TypeFloat; public class RoundF32Node extends Node { public RoundF32Node(Node lhs) { super(null, lhs); } - @Override public String label() { return "RoundF32"; } @Override protected String format() { return "((f32)%1)"; } diff --git a/src/main/java/com/seaofnodes/simple/node/SarNode.java b/src/main/java/com/seaofnodes/simple/node/SarNode.java index 54611b0d..fa63334b 100644 --- a/src/main/java/com/seaofnodes/simple/node/SarNode.java +++ b/src/main/java/com/seaofnodes/simple/node/SarNode.java @@ -7,6 +7,7 @@ import com.seaofnodes.simple.type.TypeInteger; public class SarNode extends ArithNode { public SarNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + @Override public String op() { return ">>"; } @Override long doOp( long x, long y ) { return x >> y; } @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { diff --git a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java index f65a822d..694d5dcb 100644 --- a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -1,22 +1,21 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; - import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.Var; import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.CompUnit; +import com.seaofnodes.simple.codegen.ParseAll; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.Ary; -import com.seaofnodes.simple.util.AryInt; import com.seaofnodes.simple.util.Utils; - import java.util.*; /** * The Scope node is purely a parser helper - it tracks names to nodes with a - * stack of variable records. + * stack of variables. */ -public class ScopeNode extends Node { +public class ScopeNode extends MemMergeNode { /** * The control is a name that binds to the currently active control @@ -26,43 +25,43 @@ public class ScopeNode extends Node { public static final String ARG0 = "arg"; public static final String MEM0 = "$mem"; - public Node in(Var v) { return in(v._idx); } - - @Override public Type compute() { return Type.BOTTOM; } - @Override public Node idealize() { return null; } - - // All active/live variables in all nested scopes, all run together + // All active/live variables in all nested scopes, all run together. + // Maps 1-to-1 to the _inputs array. public final Ary<Var> _vars; // Lexical scope is typed: public static class Kind { // Basic block scoping - public static class Block extends Kind { } + public static class Block extends Kind { } + // Constructor/initializer block for an allocated struct. + public static class Constructor extends Block { + public Constructor(TypeStruct self) { _self = self; } + public final TypeStruct _self; + } // Function scope - public static class Func extends Kind { + public static class Func extends Kind { + public final String _name; public final boolean _constructor; - public Func(boolean constructor) { _constructor=constructor; } + public Func(String name) { this(name,false); } + public Func(String name, boolean constructor) { _name=name; _constructor=constructor; } } - // Struct definition, mapping types to fields - public static class Define extends Kind { public Define(TypeMemPtr tmp) { _tmp=tmp; } public final TypeMemPtr _tmp; } - // Struct allocation, mapping values to fields - public static class Alloc extends Kind { public Alloc (TypeMemPtr tmp) { _tmp=tmp; } public final TypeMemPtr _tmp; } public int _lexSize; // Number of Vars in this scope } public final Ary<Kind> _kinds; // Lexical scope nesting depth public int depth() { return _kinds._len; } + public Kind klast() { return _kinds.last(); } - // Extra guards; tested predicates and casted results + // Extra guards; tested predicates and cast results private final Ary<Node> _guards; // A new ScopeNode public ScopeNode() { - _type = Type.BOTTOM; - _vars = new Ary<>(Var .class); - _kinds = new Ary<>(Kind .class); - _guards = new Ary<>(Node .class); + super(); + _vars = new Ary<>(Var .class); + _kinds = new Ary<>(Kind.class); + _guards = new Ary<>(Node.class); } @Override public String label() { return "Scope"; } @@ -73,7 +72,7 @@ public class ScopeNode extends Node { int j=1; for( int i=0; i<nIns(); i++ ) { if( j < depth() && i == _kinds.at(j)._lexSize ) { p.p("| "); j++; } - Var v = _vars.at(i); + Var v = var(i); p.p(v._type()); p.p(" "); if( v._final ) p.p("!"); @@ -92,7 +91,7 @@ public class ScopeNode extends Node { public Node ctrl() { return in(0); } - public Node mem() { return in(lookup(MEM0)); } + public Node mem() { return in(1); } public Var var(int i) { return _vars.at(i); } /** @@ -106,7 +105,7 @@ public class ScopeNode extends Node { * @return Node that was bound */ public <N extends Node> N ctrl(N n) { return setDef(0,n); } - public Node mem(Node n) { update(MEM0,n); return n; } + public Node mem(Node n) { return setDef(1,n); } public void push( Kind kind ) { kind._lexSize = _vars.size(); @@ -115,7 +114,11 @@ public class ScopeNode extends Node { // Pop a lexical scope public void pop() { - promote(); // Promote forward references to the next outer scope + promote(null); // Promote forward references to the next outer scope + _pop(); + } + // Pop a scope, no promote FRefs + public void _pop() { int n = _kinds.pop()._lexSize; popUntil(n); // Pop off inputs going out of scope _vars.setLen(n); // Pop off variables going out of scope @@ -124,78 +127,97 @@ public class ScopeNode extends Node { // Look for forward references in the last lexical scope and promote to the // next outer lexical scope. At the last scope declare them an error. - public void promote() { - Kind kind = _kinds.last(); + public void promote(CodeGen code) { + Kind kind = klast(); int n = kind._lexSize; for( int i=n; i<nIns(); i++ ) { Var v = var(i); + Node vn = in(i); if( !v.isFRef() ) continue; - if( depth()==1 ) - throw Parser.error("Undefined name '" + v._name + "'",v._loc); - _vars .swap(n,i); - _inputs.swap(n,i); - v._idx = n; + // Shuffle vars and inputs to move the forward reference to + // *before* the start of the using scope ("as if" it was not + // actually a forward reference). + for( int j=n; j<=i; j++ ) { + Var vtmp = var(j); + Node ntmp = in (j); + v._idx = j; + _vars .set(j,v ); + _inputs.set(j,vn); + v = vtmp; + vn = ntmp; + } kind._lexSize = ++n; } } - // Fields are writable local values until construction ends, even when the - // published field is final. Ordinary methods and final local variables do - // not inherit that permission. This also decides which loop Phis we need. - public boolean canInit(Var v) { - for( int i=_kinds._len-1; i>=0; i-- ) { - Kind k=_kinds.at(i); - if( k instanceof Kind.Alloc ) - return v._idx >= _kinds.at(i-1)._lexSize && v._idx < _kinds.at(i)._lexSize; - if( k instanceof Kind.Define ) return kind(v)==k; - if( k instanceof Kind.Func fun ) - return fun._constructor && i>0 && _kinds.at(i-1) instanceof Kind.Define def && kind(v)==def; - } - return false; + public boolean inFunction () { return klast() instanceof Kind.Func; } + // Is any enclosing scope a constructor? + public boolean inConstructor() { return constructorSelf() != null; } + // Nearest constructor self type, if any. + public TypeStruct constructorSelf() { + for( int i=depth()-1; i>=0; i-- ) + if( _kinds.at(i) instanceof Kind.Constructor ctor ) + return ctor._self; + else if( _kinds.at(i) instanceof Kind.Func func && FunNode.isInstance(func._name) ) + return ((TypeMemPtr)var(_kinds.at(i)._lexSize).type())._obj; + return null; } - private boolean loopVar(Var v) { - return !v._final || (canInit(v) && !v.type().isHighOrConst()); + // Find nearest enclosing function scope + public int enclosingFunction() { + for( int i=depth()-1; i>=0; i-- ) + if( _kinds.at(i) instanceof Kind.Func func && !FunNode.isInstance(func._name) ) + return i; + throw Utils.TODO("Should not reach here"); } - public boolean inConstructor() { return _kinds.last() instanceof Kind.Define; } - public boolean inAllocation () { return _kinds.last() instanceof Kind.Alloc ; } - public boolean inFunction () { return _kinds.last() instanceof Kind.Func ; } + // Find nearest enclosing function or declaration scope + public int enclosingFuncOrDecl() { + for( int i=depth()-1; i>=0; i-- ) + if( _kinds.at(i) instanceof Kind.Func func ) + return i; + throw Utils.TODO("Should not reach here"); + } - public Kind kind( Var v ) { + // Which lexical scope contains this Var? + public int kindx( Var v ) { for( int i=depth()-1; i>=0; i-- ) if( v._idx >= _kinds.at(i)._lexSize ) - return _kinds.at(i); - throw Utils.TODO(); + return i; + return -1; } - // Return the kind for the defining scope, or a function scope if found - // first. Block scopes means the variable can be r/w directly in the - // scope, Function scopes require final constants (no capture), and - // Constructor scopes mean an instance variable reference. - - // Walk up-lexical scope looking for defining lexical Kind. - // - Skip any amount of nested block scopes - // - Walking out of a function requires v be a final constant (for now) - - // and we can stop walking. + // Which lexical scope contains this Var? - // - Walking out of a method and (skipping Blocks) into the matching struct - // is OK, and this is a instance var load. - // - Walking out of a method and into the wrong struct is an error; - // including nested structs containing (no nested classes yet) + public int kindFcnx( Var v ) { + for( int i=depth()-1; i>=0; i-- ) + if( v._idx >= _kinds.at(i)._lexSize && _kinds.at(i) instanceof Kind.Func ) + return i; + throw Utils.TODO("Should not reach here"); + } - // Returns error, forward-ref, ok containing struct, ok containing function/block. + // Which function scope contains this Var? + public Kind kindFcn( Var v ) { + int kx = kindFcnx(v); + return kx == -1 ? null : _kinds.at(kx); + } - public String outOfFunction( Var v ) { - //if( v==null ) return null; // Prolly forward reference - //int i; for( i=_lexSize._len-1; i>=0 && v._idx<_lexSize.at(i); i-- ) - // if( _kinds.at(i)=="{->}" ) return "{->}"; - //return _kinds.at(i); - throw Utils.TODO(); + // Final fields are writable local values inside their own initializer or + // constructor. Final parameters, local variables, and captured bindings + // remain final. Use the same rule when deciding which loop Phis to build. + public boolean canInit(Var v) { + int fx=enclosingFuncOrDecl(); + Kind.Func fun=(Kind.Func)_kinds.at(fx); + if( !fun._constructor || v._extern!=0 ) return false; + int dx=FunNode.isInstance(fun._name) ? fx : fx-1; + return dx>=0 && kindx(v)==dx && v._idx>=_kinds.at(dx)._lexSize+2; } + private boolean loopVar(Var v) { + return !v._final || (canInit(v) && !v.type().isHighOrConst()); + } // Find name in reverse, return an index into _vars or -1. Linear scan // instead of hashtable, but probably doesn't matter until the scan @@ -211,9 +233,9 @@ public class ScopeNode extends Node { * Create a new variable name in the current scope */ public boolean define( String name, Type declaredType, boolean xfinal, Node init, Parser.Lexer loc ) { - assert _kinds.isEmpty() || name.charAt(0)!='$' ; // Later scopes do not define memory + assert _kinds.isEmpty() || name!=MEM0 ; // Later scopes do not define memory if( depth() > 0 ) - for( int i=_vars.size()-1; i>=_kinds.last()._lexSize; i-- ) { + for( int i=_vars.size()-1; i>=klast()._lexSize; i-- ) { Var n = var(i); if( n._name.equals(name) ) { if( !n.isFRef() ) return false; // Double define @@ -221,17 +243,45 @@ public class ScopeNode extends Node { if( !xfinal || !declaredType.isConstant() ) throw fref.err(); // Must be a final constant n.defFRef(declaredType,xfinal,loc); // Declare full correct type, final, source location setDef(n._idx,fref.addDef(init)); // Set FRef to defined; tell parser also + return true; } } - Var v = new Var(nIns(),name,declaredType,xfinal,loc,init==Parser.XCTRL); + Var v = new Var(nIns(),name,declaredType,xfinal,loc,init==CodeGen.CODE.XCTRL); _vars.add(v); // Creating a forward reference - if( init==Parser.XCTRL ) - init = new FRefNode(v).init(); + if( init==CodeGen.CODE.XCTRL ) + //init = new FRefNode(v).init(); + throw Utils.TODO(); addDef(init); return true; } + // Normal lookup was tried and failed. Append a forward ref in the current + // lexical scope. Scope pops promote it outwards "as if" the final constant + // had been declared before the using scope. + + public Var defineFRef( String id, Type t, boolean xfinal, Parser.Lexer loc ) { + Var var = new Var(nIns(),id,t,xfinal,loc,true); + FRefNode fref = new FRefNode(id,loc).init(); + fref._type = fref._con = t; + _vars.add(var); + addDef(fref); + return var; + } + + + // Parser state carries one complete, conservative memory value. Precise + // alias partitions belong to the graph and are introduced by peepholes. + public Node mem( int alias ) { + assert alias == 1; + return in(update(var(1),null)); + } + public void mem( int alias, Node st ) { + assert alias == 1; + update(var(1),st); + } + + /** * Lookup a name in all scopes starting from most deeply nested. * @@ -267,7 +317,7 @@ public class ScopeNode extends Node { // Set real Phi in the loop head // The phi takes its one input (no backedge yet) from a recursive // lookup, which might have insert a Phi in every loop nest. - : loop.setDef(v._idx,PhiNode.make(v._name, v.type(), loop.ctrl(), loop.in(loop.update(v,null)._idx),null).peephole()); + : loop.setDef(v._idx,PhiNode.make(v._name, v.type(), loop.ctrl(), loop.in(loop.update(v,null)._idx),null).init()); setDef(v._idx,old); } //assert !v._final || st==null; @@ -305,8 +355,14 @@ public class ScopeNode extends Node { n.keep(); dup.addDef(ctrl()); // Control input is just copied - // Memory is one binding, with the same lazy loop Phi as scalar names. - for( int i=1; i<nIns(); i++ ) + // Memory is one ordinary parser-state value. At loop heads the + // Scope sentinel triggers the same lazy-Phi machinery as variables. + dup.addDef(loop ? this : mem()); + + // Copy of other inputs + for( int i=2; i<nIns(); i++ ) + // For lazy phis on loops we use a sentinel + // that will trigger phi creation on update dup.addDef(loop && loopVar(var(i)) ? this : in(i)); return dup; } @@ -321,14 +377,17 @@ public class ScopeNode extends Node { */ public RegionNode mergeScopes(ScopeNode that, Parser.Lexer loc) { RegionNode r = ctrl(new RegionNode(loc,null,ctrl(), that.ctrl()).keep()); - _merge(that,r); + this._merge(that,r); that.kill(); // Kill merged scope CodeGen.CODE.add(r); return r.unkeep(); } - private void _merge(ScopeNode that, RegionNode r) { - for( int i = 1; i < nIns(); i++) + public void _merge(ScopeNode that, RegionNode r) { + _merge(that,r,nIns()); + } + public void _merge(ScopeNode that, RegionNode r, int max ) { + for( int i = 1; i < max; i++) if( in(i) != that.in(i) ) { // No need for redundant Phis // If we are in lazy phi mode we need to a lookup // by name as it will trigger a phi creation @@ -341,18 +400,38 @@ public class ScopeNode extends Node { // Balance arms of an IF. Extra lonely defs are thrown: "if(pred) int x;". // Forward refs are copied to the other side, "as if" they were there all along. + // 'this' scope is the default, 'scope' might have frefs. public void balanceIf( ScopeNode scope ) { - for( int i = nIns(); i < scope.nIns(); i++ ) { + if( nIns()==scope.nIns() ) + return; // Fast-path cutout + int kidx = enclosingFunction(); + assert _kinds.at(kidx) == scope._kinds.at(kidx); + if( kidx==0 ) kidx = 1; + for( int i = _kinds.at(kidx-1)._lexSize; i < scope.nIns(); i++ ) { Var n = scope.var(i); + if( i<nIns() && var(i)==n ) continue; if( n.isFRef() ) { // RHS has forward refs - _vars.add(n); // Copy to LHS - addDef(scope.in(i)); + _vars.insert(n,i); + insertDef(i,scope.in(i)); } else throw Parser.error("Cannot define a '"+n._name+"' on one arm of an if",n._loc); } } + public void balanceLoopFRefs( ScopeNode scope ) { + for( int i = nIns(); i < scope.nIns(); i++ ) { + Var n = scope.var(i); + if( n.isFRef() ) { // Loop body has forward refs + Var v = new Var(nIns(),n._name,n.type(),n._final,n._loc,true); + v._uninit = n._uninit; + _vars.add(v); + addDef(scope.in(i)); + } + } + } + + // ------------------------------------------------ // peephole the backedge scope into this loop head scope // We set the second input to the phi from the back edge (i.e. loop body) public void endLoop(ScopeNode back, ScopeNode exit ) { @@ -360,49 +439,108 @@ public class ScopeNode extends Node { assert ctrl instanceof LoopNode loop && loop.inProgress(); ctrl.setDef(2,back.ctrl()); - _endLoop(this,back,exit); + this._endLoop( this, back, exit); back.kill(); // Loop backedge is dead // Now one-time do a useless-phi removal - _useless(); + this._useless(); } // Fill in the backedge of any inserted Phis void _endLoop( ScopeNode scope, Node back, Node exit ) { for( int i=1; i<nIns(); i++ ) { if( !loopVar(var(i)) ) continue; - if( _vars.at(i).type().isHighOrConst() ) continue; // Cannot lift higher than a constant, so no Phi - if( back.in(i) != scope ) { - PhiNode phi = (PhiNode)in(i); + Node n = in(i); + if( var(i).type().isHighOrConst() && !(n instanceof PhiNode phi && phi.inProgress()) ) { // Cannot lift higher than a constant, so no Phi + Node con = ConstantNode.make(var(i).type()).init(); + n.subsume(con); + n = con; + } else if( back.in(i) != scope ) { + PhiNode phi = (PhiNode)n; assert phi.region()==scope.ctrl() && phi.in(2)==null; phi.setDef(2,back.in(i)); // Fill backedge } if( exit.in(i) == scope ) // Replace a lazy-phi on the exit path also - exit.setDef(i,in(i)); - } - } - - - // Now one-time do a useless-phi removal - void _useless( ) { - for( int i=1; i<nIns(); i++ ) { - if( in(i) instanceof PhiNode phi ) { - // Do an eager useless-phi removal - Node in = phi.peephole(); - CodeGen.CODE.addAll(phi._outputs); - phi.moveDepsToWorklist(); - if( in != phi ) { - if( !phi.iskeep() ) // Keeping phi around for parser elsewhere - phi.subsume(in); - setDef(i,in); // Set the update back into Scope - } - } + exit.setDef(i,n); } } + // ------------------------------------------------ // Up-casting: using the results of an If to improve a value. // E.g. "if( ptr ) ptr.field;" is legal because ptr is known not-null. - public void addGuards( Node ctrl, Node pred, boolean invert ) { - assert ctrl instanceof CFGNode; + // + // This operation is lexical, not a full semantic theorem prover, and thus + // its behavior changes the language spec. Optimizations that hide the + // lexical structure (e.g. constant folding control flow) can hide what is + // guarded or how the guard is applied, making it somewhat fragile. Several + // good use cases are covered here, but not everything you might like. + // + // Simple lexical facts work well: + // + // ``` + // if( ptr ) + // return ptr.field; // `ptr` is guarded non-null here. + // + // if( a && a.ptr ) + // return a.ptr.x; // `a` is guarded while parsing the RHS load, + // // and remains guarded on the true arm. + // ``` + // + // Short-circuit expressions are lowered as nested Phis. We recursively + // decompose the recognized Phi shape, so chains such as: + // + // ``` + // if( x && y && z ) + // use(x,y,z); + // ``` + // + // discover the nested `x && y` and then `x`, `y`, and `z`; this is not + // merely a one-level lookup. Likewise, nested tests carry lexical guards + // inwards: + // + // ``` + // if( x && y ) + // if( z && a ) + // if( x.ptr && z.ptr ) + // ... + // ``` + // + // Here `x` is already guarded while parsing the outer true arm, `z` while + // parsing the inner true arm, and both are available before the matching + // loads. The later post-test decomposition is just a convenience for + // values still schedulable at the post-test control. + // + // The hard limit is that a semantic implication is not always a + // schedulable Node. For `(x && y)`, the true side implies that `y` was + // true, but `y` might be a value computed only on the RHS control arm: + // + // ``` + // val ptr = ...; + // if( ptr && (ptr=fcn() ? 0 : arg ) {} + // ``` + // + // A Guard pinned after the Phi merge and using the branch-local `ptr` + // value would give that value a use above its defining arm. GCM then has + // no legal block between the value's early control and the LCA of its + // uses. Thus `_addGuards` first checks that the predicate's effective + // early CFG dominates the control where the Guard would be pinned. If it + // does not, we skip that guard even if the fact is semantically true. + // + // A more complete answer would need a richer logical/proof node, or would + // need to rematerialize predicates on the guarded path instead of using + // branch-local nodes directly. Plain GuardNodes are intentionally kept to + // facts represented by values already available at the guard control. + // + // TODO: Make this more robust for shapes like: + // ``` + // if( !ary || !ary# ) exit(); // facts about ary are lost, despite the early exit + // for( ...; ary# ) ...; + // ``` + // Here the guard ends up guarding the whole expression (both ary and ary# + // are not-null), but loses that ary is not-null. A better answer might be + // a dedicated logical expression node, or some sort of gather-of-checked + // values to Guard post-test. + + public void addGuards( CFGNode ctrl, Node pred, boolean invert ) { _guards.add(ctrl); // Marker between guard sets // add pred & its cast to the normal input list, with special Vars if( pred==null || pred.isDead() ) @@ -410,10 +548,11 @@ public class ScopeNode extends Node { _addGuards(ctrl,pred,invert); } - private void _addGuards( Node ctrl, Node pred, boolean invert ) { + private void _addGuards( CFGNode ctrl, Node pred, boolean invert ) { if( pred==null || pred.isDead() ) return; - if( !availableAt(pred,(CFGNode)ctrl,new BitSet()) ) + CFGNode early = CFGNode.earlyCFG(pred); + if( early != null && !early.dominates(ctrl,null) ) return; // A single negation is handled below. For !!p (including p != null) // or a negated short-circuit Phi, also discover the underlying facts. @@ -423,8 +562,10 @@ public class ScopeNode extends Node { _addGuards(ctrl,not.in(1),!invert); pred.unkeep(); } - // A false || or true && proves both operands. Only guard values - // available at this control, not ones confined to the RHS branch. + // Short-circuit logic is represented by a Phi. For `a || b` being + // false, or `a && b` being true, both individual operands have the + // same proven value as the whole expression. The "skipped RHS" path + // is the Region input whose Phi value is the controlling If predicate. if( pred instanceof PhiNode phi && phi.nIns()==3 && phi.region() instanceof RegionNode r && r.nIns()==3 ) for( int i=1; i<3; i++ ) if( r.in(i) instanceof CProjNode prj && prj.ctrl() instanceof IfNode iff ) { @@ -438,31 +579,19 @@ public class ScopeNode extends Node { // Invert the If conditional if( invert ) pred = pred instanceof NotNode not ? not.in(1) : CodeGen.CODE.add(new NotNode(pred).peephole()); + Node zeroPred = pred instanceof NotNode not ? not.in(1) : null; // This is a zero/null test. // Compute the positive test type. _addGuard(true,ctrl,pred); - if( pred instanceof NotNode not ) - _addGuard(false,ctrl,not.in(1)); - } - - // A Phi's value is available at its Region. Other data nodes require all - // their inputs, including pinned control, to dominate the proposed guard. - static boolean availableAt(Node n, CFGNode ctrl, BitSet visit) { - if( n==null || n instanceof ConstantNode || visit.get(n._nid) ) return true; - visit.set(n._nid); - if( n instanceof PhiNode phi ) n = phi.region(); - if( n instanceof CFGNode cfg ) { - for( CFGNode dom=ctrl; dom!=null; dom=dom.idom() ) - if( dom==cfg ) return true; - return false; - } - for( Node def : n._inputs ) - if( !availableAt(def,ctrl,visit) ) return false; - return true; + + // Compute the negative test type. + if( zeroPred != null ) + _addGuard(false,ctrl,zeroPred); } private static Node shortCircuitSkippedPred( Node val, Node pred, CProjNode prj, boolean invert ) { - if( val == null ) return null; + if( val == null ) + return null; if( val == pred ) return (prj._idx==0)==invert ? val : null; if( val instanceof NotNode not && not.in(1)==pred ) @@ -475,16 +604,19 @@ public class ScopeNode extends Node { } private void _addGuard(boolean nonZero, Node ctrl, Node pred) { - Node guard = new GuardNode(nonZero,ctrl,pred.keep()).peephole(); - if( guard==pred ) { pred.unkeep(); return; } - guard.keep(); - _guards.add(pred); - _guards.add(guard); - replace(pred,guard); + Node guard = new GuardNode(nonZero,ctrl,pred).peephole(); + if( guard != pred ) { + pred.keep(); + guard.keep(); + _guards.add(pred); + _guards.add(guard); + replace(pred,guard,nonZero); + } } + // Remove matching pred/cast pairs from this guarded region. - public ScopeNode removeGuards( Node ctrl ) { + public void removeGuards( Node ctrl ) { assert ctrl instanceof CFGNode; // Pop the guards up to this region's marker. while( true ) { @@ -494,7 +626,6 @@ public class ScopeNode extends Node { g .unkill(); // Pop/kill cast _guards.pop().unkill(); // Pop/kill pred } - return this; } // If we find a guarded instance of pred, replace with the upcasted version @@ -524,11 +655,65 @@ public class ScopeNode extends Node { } - private void replace( Node old, Node cast ) { + private void replace( Node old, Node cast, boolean nonZero ) { assert old!=null && old!=cast; for( int i=0; i<nIns(); i++ ) - if( in(i)==old ) + if( in(i)==old || nonZero && nullCheckedPhi(in(i),old) ) setDef(i,cast); } + private static boolean nullCheckedPhi( Node n, Node old ) { + if( !(n instanceof PhiNode phi) || phi.nIns()!=3 ) + return false; + if( phi.in(1)==null || phi.in(2)==null ) + return false; + int zero = phi.in(1)._type == phi.in(1)._type.makeZero() ? 1 : + phi.in(2)._type == phi.in(2)._type.makeZero() ? 2 : -1; + if( zero == -1 ) + return false; + Node val = phi.in(3-zero); + return val==old || + val instanceof GuardNode guard && guard._nonZero && guard.in(1)==old; + } + + // ------------------------------------------------------------------------ + // Insert a top-level public external symbol, "as-if" some #include/import + // was executed at the top of tile. + public Var insertExtern( ExternNode ext ) { + // Size of top-level scope + int lexSize = _kinds.at(0)._lexSize; + Var var = new Var(lexSize,ext._extern,ext._con,true,null); + insert(var,ext,0); + return var; + } + + // ------------------------------------------------------------------------ + // Insert a Var with Node n in the lex scope just prior to kidx + private void insert( Var var, Node n, int kidx ) { + // Insert just prior to scope kidx + int nidx = _kinds.at(kidx)._lexSize; + _vars.insert(var,nidx); + insertDef(nidx,n); + // All outer Kinds lexical offsets bump by one + for( int i = kidx; i < depth(); i++ ) + _kinds.at(i)._lexSize++; + // All Vars outside the innermost lexical scope bump out one + for( int i=nidx+1; i<_vars._len; i++ ) + var(i)._idx++; + } + + // Save the existing Node state to an array + public Node[] save() { + Node[] olds = new Node[nIns()]; + for( int i=0; i<nIns(); i++ ) + olds[i] = in(i)==null ? null : in(i).keep(); + return olds; + } + + // Restore saved Node state back into scope + public void restore( Node[] olds ) { + assert olds.length<=nIns(); + for( int i=0; i<olds.length; i++ ) + setDef(i,olds[i]==null ? null : olds[i].unkeep()); + } } diff --git a/src/main/java/com/seaofnodes/simple/node/ShlNode.java b/src/main/java/com/seaofnodes/simple/node/ShlNode.java index 7d6e8e2e..380fc246 100644 --- a/src/main/java/com/seaofnodes/simple/node/ShlNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ShlNode.java @@ -8,6 +8,7 @@ import com.seaofnodes.simple.type.TypeInteger; public class ShlNode extends ArithNode { public ShlNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + @Override public String op() { return "<<"; } @Override long doOp( long x, long y ) { return x << y; } @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { @@ -36,7 +37,7 @@ public class ShlNode extends ArithNode { if( lhs instanceof AddNode add && addDep(add).in(2)._type instanceof TypeInteger c && c.isConstant() ) { long sum = c.value() << shl.value(); if( Integer.MIN_VALUE <= sum && sum <= Integer.MAX_VALUE ) - return new AddNode(new ShlNode(_loc,add.in(1),rhs).peephole(), com.seaofnodes.simple.Parser.con(sum) ); + return new AddNode(new ShlNode(_loc,add.in(1),rhs).peephole(), CodeGen.CODE.con(sum) ); } } diff --git a/src/main/java/com/seaofnodes/simple/node/ShrNode.java b/src/main/java/com/seaofnodes/simple/node/ShrNode.java index 4ce5f5b4..17170a1f 100644 --- a/src/main/java/com/seaofnodes/simple/node/ShrNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ShrNode.java @@ -7,6 +7,7 @@ import com.seaofnodes.simple.type.TypeInteger; public class ShrNode extends ArithNode { public ShrNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + @Override public String op() { return ">>>"; } @Override long doOp( long x, long y ) { return x >>> y; } @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { diff --git a/src/main/java/com/seaofnodes/simple/node/SplitNode.java b/src/main/java/com/seaofnodes/simple/node/SplitNode.java index a7fe6c5f..3042f857 100644 --- a/src/main/java/com/seaofnodes/simple/node/SplitNode.java +++ b/src/main/java/com/seaofnodes/simple/node/SplitNode.java @@ -3,13 +3,16 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; +import com.seaofnodes.simple.codegen.LRG; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.util.SB; public abstract class SplitNode extends MachConcreteNode { + public final LRG _lrg; // Live range for split public final String _kind; // Kind of split - public final byte _round; - public SplitNode(String kind, byte round, Node[] nodes) { super(nodes); _kind = kind; _round = round; } + public final byte _round; // Debugging info for which RegAlloc round + public SplitNode(LRG lrg,String kind, byte round, Node[] nodes) { super(nodes); _lrg=lrg; _kind = kind; _round = round; } // A register-only transfer may need another split to reach stack storage. public boolean isNonStackSplit() { return false; } @Override public String op() { return "mov"; } @@ -19,6 +22,7 @@ public abstract class SplitNode extends MachConcreteNode { else p.n(in(1)); return p.close(); } + public FunNode fun(Encoding enc) { return enc._code._regAlloc.lrg(this)._fun; } @Override public Type compute() { return in(0)._type; } @Override public Node idealize() { return null; } @Override public void asm(CodeGen code, SB sb) { diff --git a/src/main/java/com/seaofnodes/simple/node/StartCUNode.java b/src/main/java/com/seaofnodes/simple/node/StartCUNode.java new file mode 100644 index 00000000..0f7a322a --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/StartCUNode.java @@ -0,0 +1,22 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.Type; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// Start of a single compilation unit, distinct from the outer whole-program Start. +public class StartCUNode extends StartNode { + public final String _fname; + public StartCUNode(StartNode start, StopCUNode stop, Type arg, String fname) { super(start,stop,arg); _fname = fname; } + public StartCUNode(StartCUNode start) { super(start); _fname = start._fname; } + + + @Override public CFGNode cfg0() { return (CFGNode)in(0); } + + // IDom is cfg0() and depth is always Start+1 which is 0+1 or 1 + @Override public int idepth() { return 1; } + @Override public CFGNode idom(Node dep) { return cfg0(); } + +} diff --git a/src/main/java/com/seaofnodes/simple/node/StartNode.java b/src/main/java/com/seaofnodes/simple/node/StartNode.java index 2c913b88..3c605e30 100644 --- a/src/main/java/com/seaofnodes/simple/node/StartNode.java +++ b/src/main/java/com/seaofnodes/simple/node/StartNode.java @@ -3,61 +3,78 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.Parser; import com.seaofnodes.simple.type.*; - -/** Unknown callers: control, exposed memory, and the entry argument. - * Stop feeds discovered addresses back here during SCCP. This closes the - * cycle between known code and unknown callers without a runtime loop. +import com.seaofnodes.simple.util.Utils; +import java.util.Arrays; +import java.util.HashSet; + +/** + * The Start node represents the external world calling, be it calling the + * entry <clinit> or just some linked caller calling. + * <p> + * Start initially has 1 input (arg) from outside and the initial control. + * In ch10 we also add mem aliases as structs get defined; each field in struct + * adds a distinct alias to Start's tuple. + * <p> + * By ch25, Start is the external world (or linker) calling with everything + * that has escaped. Start is NOT the entry <clinit> itself, which can assume + * no memory nor fidxs exist nor escaped. Start gets escaped things from Stop; + * together these close the Grand Cycle between the code we know and the code + * we don't. */ public class StartNode extends LoopNode implements MultiNode { final Type _arg; - private TypeTuple _closed; - - public StartNode(Type arg) { super(null,null); _arg = arg; _type = compute(); } - public StartNode(StartNode start) { super(start); _arg = start==null ? null : start._arg; _closed=start==null ? null : start._closed; } - public void close() { - _closed=(TypeTuple)_type; - ProjNode mem=proj(1); - if( mem!=null ) mem.subsume(new ConstantNode(mem._type).peephole()); - // Stop is a graph root even after the analysis feedback edge is gone. - Node stop=in(1).keep(); - setDef(1,null); - stop.unkeep(); - } + public StartNode(StartNode start, StopNode stop, Type arg) { super(null,start,stop); _arg = arg; } + public StartNode(StartNode start) { super(start); _arg = start==null ? null : start._arg; } - @Override public String label() { return "Start"; } + public Type arg() { return _arg; } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p(label()); } - @Override public CFGNode cfg0() { return null; } + @Override public CFGNode cfg0() { return null; } // Get the one control following; error to call with more than one e.g. an - // IfNode or other multi-way branch. For Start, its "main" + // IfNode or other multi-way branch. For Start, it's the class init for + // this compilation unit. @Override public CFGNode uctrl() { - // Find "main", it's the start. + // Find module.<clinit>, it's the start. CFGNode C = null; for( Node use : _outputs ) - if( use instanceof FunNode fun && fun.sig().isa(CodeGen.CODE._main) ) + if( use instanceof FunNode fun && CodeGen.CODE.entryClinitName().equals(fun._name) ) { assert C==null; C = fun; } + if( C==null ) + for( FunNode fun : CodeGen.CODE._linker ) + if( fun!=null && !fun.isDead() && CodeGen.CODE.entryClinitName().equals(fun._name) ) + { assert C==null; C = fun; } return C; } - - @Override public TypeTuple compute() { - if( _closed!=null ) return _closed; - TypeMem mem = in(1)==null || CodeGen.CODE._phase==null || - CodeGen.CODE._phase.ordinal()<CodeGen.Phase.Opto.ordinal() ? TypeMem.BOT : - in(1)._type instanceof TypeMem m ? m : in(1)._type.isHigh() ? TypeMem.START : TypeMem.BOT; - return TypeTuple.make(Type.CONTROL,mem,_arg); + boolean escapedFIDX(int fidx) { + // Public class fields are callable by the unknown external world + // regardless of the current SCCP approximation of Start memory. + if( CodeGen.CODE.publicFIDX(fidx) ) return true; + if( !(_type instanceof TypeTuple tt) ) + return !_type.isHigh(); // TOP never escapes, BOTTOM always escapes + TypeMem tmem = (TypeMem)tt._types[1]; + if( tmem == TypeMem.BOT ) return false; + if( tmem == TypeMem.TOP ) return false; + if( XInt.isHigh(tmem._escFs) ) + return false; + return XInt.bit(tmem._escFs,fidx); } - public boolean escapedFIDX(int fidx) { - if( !(_type instanceof TypeTuple tt) ) return !_type.isHigh(); - return XInt.bit(tt.mem()._escFs,fidx); + @Override public TypeTuple compute() { + StopNode stop = (StopNode)in(1); + TypeMem tmem = stop._type instanceof TypeTuple tt + ? tt.mem() + : stop._type.isHigh() ? TypeMem.START : TypeMem.BOT; + assert tmem._alias==1 && !tmem._one; + return TypeTuple.make(Type.CONTROL,tmem,_arg); } @Override public Node idealize() { return null; } @@ -66,4 +83,5 @@ public class StartNode extends LoopNode implements MultiNode { @Override public int idepth() { return 0; } @Override public CFGNode idom(Node dep) { return null; } + @Override public boolean eq( Node n ) { return true; } } diff --git a/src/main/java/com/seaofnodes/simple/node/StopCUNode.java b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java new file mode 100644 index 00000000..53c2325c --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java @@ -0,0 +1,126 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.*; + +import java.util.HashMap; +import java.util.IdentityHashMap; + +// Stop of a single compilation unit, distinct from the outer whole-program Stop. +public class StopCUNode extends StopNode { + public StopCUNode() { super(); } + public StopCUNode(StopNode stop) { super(stop); } + + + public StartCUNode start() { + for( Node use : _outputs ) + if( use instanceof StartCUNode start ) + return start; + return null; + } + + @Override + public TypeTuple compute() { + // During Parsing, new Stops and Returns are being added. This Stop + // cannot know if more are coming, so must assume the worst. + if( CodeGen.CODE._phase ==null || CodeGen.CODE._phase == CodeGen.Phase.Parse ) + return TypeTuple.STATE; + + StartCUNode start = start(); + // Memory start from all returns + TypeMem xmem = TypeMem.START; + // Escape summaries are monotone discovery facts. In Opto, a Return + // can widen from a precise set to FULL; the public-only normalization + // below must not forget private bits already discovered precisely from + // that Return on an earlier SCCP step. + if( CodeGen.CODE._phase.ordinal() >= CodeGen.Phase.Opto.ordinal() && + CodeGen.CODE._phase.ordinal() <= CodeGen.Phase.Lower.ordinal() && + _type instanceof TypeTuple old && old.mem() instanceof TypeMem omem ) + xmem = xmem.makeFrom(XInt.meet(xmem._escFs,omem._escFs), + XInt.meet(xmem._escAs,omem._escAs)); + + for( Node def : _inputs ) { + ReturnNode ret = (ReturnNode)def; + if( ret._type == Type.TOP ) continue; + if( !(ret._type instanceof TypeTuple tret) ) + return TypeTuple.STATE; // Must be Type.BOTTOM + + FunNode fun = ret.fun(); + boolean noUserCtor = fun.isInstance() && + !CodeGen.hasUserConstructor(((TypeMemPtr)fun.sig().arg(0))._obj); + // Instance <init> is an allocation helper, never an externally + // callable constructor when a user constructor exists. Otherwise + // the same no-arg <init> is the public constructor. + boolean pub = fun.isPublic() && (fun.isClz() || noUserCtor); + TypeFunPtr sig = fun.sig(); + // If public init, the function escapes and the related class escapes + if( pub ) { + xmem = xmem.escapes(sig); + // Classes also escape their public clazz pointer, which then + // escapes all their public fields. Private fields do not + // escape, and are only available from the local class code. + if( fun.isClz() ) + xmem = xmem.escapesAliases(sig._sig[0]); + } + + // The instance <init> Start edge supplies conservative pre-Opto + // inputs, but its private-memory result is consumed only by the + // allocation sequence and is never an externally visible return. + if( fun.isInstance() && !noUserCtor ) continue; + + // Does this function escape? If not, then no need to MEET + // here as its effects are not visible to the outside world. + addDepForwards(start); // Check again if function escapes + if( pub || start.escapedFIDX(sig.fidx()) ) { + // External callers already hold their object arguments, so + // writes through those arguments expose addresses too. + for( Node use : fun._outputs ) + if( use instanceof ParmNode p && p._idx>=2 ) + xmem=publicBottom(xmem.escapes(addDepForwards(p)._type)); + if( tret.ctl().isHigh() ) continue; // Return not reachable + // Capture (precisely?) all escaping pointer aliases and fidxs. + // Escaped fidxs means the linked world can call that function; + // escaped aliases means the linked world can R/W those aliases. + xmem = xmem.escapes(tret.ret()); + xmem = publicBottom(xmem); + xmem = (TypeMem)xmem.meet(publicBottom(tret.mem(),xmem)); + } + } + // Native retention is shared by the whole program, including calls + // on paths with no Return. A class boundary must not hide it. + for( CallNode call : CodeGen.CODE._calls ) + if( !call.isDead() ) xmem=publicBottom(xmem.escapes(call.exposed(this))); + assert xmem._alias==1 && !xmem._one; + return TypeTuple.make(Type.CONTROL,xmem,Type.BOTTOM); + } + + // FULL escape sets arise from global BOTTOM. Across a compilation-unit + // boundary global BOTTOM exposes every public field, while retaining any + // private escapes already discovered precisely. + private TypeMem publicBottom(TypeMem mem) { return publicBottom(mem,mem); } + private TypeMem publicBottom(TypeMem mem, TypeMem prior) { + // The pessimistic Iter pass naturally lifts transient FULL inputs to + // precise finite sets. Replacing FULL early there would let a later + // input refinement add bits, reversing Iter's monotonic direction. + // This normalization is needed only for Opto's optimistic grand cycle. + if( CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal() || + CodeGen.CODE._phase.ordinal() > CodeGen.Phase.Lower.ordinal() ) return mem; + int[] escFs = XInt.isHigh(mem._escFs) + ? XInt.meet(prior._escFs,CodeGen.CODE.publicFIDXs()) + : mem._escFs; + int[] escAs = XInt.isHigh(mem._escAs) + ? XInt.meet(prior._escAs,CodeGen.CODE.publicAliases()) + : mem._escAs; + return escFs==mem._escFs && escAs==mem._escAs ? mem : mem.makeFrom(escFs,escAs); + } + + @Override + public Node idealize() { + int len = nIns(); + for( int i=0; i<nIns(); i++ ) + if( in(i) instanceof ReturnNode ret && addDep(ret.fun()).isDead() ) + delDef(i--); + if( len != nIns() ) return this; + return null; + } +} diff --git a/src/main/java/com/seaofnodes/simple/node/StopNode.java b/src/main/java/com/seaofnodes/simple/node/StopNode.java index 83acc8e3..7238f1f9 100644 --- a/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -2,37 +2,38 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; -import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.type.*; -public class StopNode extends CFGNode { +import java.util.HashMap; +import java.util.IdentityHashMap; - public final String _src; - private Type _closed; - public void close() { _closed=_type; } +// A collection of function ReturnNodes for a compilation unit. +public class StopNode extends CFGNode { - public StopNode(String src) { + public StopNode() { super(); - _src = src; _type = compute(); } - public StopNode(StopNode stop) { super(stop); _src = stop==null ? null : stop._src; _closed=stop==null ? null : stop._closed; } + public StopNode(StopNode stop) { super(stop); } - @Override - public String label() { - return "Stop"; - } @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { // For the sake of many old tests, and single value prints as "return val" - ReturnNode ret1 = ret(); + ReturnNode ret1 = null; + for( Node n : _inputs ) { + if( n instanceof ReturnNode ret ) { + if( ret1 != null ) { ret1=null; break; } + ret1 = ret; + } + } if( ret1!=null ) return p.n(ret1); + p.p("Stop[ "); - for( Node ret : _inputs ) - if( ret!=null ) { - String name = ((ReturnNode)ret).fun()._name; + for( Node n : _inputs ) + if( n instanceof ReturnNode ret ) { + String name = ret.fun()._name; if( name== null || !name.startsWith("sys.") ) p.n(ret).p(" "); } @@ -45,42 +46,36 @@ public class StopNode extends CFGNode { // If a single Return, return it. // Otherwise, null because ambiguous. public ReturnNode ret() { - Node ret1 = this; - for( Node ret : _inputs ) { - String name = ((ReturnNode)ret).fun()._name; - if( name==null || !name.startsWith("sys.") ) - ret1 = ret1==this ? ((ReturnNode)ret) : null; + ReturnNode ret1 = null; + for( Node n : _inputs ) { + // TODO: remove isPublic + if( n instanceof ReturnNode ret && ret.fun().isPublic() ) { + if( ret1 != null ) return null; // Ambiguous + ret1 = ret; + } } - return ret1==this ? null : (ReturnNode)ret1; + return ret1; } @Override - public Type compute() { - if( _closed!=null ) return _closed; - CodeGen code=CodeGen.CODE; - if( code._phase==null || code._phase.ordinal()<CodeGen.Phase.Opto.ordinal() ) return TypeMem.BOT; - TypeMem mem=TypeMem.START; - for( Node n : _inputs ) { - ReturnNode ret=(ReturnNode)n; - if( !ret.fun().unknownCallers() ) continue; - // An external caller already holds its arguments. Writes through - // those pointers are visible even if this function returns a scalar. - for( Node use : ret.fun()._outputs ) - if( use instanceof ParmNode p && p._idx>=2 ) - mem=mem.escapes(addDepForwards(p)._type); - if( !(ret._type instanceof TypeTuple tt) || tt._types[0].isHigh() ) continue; - mem=mem.escapes(tt.ret()).escapes(tt._types[1]); - } - for( CallNode call : code._calls ) - if( !call.isDead() ) mem=mem.escapes(call.exposed(this)); - return mem; + public TypeTuple compute() { + // During Parsing, new Stops and Returns are being added. This Stop + // cannot know if more are coming, so must assume the worst. + if( CodeGen.CODE._phase ==null || CodeGen.CODE._phase == CodeGen.Phase.Parse ) + return TypeTuple.STATE; + + // Just meet-over-inputs. + TypeTuple tt = TypeTuple.STOP_HIGH; + for( Node def : _inputs ) + tt = (TypeTuple)tt.meet(def._type); + assert tt.mem()._alias==1 && !tt.mem()._one; + return tt; } - @Override - public Node idealize() { + @Override public Node idealize() { int len = nIns(); for( int i=0; i<nIns(); i++ ) - if( addDep(((ReturnNode)in(i)).fun()).isDead() ) + if( in(i).nIns()==0 ) delDef(i--); if( len != nIns() ) return this; return null; @@ -90,7 +85,10 @@ public class StopNode extends CFGNode { if( validIDepth() ) return _idepth; int d=0; for( Node ret : _inputs ) - d = Math.max(d,((ReturnNode)ret).idepth()+1); + // All public results are hooked on the Stop, including data + // values, so must check CFG + if( ret instanceof CFGNode cfg ) + d = Math.max(d,cfg.idepth()+1); return cacheIDepth(d); } } diff --git a/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/src/main/java/com/seaofnodes/simple/node/StoreNode.java index 06ff51fc..bdd07c8d 100644 --- a/src/main/java/com/seaofnodes/simple/node/StoreNode.java +++ b/src/main/java/com/seaofnodes/simple/node/StoreNode.java @@ -2,9 +2,12 @@ package com.seaofnodes.simple.node; import com.seaofnodes.print.ExprPrinter; -import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.*; +import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +import java.util.HashMap; +import java.util.IdentityHashMap; /** * Store represents setting a value to a memory based object, in chapter 10 @@ -12,7 +15,10 @@ import com.seaofnodes.simple.type.*; */ public class StoreNode extends MemOpNode { - private final boolean _init; // Initializing writes are allowed to write null + final boolean _init; // Initializing writes are allowed to write null + // Semantic store width in bytes. Zero is the sole undecided state; + // otherwise hardware and ideal Stores are the 1/2/4/8-byte variants. + private byte _size; /** * @param name The struct field we are assigning to @@ -21,20 +27,32 @@ public class StoreNode extends MemOpNode { * @param off The offset inside the struct base * @param value Value to be stored */ - public StoreNode(Parser.Lexer loc, String name, int alias, Type glb, Node mem, Node ptr, Node off, Node value, boolean init) { - super(loc, name, alias, false, glb, mem, ptr, off, value); + public StoreNode(Parser.Lexer loc, String name, int alias, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value, boolean init) { + this(loc,name,alias,decl,ctrl,mem,ptr,off,value,init,(byte)0); + } + private StoreNode(Parser.Lexer loc, String name, int alias, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value, boolean init, byte size) { + super(loc, name, alias, false, decl, ctrl, mem, ptr, off, value); _init = init; + _size = size; } - // Debugger label + @Override boolean canDrop(MemOpNode other, Node dep) { return super.canDrop(other,dep) && _init==((StoreNode)other)._init; } + // Debugger label @Override public String label() { return "st_"+mlabel(); } @Override public boolean isMem() { return true; } + public Node nnptr() { + if( !(ptr() instanceof GuardNode cast) ) return ptr(); + assert cast._nonZero; + return cast.in(1); + } public Node val() { return in(4); } + public int storeSize() { assert _size==1 || _size==2 || _size==4 || _size==8; return _size; } + public int log_size() { return Integer.numberOfTrailingZeros(storeSize()); }; @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { @@ -43,63 +61,123 @@ public class StoreNode extends MemOpNode { @Override public Type compute() { - if( _alias==1 ) return TypeMem.BOT; - Type val = val()._type; - // Initializing Stores are emitted at helper exits for a fresh object. - // They replace this private field; Escape later joins it with public - // memory. Keep an unwritten BOTTOM visible to the completion check. - if( _init ) return TypeMem.make(_alias,val,XInt.EMPTY,XInt.EMPTY).escapes(val); - assert !_declaredType.isFRef(); - Type t = val.join(_declaredType).meet(MemMergeNode.contents(mem(),_alias,this)); - TypeMem prior=mem()._type instanceof TypeMem m ? m : mem()._type.isHigh() ? TypeMem.TOP : TypeMem.BOT; - return prior.withContents(_alias,t).escapes(val); + Type val = val()._type; + Type mem0 = mem()._type; + Type ptr0 = ptr()._type; + // Validate argument types + if( ptr0.isHigh() ) + return TypeMem.TOP; + if( !(mem0 instanceof TypeMem mem) || + !(ptr0 instanceof TypeMemPtr ptr) ) + return mem0.isHigh() || ptr0==Type.NIL ? TypeMem.TOP : TypeMem.BOT; + + // Sharpen memory value; required for narrowing stores where the parser + // inserts zero/sign masking and somebody reads the TypeMem type. + Type decl = declaredType(); + if( decl != Type.BOTTOM ) + { assert decl != Type.TOP; val = val.join(decl); } + // A singleton class's final constant field lives in the static image. + // Its precise memory slice is a constant too; ordinary peepholes can + // replace this Store before scheduling or instruction selection. + // A constant runtime value alone is not enough: mutable fields (notably + // $init) still need their writes, and instance fields need allocation. + Field fld = ptr._obj.field(_name); + if( _alias!=1 && ptr._one && Parser.startsClzPrefix(ptr._obj._name) && + fld!=null && fld._final && decl.isConstant() ) + return TypeMem.make(_alias,val,true,true,true,mem._escFs,mem._escAs).escapesFrom(val); + + // Allocation uses a private TypeMem and nothing else does. This + // memory is truly private; a temporary singleton until it escapes - + // which is never does in a constructor. + if( !_name.equals("[]") && mem._one ) + // Just track the stored value + return TypeMem.make(_alias,val,true,false,_init,null,null).escapesFrom(val); + + // Normal aliasing Store. + assert mem._alias==1 || mem._alias==_alias; // Perfect aliasing + + // Single alias store. If memory is fat, attempt to get a narrow slice. + Type tfld = mem._alias==1 && mem._t instanceof TypeStruct ts + ? ts._fields[ts.findAlias(_alias)]._t + : mem._t; + + // Same alias, meet into other fields. + Type t = val.meet(tfld); + // Bulk memory's final bit describes its conservative remainder, not + // this newly precise slice. Slice finality comes from the field. + boolean xfinal = fld != null && fld._final; + return TypeMem.make(_alias,t,mem._one,mem._clz,xfinal,mem._escFs,mem._escAs).escapesFrom(val); } @Override public Node idealize() { - if( _alias==1 ) { - if( !(ptr()._type instanceof TypeMemPtr ptr) || ptr._obj._open ) return null; - Field field = ptr._obj.field(_name); - if( field==null ) return null; - Node value = new ConvertNode(field._t,val()).peephole(); - StoreNode store = new StoreNode(_loc,_name,field._alias,field._t,mem(),ptr(),off(),value,_init); - store.setDef(0,in(0)); - return new MemMergeNode(mem(),field._alias,store.peephole()); - } - if( mem() instanceof MemMergeNode merge ) { - setDef(1,CodeGen.CODE.add(merge.alias(_alias))); - return this; + // Freeze the semantic Store variant as soon as the pointer exposes a + // canonical field declaration. Waiting until after structural memory + // peeps can lose the declaration to a flow-sensitive constant field + // state. + if( _size==0 && (_size= storageType()) != 0 ) { + unlock(); // Width participates in the cached hash. + return init(); } + assert !(mem()._type instanceof TypeMem mem) || _alias == 1 || mem._alias == 1 || _alias == mem._alias; - if( mem() instanceof CastNode cast ) { - setDef(1,cast.in(1)); + // Simple store-after-MemMerge to a known alias can bypass. Happens when inlining. + Node mem = mem(); + if( _alias != 1 && mem instanceof MemMergeNode merge ) { + setDef(1,merge.alias(_alias)); return this; } - if( _init && mem() instanceof ProjNode p && p.in(0) instanceof NewNode && - val()._type.isConstant() && MemMergeNode.contents(mem(),_alias,this)==val()._type ) - return mem(); // calloc already supplied this legal default. + Field fld; + if( _alias==1 && ptr()._type instanceof TypeMemPtr tmp && (fld=tmp._obj.field(_name)) != null && _size != 0 ) { + // Expand Store(bulkMem,alias#1) into MemMerge(bulkMem,#N:Store(bulkMem,alias#N)). + + // Normally I'd like to just drop in this xform and move on, but + // users of the bulk Store memory might nontheless depend on the + // sharp memory type (e.g. I64) from the store's memory. Using the + // MemMerge means using the bulk memory which e.g. might be BOT. + // Check for store users with the same alias, and use the new store + // directly, preserving the sharper graph. + Node st = new StoreNode(_loc,_name,fld._alias,fld._t,in(0),mem,ptr(),off(),val(),_init,_size).peephole(); + for( int i=0; i<nOuts(); i++ ) { + Node use = out(i); + if( useAlias(use)==fld._alias ) { + int idx = use._inputs.find(this); + assert idx != -1; + CodeGen.CODE.add(use).setDef(idx,st); + i--; // setDef removed use from this Store's outputs + } + } + MemMergeNode mmm = mem instanceof BulkMemPhiNode bulk + ? bulk.aggregate(mem,fld._alias,st) + : new MemMergeNode(null,mem); + if( !(mem instanceof BulkMemPhiNode) ) + mmm.alias(fld._alias,st); + return mmm.init(); + } // Simple store-after-store on same address. Should pick up the // required init-store being stomped by a first user store. - if( mem() instanceof StoreNode st && - ptr()==st.ptr() && _alias==st._alias && // Must check same object - off()==st.off() && // And same offset + if( mem instanceof StoreNode st && + ptr()==st.ptr() && // Must check same object + off()==st.off() && // And same offset (could be "same alias" but this handles arrays to same index) ptr()._type instanceof TypeMemPtr && // No bother if weird dead pointers // Must have exactly one use of "this" or you get weird // non-serializable memory effects in the worse case. checkOnlyUse(st) ) { - assert _name.equals(st._name); // Equiv class aliasing is perfect + assert _name==st._name; // Equiv class aliasing is perfect setDef(1,st.mem()); return this; } + // Private constructor memory describes the value that the next helper + // reads. Keep its conversion, even if a machine store could truncate it. + // This is the same rule as in the construction and escape chapters. + boolean priv = mem()._type instanceof TypeMem tm && tm._one; // Value is automatically truncated by narrow store - // Private summaries describe the converted field value, so retain - // its conversion here. Public memory only needs the declared bound. - if( !_init && val() instanceof AndNode and && and.in(2)._type.isConstant() ) { - int log = _declaredType.log_size(); + if( !priv && _size!=0 && val() instanceof AndNode and && and.in(2)._type.isConstant() ) { + int log = log_size(); if( log<3 ) { // And-mask vs narrow store long mask = ((TypeInteger)and.in(2)._type).value(); long bits = (1L<<(8<<log))-1; @@ -111,7 +189,7 @@ public class StoreNode extends MemOpNode { } // Store will chop high order bits off; math to change those bits can be dropped. - if( !_init && val() instanceof SarNode shr && + if( !priv && _size!=0 && val() instanceof SarNode shr && shr.in(1) instanceof ShlNode shl && shr.in(2)._type.isConstant() && shl.in(2)._type.isConstant() ) { @@ -127,15 +205,56 @@ public class StoreNode extends MemOpNode { return this; } } + return null; } + // Flow-sensitive pointer types can carry a current field value (e.g. the + // constant zero in a u32 field). Storage width comes from the canonical + // struct declaration. If the pointer/definition is not available yet, + // leave the Store undecided instead of consulting the value or `_con`. + private byte storageType() { + if( !(ptr()._type instanceof TypeMemPtr tmp) ) + return 0; + TypeStruct obj = (TypeStruct)Parser.TYPES.get(tmp._obj._name); + Field fld = obj.field(_name); + if( fld == null || fld._t == null ) + return 0; + if( fld._t == Type.BOTTOM ) + return 0; // Store into a final field + assert fld._t != Type.TOP; + // Freshly parsed field stores retain the declaring structure on their + // symbolic offset even after the pointer's flow type sharpens. + if( fld._t.isConstant() ) { + if( !(off() instanceof ConFldOffNode coff) ) + return 0; + fld = coff._ts.field(_name); + if( fld == null || fld._t == null ) + return 0; + } + // Compute storage size from field type + byte size = (byte)(1 << fld._t.log_size()); + assert size==1 || size==2 || size==4 || size==8; + return size; + } + + // Alias required by a direct consumer of this Store's memory result. + // Keep in sync with BulkMemPhiNode.outputAlias. + private int useAlias(Node use) { + return switch( use ) { + case MemOpNode mem -> mem._alias; + case MemPhiNode phi -> phi._alias; + case EscapeNode esc -> esc.pub()==this ? esc.fld()._alias : 0; + default -> 0; + }; + } + // Phi(mem, Store(phi,ptr,off,val)) -> Store(mem,ptr,off,Phi(load(mem),val)). // With no observers of the backedge store, only the last value matters. // The entry load preserves the field even when the loop takes zero trips. Node sink(MemPhiNode phi) { phi.addDepForwards(this); - if( nOuts()!=1 ) return null; + if( nOuts()!=1 || _alias==1 || _size==0 || _init ) return null; phi.addDep(ptr()); // Start with a fixed field of an allocation dominating the loop. // In particular, do not sink stores through a loop-varying address. @@ -159,9 +278,9 @@ public class StoreNode extends MemOpNode { if( ctrl==null ) return null; Node mem = phi.in(1); - Node init = new LoadNode(_loc,_name,_alias,mem,ptr(),off()).peephole(); + Node init = new LoadNode(_loc,_name,_alias,null,mem,ptr(),off()).peephole(); Node value = CodeGen.CODE.add(new PhiNode(_name,loop,init,val()).peephole()); - Node sink = new StoreNode(_loc,_name,_alias,_declaredType,mem,ptr(),off(),value,_init); + Node sink = new StoreNode(_loc,_name,_alias,_con,null,mem,ptr(),off(),value,_init,_size); // Break the memory cycle, killing the old store. Keep the Phi alive // even if that store was its last use, until our caller replaces it. phi.keep(); @@ -185,11 +304,12 @@ public class StoreNode extends MemOpNode { public Parser.ParseException err() { Parser.ParseException err = super.err(); if( err != null ) return err; - if( !(ptr()._type instanceof TypeMemPtr tmp) ) - return null; // Report the bad or unresolved receiver at its source. - Field field = tmp._obj.field(_name); - if( field==null ) return Parser.error("Cannot resolve field '"+_name+"'",_loc); - if( (tmp._ro || field._final) && !_init ) + assert _size>0 || CodeGen.CODE._phase.ordinal() <= CodeGen.Phase.FinalizeIR.ordinal(); + if( ptr()._type == Type.TOP ) // Dead store? + return null; + TypeMemPtr tmp = (TypeMemPtr)ptr()._type; + Field f = tmp._obj.field(_name); + if( f!=null && (tmp._ro || f._final) && !_init ) return Parser.error("Cannot modify final field '"+_name+"'",_loc); return null; } @@ -202,4 +322,5 @@ public class StoreNode extends MemOpNode { // Identity also disables generic Phi factoring of Stores. // The closed-loop Store sink is a separate peep and still applies. @Override public boolean eq(Node n) { return this == n; } + @Override int hash() { return super.hash() ^ _size; } } diff --git a/src/main/java/com/seaofnodes/simple/node/StructNode.java b/src/main/java/com/seaofnodes/simple/node/StructNode.java deleted file mode 100644 index a91ee5de..00000000 --- a/src/main/java/com/seaofnodes/simple/node/StructNode.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.print.ExprPrinter; - -import com.seaofnodes.simple.type.*; -import com.seaofnodes.simple.util.SB; - -/** - * Build a compound object - */ -public class StructNode extends Node { - - public TypeStruct _ts; - public StructNode(TypeStruct ts) { _ts=ts; assert !ts._open; } - - @Override boolean _upgradeType(java.util.HashMap<String,Type> types) { - if( _ts==null ) return false; - TypeStruct ts = (TypeStruct)_ts.upgradeType(types); - if( ts==_ts ) return false; - unlock(); - _ts = ts; - return true; - } - - @Override public String label() { return _ts==null ? "STRUCT?" : _ts.str(); } - - @Override - protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { - if( _ts==null ) return p.p("STRUCT?"); - p.p(_ts._name).p(" {"); - for( int i=0; i<nIns(); i++ ) { - p.p(_ts._fields[i]._fname).p(":"); - p.p(in(i)==null ? Type.BOTTOM : in(i)._type); - p.p("; "); - } - return p.unchar("; ").p("}"); - } - - @Override - public TypeStruct compute() { - if( _ts==null ) return TypeStruct.BOT; - Field[] fs = new Field[_ts._fields.length]; - for( int i=0; i<fs.length; i++ ) - fs[i] = _ts._fields[i].makeFrom(in(i)==null ? Type.TOP : in(i)._type); - return TypeStruct.make(_ts._name,false,fs); - } - - @Override - public Node idealize() { return null; } - - @Override - public boolean eq(Node n) { return _ts == ((StructNode)n)._ts; } - - @Override - int hash() { return _ts==null ? 0 : _ts.hashCode(); } -} diff --git a/src/main/java/com/seaofnodes/simple/node/SubFNode.java b/src/main/java/com/seaofnodes/simple/node/SubFNode.java deleted file mode 100644 index d6a908ca..00000000 --- a/src/main/java/com/seaofnodes/simple/node/SubFNode.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.seaofnodes.simple.node; - -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeFloat; - -import java.util.BitSet; - -public class SubFNode extends ArithFNode { - public SubFNode(Node lhs, Node rhs) { super(lhs, rhs); } - public SubFNode(ArithNode n) { super(n); } - - @Override public String label() { return "SubF"; } - - @Override public String glabel() { return "-"; } - - @Override double doOp( double x, double y ) { return x-y; } - - @Override - public Node idealize() { - // Sub of 0. - Type t2 = in(2)._type; - if ( t2.isConstant() && t2 instanceof TypeFloat i && i.value()==0 ) - return in(1); - - return null; - } - @Override Node copy(Node lhs, Node rhs) { return new SubFNode(lhs,rhs); } -} diff --git a/src/main/java/com/seaofnodes/simple/node/SubNode.java b/src/main/java/com/seaofnodes/simple/node/SubNode.java index c47ffb3b..adad2ed5 100644 --- a/src/main/java/com/seaofnodes/simple/node/SubNode.java +++ b/src/main/java/com/seaofnodes/simple/node/SubNode.java @@ -10,6 +10,7 @@ public class SubNode extends ArithNode { public SubNode(Node lhs, Node rhs) { super(null, lhs, rhs); } public SubNode(Node lhs, Node rhs, byte mode) { super(null, lhs, rhs,mode); } + @Override public String op() { return "-"; } @Override long doOp( long x, long y ) { return x - y; } diff --git a/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java b/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java index 840a95ec..1acf55b6 100644 --- a/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java @@ -9,7 +9,6 @@ import com.seaofnodes.simple.type.TypeInteger; public class ToFloatNode extends Node { public ToFloatNode(Node lhs) { super(null, lhs); } - @Override public String label() { return "ToFloat"; } @Override protected String format() { return "(flt)%1"; } diff --git a/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java b/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java index df740202..9136aacc 100644 --- a/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java +++ b/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java @@ -7,6 +7,7 @@ import com.seaofnodes.simple.type.TypeInteger; // Truncate toward zero, saturating overflow; NaN converts to zero. public class ToIntegerNode extends Node { public ToIntegerNode(Node lhs) { super(null,lhs); } + @Override public String label() { return "ToInteger"; } @Override protected String format() { return "(int)%1"; } diff --git a/src/main/java/com/seaofnodes/simple/node/TypeNode.java b/src/main/java/com/seaofnodes/simple/node/TypeNode.java new file mode 100644 index 00000000..970472da --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/node/TypeNode.java @@ -0,0 +1,32 @@ +package com.seaofnodes.simple.node; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Utils; +import java.util.BitSet; +import java.util.HashMap; + +// Node carries an extra internal type +public abstract class TypeNode extends Node { + public Type _con; + public TypeNode(Type t, Node... nodes) { super(nodes); assert t!=null; _con = t; } + public TypeNode(TypeNode tn) { super(tn); _con = tn._con; } + // Copy edges from ideal, but 'this' is a Type-Mach-Node (e.g. MemOpX86) and needs a _con + public TypeNode(Node ideal, Type con) { super(ideal); _con = con; } + + // Upgrade the internal type + @Override boolean _upgradeType( HashMap<String,Type> TYPES) { + return liftType(_con.upgradeType(TYPES)); + } + + // Lift type + public boolean liftType( Type t ) { + if( t == _con ) return false; + unlock(); // Unlock before changing _con + _con = t; + return true; + } + + @Override public boolean eq(Node n) { return _con==((TypeNode)n)._con; } + @Override int hash() { return _con.hashCode(); } +} diff --git a/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java b/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java index 477b9af8..3792e224 100644 --- a/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java +++ b/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java @@ -8,7 +8,7 @@ import com.seaofnodes.simple.type.Type; public class XCtrlNode extends CFGNode { public XCtrlNode() { super(new Node[]{CodeGen.CODE._start}); } - @Override public String label() { return "Xctrl"; } + @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("Xctrl"); } @Override public boolean isConst() { return true; } @Override public Type compute() { return Type.XCONTROL; } diff --git a/src/main/java/com/seaofnodes/simple/node/XorNode.java b/src/main/java/com/seaofnodes/simple/node/XorNode.java index e3dac2d0..e9cd13a5 100644 --- a/src/main/java/com/seaofnodes/simple/node/XorNode.java +++ b/src/main/java/com/seaofnodes/simple/node/XorNode.java @@ -7,6 +7,7 @@ import com.seaofnodes.simple.type.TypeInteger; public class XorNode extends ArithNode { public XorNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); } + @Override public String op() { return "^"; } @Override long doOp( long x, long y ) { return x ^ y; } @Override TypeInteger doOp( TypeInteger x, TypeInteger y ) { diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java index 127f17ff..31bbde86 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/BranchARM.java @@ -9,9 +9,11 @@ import com.seaofnodes.simple.util.Utils; // Jump on flags, uses flags public class BranchARM extends IfNode implements MachNode, RIPRelSize { String _bop; - BranchARM(IfNode iff, String bop ) { + final boolean _fp; + BranchARM(IfNode iff, String bop, boolean fp ) { super(iff); _bop = bop; + _fp = fp; } @Override public String op() { return "j"+_bop; } @Override public String label() { return op(); } @@ -36,7 +38,7 @@ public class BranchARM extends IfNode implements MachNode, RIPRelSize { // by comparison (or sub). No need for regs because it uses flags if( in(1)!=null ) { // B.cond - enc.add4( Arm64.b_cond(Arm64.OP_BRANCH, 0, Arm64.make_condition(_bop)) ); + enc.add4( Arm64.b_cond(Arm64.OP_BRANCH, 0, Arm64.make_condition(_bop, _fp)) ); } else { if( _bop=="!=" ) return; // Inverted, no code enc.add4(Arm64.b(Arm64.OP_UJMP, 0)); @@ -56,7 +58,7 @@ public class BranchARM extends IfNode implements MachNode, RIPRelSize { @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { assert !( in(1)==null && _bop=="!=" ); // Inverted never-node, no code no patch if( opLen==4 ) { - enc.patch4(opStart,Arm64.b_cond(Arm64.OP_BRANCH, delta, Arm64.make_condition(_bop))); + enc.patch4(opStart,Arm64.b_cond(Arm64.OP_BRANCH, delta, Arm64.make_condition(_bop, _fp))); } else { throw Utils.TODO(); } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java index b6d82f09..4de28f3a 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/CallARM.java @@ -14,8 +14,7 @@ public class CallARM extends CallNode implements MachNode, RIPRelSize { _inputs.pop(); // Pop constant target assert tfp.isConstant(); _tfp = tfp; - FunNode fun = CodeGen.CODE.link(tfp); - _name = fun==null ? CodeGen.CODE.externFunc(tfp) : fun._name; + _name = CodeGen.CODE.funcName(tfp.fidx()); } @Override public String op() { return "call"; } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/FloatARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/FltARM.java index dc21e075..db2540f7 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/FloatARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/FltARM.java @@ -4,21 +4,22 @@ import com.seaofnodes.isa.Arm64; import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.ConstantNode; import com.seaofnodes.simple.node.MachNode; -import com.seaofnodes.simple.type.TypeFloat; import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; //FMOV (scalar, immediate) //Floating-point move immediate. -public class FloatARM extends ConstantNode implements MachNode, RIPRelSize { - FloatARM(ConstantNode con) { super(con); } +public class FltARM extends ConstantNode implements MachNode, RIPRelSize { + final String _ext; + FltARM( ConstantNode con, String ext ) { super(con); _ext = ext; } @Override public String op() { return "ld8"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return arm.DMASK; } @Override public boolean isClone() { return true; } - @Override public FloatARM copy() { return new FloatARM(this); } + @Override public FltARM copy() { return new FltARM(this,_ext); } @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); enc.largeConstant(this,_con,0,-1/*TODO: ARM-style ELF patching*/); short dst = (short)(enc.reg(this) - arm.D_OFFSET); enc.add4(Arm64.load_pc(Arm64.OPF_ARM, 0, dst)); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java index 7a9e74c6..72b76edd 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/IntARM.java @@ -7,19 +7,22 @@ import com.seaofnodes.simple.node.MachNode; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; // Integer constants public class IntARM extends ConstantNode implements MachNode { - IntARM( ConstantNode con ) { super(con); } + final String _ext; + IntARM( ConstantNode con, String ext ) { super(con); _ext = ext; } @Override public String op() { return "ldi"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return arm.WMASK; } @Override public boolean isClone() { return true; } - @Override public IntARM copy() { return new IntARM(this); } + @Override public IntARM copy() { return new IntARM(this,_ext); } @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); short self = enc.reg(this); long x = _con==Type.NIL ? 0 : ((TypeInteger)_con).value(); Arm64.constant(enc,self,x); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java index 8fc22a67..53494092 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/LoadARM.java @@ -25,7 +25,7 @@ public class LoadARM extends MemOpARM { // ldr(immediate - unsigned offset) | ldr(register) @Override public void encoding( Encoding enc ) { - ldst_encode(enc, OP_LOADS[Integer.numberOfTrailingZeros(size())], OP_LOAD_RS[_declaredType.log_size()], this, size()); + ldst_encode(enc, OP_LOADS[Integer.numberOfTrailingZeros(size())], OP_LOAD_RS[_con.log_size()], this, size()); } @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(this)).p(","); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java index dfbe5e06..14eaf510 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/MemOpARM.java @@ -7,13 +7,13 @@ import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; import java.lang.StringBuilder; public abstract class MemOpARM extends MemOpNode implements MachNode { final int _off; // Limit 9 bits sized, or (13 bits<<logsize) unsigned final int _imm; // Limit ? bits - final char _sz = (char)('0'+(1<<_declaredType.log_size())); + final int _bytes; + final char _sz; MemOpARM(MemOpNode mop, Node ptr, Node idx, int off, int imm) { super(mop,mop); assert ptr._type instanceof TypeMemPtr && !(ptr instanceof AddNode); @@ -22,6 +22,8 @@ public abstract class MemOpARM extends MemOpNode implements MachNode { _inputs.setX(3, idx); _off = off; _imm = imm; + _bytes = mop instanceof StoreNode st ? st.storeSize() : 1<<_con.log_size(); + _sz = (char)('0'+_bytes); } // Store-based flavors have a value edge @@ -35,10 +37,10 @@ public abstract class MemOpARM extends MemOpNode implements MachNode { @Override protected ExprPrinter<Node> _printMach(ExprPrinter<Node> p) { return p.p(".").p(_name); } - @Override public Type compute() { throw Utils.TODO(); } - @Override public Node idealize() { throw Utils.TODO(); } + @Override public Type compute() { throw new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } - int size() { return 1<<_declaredType.log_size(); } + int size() { return _bytes; } // Wider mask to store both GPRs and FPRs @Override public RegMask regmap(int i) { diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java index 7110df62..8bb8983e 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/NewARM.java @@ -32,7 +32,8 @@ public class NewARM extends NewNode implements MachNode, RIPRelSize { } // General form: "alloc #bytes PC" @Override public void asm(CodeGen code, SB sb) { - sb.p("ldi x0=#1\n"); + sb.p("\n"); // Multi-op trigger + sb.p("ldi ").p(code._mach.regs()[_arg2Reg]).p(" = #1\n"); sb.p("call #calloc"); } } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java index afbbd3bf..e5b06c39 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/SetARM.java @@ -9,12 +9,14 @@ import com.seaofnodes.simple.util.SB; // e.g CSET, stores flag info into GPRS after compare public class SetARM extends MachConcreteNode implements MachNode { final String _bop; // One of <,<=,== - SetARM(Node cmp, String bop) { + final boolean _fp; // FP compare (unsigned encoding) + SetARM(Node cmp, String bop, boolean fp) { super(cmp); _inputs.setLen(1); // Pop the cmp inputs // Replace with the matched cmp _inputs.push(cmp); _bop = bop; + _fp = fp; } @Override public String op() { return "set"+_bop; } @Override public RegMask regmap(int i) { assert i==1; return arm.FLAGS_MASK; } @@ -22,7 +24,7 @@ public class SetARM extends MachConcreteNode implements MachNode { @Override public void encoding( Encoding enc ) { // Using RM==RN==31 implies a *zero* value, not R31. - int body = Arm64.cset(Arm64.OP_CSET,0b11111, Arm64.make_condition(_bop), 0b011111, enc.reg(this)); + int body = Arm64.cset(Arm64.OP_CSET,0b11111, Arm64.make_condition(_bop, _fp), 0b011111, enc.reg(this)); enc.add4(body); } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java index f93f0f24..dcfeb5d8 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitARM.java @@ -9,7 +9,7 @@ import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; public class SplitARM extends SplitNode { - SplitARM(String kind, byte round) { super(kind,round,new Node[2]);} + SplitARM(LRG lrg, String kind, byte round) { super(lrg,kind,round,new Node[2]);} @Override public RegMask regmap(int i) { return arm.SPLIT_MASK; } @Override public RegMask outregmap() { return arm.SPLIT_MASK; } @@ -25,7 +25,7 @@ public class SplitARM extends SplitNode { if(src >= arm.MAX_REG) { throw Utils.TODO(); } - int off = enc._fun.computeStackOffset(enc._code,dst); + int off = fun(enc).computeStackOffset(enc._code,dst); if( srcX ) src -= arm.D_OFFSET; enc.add4(Arm64.load_str_imm(Arm64.OP_STORE_IMM_64, off, arm.RSP, src, 8)); return; @@ -33,7 +33,7 @@ public class SplitARM extends SplitNode { if(src >= arm.MAX_REG) { // Load from SP - int off = enc._fun.computeStackOffset(enc._code,src); + int off = fun(enc).computeStackOffset(enc._code,src); if( dstX ) dst -= arm.D_OFFSET; enc.add4(Arm64.load_str_imm(Arm64.OP_LOAD_IMM_64, off, arm.RSP, dst, 8)); return; @@ -57,10 +57,4 @@ public class SplitARM extends SplitNode { enc.add4(Arm64.f_mov_general(Arm64.OP_FMOV, 0b01, 0, 0b110, src - arm.D_OFFSET, dst)); } } - - // General form: "mov dst = src" - @Override public void asm(CodeGen code, SB sb) { - FunNode fun = code._encoding==null ? null : code._encoding._fun; - sb.p(code.reg(this,fun)).p(" = ").p(code.reg(in(1),fun)); - } } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java index 3e045a6f..25e5d501 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitFlagsARM.java @@ -6,7 +6,7 @@ import com.seaofnodes.simple.codegen.*; // NZCV transfers require a GPR. Further splits can spill that GPR to the stack. public class SplitFlagsARM extends SplitARM { private static final RegMask MASK = new RegMask(arm.WR_BITS,1L); - SplitFlagsARM(String kind,byte round) { super(kind,round); } + SplitFlagsARM(LRG lrg,String kind,byte round) { super(lrg,kind,round); } @Override public boolean isNonStackSplit() { return true; } @Override public RegMask regmap(int i) { return MASK; } @Override public RegMask outregmap() { return MASK; } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java index 46eff4ca..c9b2127a 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/SplitGPRARM.java @@ -5,7 +5,7 @@ import com.seaofnodes.simple.codegen.*; // Keep a saved NZCV value in GPRs and stack slots through subsequent spills. public class SplitGPRARM extends SplitARM { private static final RegMask MASK = new RegMask(arm.WR_BITS,-2L); - SplitGPRARM(String kind,byte round) { super(kind,round); } + SplitGPRARM(LRG lrg,String kind,byte round) { super(lrg,kind,round); } @Override public RegMask regmap(int i) { return MASK; } @Override public RegMask outregmap() { return MASK; } } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java index 2cc54f76..0b90ac8b 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/StoreARM.java @@ -28,7 +28,7 @@ public class StoreARM extends MemOpARM { private static final int[] OP_STORE_RS = new int[]{ Arm64.OP_STORE_R_8, Arm64.OP_STORE_R_16, Arm64.OP_STORE_R_32, Arm64.OP_STORE_R_64, }; @Override public void encoding( Encoding enc ) { - ldst_encode(enc, OP_STORES[Integer.numberOfTrailingZeros(size())], OP_STORE_RS[_declaredType.log_size()], val(), size()); + ldst_encode(enc, OP_STORES[Integer.numberOfTrailingZeros(size())], OP_STORE_RS[Integer.numberOfTrailingZeros(_bytes)], val(), size()); } @Override public void asm(CodeGen code, SB sb) { asm_address(code,sb).p(","); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java index 2c41dc57..a053ba46 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/TFPARM.java @@ -2,28 +2,30 @@ package com.seaofnodes.simple.node.cpus.arm; import com.seaofnodes.isa.Arm64; import com.seaofnodes.simple.codegen.*; -import com.seaofnodes.simple.node.ConstantNode; -import com.seaofnodes.simple.node.MachNode; -import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.TypeFunPtr; import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; -public class TFPARM extends ConstantNode implements MachNode, RIPRelSize { - TFPARM( ConstantNode con ) { super(con); } +public class TFPARM extends FunPtrNode implements MachNode, RIPRelSize { + final String _ext; + // Pointer to a Simple function + TFPARM( FunPtrNode fptr ) { super(fptr); _ext = null; } + // Pointer to an Extern "C" function + TFPARM( TypeFunPtr fptr, String ext ) { super(fptr,CodeGen.CODE._start,null); _type = fptr; _ext = ext; } @Override public String op() { return "ldx"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return arm.WMASK; } @Override public boolean isClone() { return true; } - @Override public TFPARM copy() { return new TFPARM(this); } + @Override public TFPARM copy() { return new TFPARM((TypeFunPtr)_con,_ext); } @Override public void encoding( Encoding enc ) { - enc.relo(this); - short self = enc.reg(this); + if( _ext==null ) enc.relo(this); // Internal patch + else enc.external(this,_ext); // ELF-file external patch + short dst = enc.reg(this); // adrp x0, 0 - int adrp = Arm64.adrp(1,0, Arm64.OP_ADRP, 0,self); + enc.add4(Arm64.adrp(1,0, Arm64.OP_ADRP, 0,dst)); // add x0, x0, 0 - enc.add4(adrp); - arm.imm_inst(enc,Arm64.OPI_ADD,0, 0); + arm.imm_inst(enc,Arm64.OPI_ADD,0, dst); } @Override public byte encSize(int delta) { @@ -34,16 +36,7 @@ public class TFPARM extends ConstantNode implements MachNode, RIPRelSize { @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { short rpc = enc.reg(this); if(opLen == 8 ) { - // ARM encoding delta is from PC & 0xFFF - int target = opStart+delta; - int base = opStart & ~0xFFF; - delta = target-base; - int adrp_delta = delta >> 12; - // ADRP splits a signed page offset into two unsigned bit fields. - // Mask the high field too, including for an address below this PC. - enc.patch4(opStart, Arm64.adrp(1, adrp_delta & 0b11, 0b10000, (adrp_delta >> 2) & 0x7FFFF, rpc)); - // low 12 bits via add - enc.patch4(opStart+4, Arm64.imm_inst_l(Arm64.OPI_ADD, delta & 0xfff, rpc)); + arm.patch_adrp_add(enc, opStart, delta, rpc); } else { // should not happen as one instruction is 4 byte, and TFP arm encodes 2. throw Utils.TODO(); @@ -52,7 +45,7 @@ public class TFPARM extends ConstantNode implements MachNode, RIPRelSize { @Override public void asm(CodeGen code, SB sb) { String reg = code.reg(this); - _con.print(sb.p(reg).p(" #")); + _type.print(sb.p(reg).p(" #")); } @Override public boolean eq(Node n) { return this==n; } } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java index d1cafa96..b2531f97 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/TMPARM.java @@ -10,14 +10,16 @@ import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; public class TMPARM extends ConstantNode implements MachNode, RIPRelSize { - TMPARM( ConstantNode con ) { super(con); } + final String _ext; + TMPARM( ConstantNode con, String ext ) { super(con); _ext = ext; } @Override public String op() { return "ldp"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return arm.WMASK; } @Override public boolean isClone() { return true; } - @Override public TMPARM copy() { return new TMPARM(this); } + @Override public TMPARM copy() { return new TMPARM(this,_ext); } @Override public void encoding( Encoding enc ) { - enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1); + if( _ext!=null ) enc.externalData(this,_ext); + else enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1); short dst = enc.reg(this); // adrp x0, 0 enc.add4(Arm64.adrp(1,0, Arm64.OP_ADRP, 0,dst)); @@ -33,15 +35,7 @@ public class TMPARM extends ConstantNode implements MachNode, RIPRelSize { @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { short dst = enc.reg(this); if(opLen == 8 ) { - // ARM encoding delta is from PC & 0xFFF - int target = opStart+delta; - int base = opStart & ~0xFFF; - delta = target-base; - int adrp_delta = delta >> 12; - // patch upper 20 bits via adrp - enc.patch4(opStart, Arm64.adrp(1, adrp_delta & 0b11, 0b10000, adrp_delta >> 2, dst)); - // low 12 bits via add - enc.patch4(opStart+4, Arm64.imm_inst_l(Arm64.OPI_ADD, delta & 0xfff, dst)); + arm.patch_adrp_add(enc, opStart, delta, dst); } else { throw Utils.TODO(); } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java index f004b0b8..3c965da5 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java @@ -212,6 +212,14 @@ public class arm extends Machine { enc.add4(body); } + public static void patch_adrp_add(Encoding enc, int opStart, int delta, int rd) { + int target = opStart + delta; + int pageDelta = ((target & ~0xFFF) - (opStart & ~0xFFF)) >> 12; + int imm21 = pageDelta & 0x1FFFFF; + enc.patch4(opStart , Arm64.adrp(1, imm21 & 3, OP_ADRP, imm21 >>> 2, rd)); + enc.patch4(opStart+4, Arm64.imm_inst_l(OPI_ADD, target & 0xFFF, rd)); + } + public static void f_cmp(Encoding enc, Node n) { short reg1 = (short)(enc.reg(n.in(1))-D_OFFSET); short reg2 = (short)(enc.reg(n.in(2))-D_OFFSET); @@ -225,11 +233,16 @@ public class arm extends Machine { if( idx==1 ) return null; // Count floats in signature up to index if( idx-2 >= tfp.nargs() ) return null; // Anti-dependence - if( tfp.arg(idx-2) instanceof TypeMem ) return null; // Hidden private memory + boolean hidden = hiddenSelf(tfp); + int sigidx = idx-2; + if( hidden ) { + if( sigidx==0 ) return null; + sigidx--; + } // Count floats in signature up to index int fcnt=0; - for( int i=2; i<idx; i++ ) - if( tfp.arg(i-2) instanceof TypeFloat) + for( int i=hidden ? 1 : 0; i<idx-2; i++ ) + if( tfp.arg(i) instanceof TypeFloat) fcnt++; // Floats up to XMMS in XMM registers if( tfp.arg(idx-2) instanceof TypeFloat ) { @@ -237,24 +250,27 @@ public class arm extends Machine { return XMMS[fcnt]; } else { RegMask[] cargs = CALLINMASK; - if( idx-2-fcnt < cargs.length ) - return cargs[idx-2-fcnt]; + if( sigidx-fcnt < cargs.length ) + return cargs[sigidx-fcnt]; } // Pass on stack slot (8 and higher) if( maxArgSlot>0 ) throw Utils.TODO(); - return new RegMask(MAX_REG + 1 + (idx - 2)); + return new RegMask(MAX_REG + 1 + sigidx); } // Return the max stack slot used by this signature, or 0 @Override public short maxArgSlot( TypeFunPtr tfp ) { int icnt=0, fcnt=0; // Count of ints, floats - for( int i=0; i<tfp.nargs(); i++ ) { + for( int i=hiddenSelf(tfp) ? 1 : 0; i<tfp.nargs(); i++ ) { if( tfp.arg(i) instanceof TypeFloat ) fcnt++; else icnt++; } int nstk = Math.max(icnt-8,0)+Math.max(fcnt-8,0); return (short)nstk; } + private static boolean hiddenSelf(TypeFunPtr tfp) { + return tfp.nargs() > 0 && tfp.arg(0) == TypePtr.PTR; + } private static final long CALLEE_SAVE = 1L<<X19 | @@ -267,20 +283,20 @@ public class arm extends Machine { @Override public long callerSave() { return CALLER_SAVE; } @Override public long neverSave() { return 1L<<RSP; } @Override public RegMask retMask( TypeFunPtr tfp ) { - return tfp.ret() instanceof TypeMem ? null : tfp.ret() instanceof TypeFloat ? D0_MASK : X0_MASK; + return tfp.ret() instanceof TypeFloat ? D0_MASK : X0_MASK; } @Override public int rpc() { return X30; } // Create a split op; any register to any register, including stack slots - @Override public SplitNode split(String kind, byte round, LRG lrg) { + @Override public SplitNode split(LRG lrg, String kind, byte round) { RegMask def = lrg._machDef==null ? null : lrg._machDef.outregmap(), use = lrg.useMask(); if( (def!=null && def.size1() && def.test(FLAGS)) || (use!=null && use.size1() && use.test(FLAGS)) ) - return new SplitFlagsARM(kind,round); + return new SplitFlagsARM(lrg,kind,round); // Spilling the saved flags requires a GPR between NZCV and the stack. if( lrg._machDef instanceof SplitFlagsARM || lrg._machDef instanceof SplitGPRARM ) - return new SplitGPRARM(kind,round); - return new SplitARM(kind,round); + return new SplitGPRARM(lrg,kind,round); + return new SplitARM(lrg,kind,round); } // Return a MachNode unconditional branch @@ -291,30 +307,34 @@ public class arm extends Machine { // Instruction selection @Override public Node instSelect(Node n ) { return switch( n ) { - case AddFNode addf -> new AddFARM(addf); case AddNode add -> add(add); case AndNode and -> and(and); case BoolNode bool -> cmp(bool); case CallNode call -> call(call); - case CastNode cast -> new CastMach(cast); + case PtrToIntNode ptr -> new PtrToIntMach(ptr); + case GuardNode guard -> new GuardMach(guard); case CallEndNode cend-> new CallEndMach(cend); + case CLInitNode init -> new CLInitNode(init); case CProjNode c -> new CProjNode(c); case ConstantNode con-> con(con); - case DivFNode divf -> new DivFARM(divf); - case DivNode div -> new DivARM(div); + case DivNode div -> div.mode()==2 ? new DivFARM(div) : new DivARM(div); + case EscapeNode esc -> new EscapeNode(esc); case FunNode fun -> new FunARM(fun); + case FunPtrNode fptr -> fptr(fptr); case IfNode iff -> jmp(iff); case LoadNode ld -> ld(ld); case MemMergeNode mem-> new MemMergeNode(mem); - case MinusNode neg -> new NegARM(neg); - case MulFNode mulf -> new MulFARM(mulf); - case MulNode mul -> new MulARM(mul); + case MinusNode neg -> { + if( neg.mode()==2 ) throw Utils.TODO(); + yield new NegARM(neg); + } + case MulNode mul -> mul.mode()==2 ? new MulFARM(mul) : new MulARM(mul); case NewNode nnn -> new NewARM(nnn); case NotNode not -> new NotARM(not); case OrNode or -> or(or); case ParmNode parm -> new ParmARM(parm); case BulkMemPhiNode phi -> new BulkMemPhiNode(phi); - case MemPhiNode phi -> new MemPhiNode(phi); + case MemPhiNode phi -> new MemPhiNode(phi); case PhiNode phi -> new PhiNode(phi); case ProjNode prj -> prj(prj); case ReadOnlyNode read -> new ReadOnlyMach(read); @@ -322,11 +342,12 @@ public class arm extends Machine { case SarNode sar -> asr(sar); case ShlNode shl -> lsl(shl); case ShrNode shr -> lsr(shr); + case StartCUNode start -> new StartCUNode(start); + case StopCUNode stop -> new StopCUNode(stop); case StartNode start -> new StartNode(start); case StopNode stop -> new StopNode(stop); case StoreNode st -> st(st); - case SubFNode subf -> new SubFARM(subf); - case SubNode sub -> sub(sub); + case SubNode sub -> sub.mode()==2 ? new SubFARM(sub) : sub(sub); case ToFloatNode tfn -> new I2F8ARM(tfn); case ToIntegerNode cvt -> new F8ToIARM(cvt); case XorNode xor -> xor(xor); @@ -339,7 +360,7 @@ public class arm extends Machine { private Node cmp(BoolNode bool){ Node cmp = _cmp(bool); - return new SetARM(cmp, bool.op()); + return new SetARM(cmp, bool.op(), cmp instanceof CmpFARM); } private Node _cmp(BoolNode bool) { if( bool.isFloat() ) @@ -358,13 +379,18 @@ public class arm extends Machine { // Most general arith ops will also set flags, which the Jmp needs directly. // Loads do not set the flags, and will need an explicit TEST String op = "!="; - if( iff.in(1) instanceof BoolNode bool ) op = bool.op(); + boolean fp = false; + if( iff.in(1) instanceof BoolNode bool ) { op = bool.op(); fp = bool.isFloat(); } else if( iff.in(1)==null ) op = "=="; // Never-node cutout - else iff.setDef(1, new BoolNode.NE(iff.in(1), new ConstantNode(TypeInteger.ZERO),(byte)1)); - return new BranchARM(iff, op); + else iff.setDef(1, new BoolNode.NE(iff.in(1), + ConstantNode.raw(TypeInteger.ZERO), + (byte)(iff.in(1)._type instanceof TypeFloat ? 2 : 1))); + return new BranchARM(iff, op, fp); } private Node add(AddNode add) { + if( add.mode()==2 ) + return new AddFARM(add); return add.in(2) instanceof ConstantNode off && off._con instanceof TypeInteger ti && imm12(ti) ? new AddIARM(add, (int)ti.value()) : new AddARM(add); @@ -377,22 +403,30 @@ public class arm extends Machine { } private Node con( ConstantNode con ) { - if( !con._con.isConstant() ) return new ConstantNode( con ); // Default unknown caller inputs + if( !con._con.isConstant() ) + return ConstantNode.raw(con); // Default unknown caller inputs + String ext = con instanceof ExternNode ext0 ? ext0._extern : null; return switch( con._con ) { - case TypeInteger ti -> new IntARM(con); - case TypeFloat tf -> new FloatARM(con); - case TypeFunPtr tfp -> new TFPARM(con); - case TypeMemPtr tmp -> new TMPARM(con); + case TypeInteger ti -> new IntARM(con,ext); + case TypeFloat tf -> new FltARM(con,ext); + case TypeMemPtr tmp -> new TMPARM(con,ext); + case TypeFunPtr tfp -> new TFPARM(tfp,ext); case TypeNil tn -> throw Utils.TODO(); // TOP, BOTTOM, XCtrl, Ctrl, etc. Never any executable code. - case Type t -> t==Type.NIL ? new IntARM(con) : new ConstantNode(con); + case Type t -> t==Type.NIL ? new IntARM(con,ext) : ConstantNode.raw(con); }; } + private Node fptr( FunPtrNode con ) { + return new TFPARM(con); + } + private Node call(CallNode call){ - return call.fptr() instanceof ConstantNode con && con._con instanceof TypeFunPtr tfp - ? new CallARM(call, tfp) - : new CallRRARM(call); + return call.fptr() instanceof FunPtrNode fptr + ? new CallARM(call, (TypeFunPtr)fptr._type) + : call.fptr() instanceof ConstantNode con + ? new CallARM(call, (TypeFunPtr)con._type) + : new CallRRARM(call); } private Node or(OrNode or) { @@ -434,6 +468,13 @@ public class arm extends Machine { : new LsrARM(lsr); } + private Node prj(ProjNode prj) { + if( prj.in(0) instanceof CLInitNode ) return new ProjNode(prj); + TypeInteger ret = cReturn(prj); + if( ret!=null ) return new ExtendARM(prj,ret); + return prj.in(0) instanceof StartNode ? new ProjNode(prj) : new ProjARM(prj); + } + private static int off; private static Node idx; private Node st(StoreNode st) { @@ -459,9 +500,4 @@ public class arm extends Machine { return mop; } - private Node prj(ProjNode prj) { - TypeInteger ret = cReturn(prj); - return ret==null ? new ProjARM(prj) : new ExtendARM(prj,ret); - } - } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java index 14581111..59bad64e 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/AUIPC.java @@ -2,19 +2,19 @@ package com.seaofnodes.simple.node.cpus.riscv; import com.seaofnodes.isa.RiscV; import com.seaofnodes.simple.codegen.*; -import com.seaofnodes.simple.node.ConstantNode; import com.seaofnodes.simple.node.MachNode; +import com.seaofnodes.simple.node.FunPtrNode; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; // Add upper 20bits to PC. Immediate comes from the relocation info. -public class AUIPC extends ConstantNode implements MachNode, RIPRelSize { - AUIPC( Type tfp ) { super(tfp); } +public class AUIPC extends FunPtrNode implements MachNode, RIPRelSize { + AUIPC( FunPtrNode fptr ) { super(fptr); } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return riscv.WMASK; } @Override public boolean isClone() { return true; } - @Override public AUIPC copy() { return new AUIPC(_con); } + @Override public AUIPC copy() { return new AUIPC(this); } @Override public String op() { return "auipc"; } @Override public void encoding( Encoding enc ) { enc.relo(this); @@ -37,7 +37,6 @@ public class AUIPC extends ConstantNode implements MachNode, RIPRelSize { @Override public void asm(CodeGen code, SB sb) { String reg = code.reg(this); sb.p(reg).p(" = PC+#"); - if( _con == null ) sb.p("---"); - else _con.print(sb); + _type.print(sb); } } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java index 03bedd0d..b1426a3f 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRISC.java @@ -15,8 +15,7 @@ public class CallRISC extends CallNode implements MachNode, RIPRelSize { assert tfp.isConstant(); _inputs.pop(); // Pop constant target _tfp = tfp; - FunNode fun = CodeGen.CODE.link(tfp); - _name = fun==null ? CodeGen.CODE.externFunc(tfp) : fun._name; + _name = CodeGen.CODE.funcName(tfp.fidx()); } @Override public String op() { return "call"; } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRRISC.java index 5eee596d..cda4bc79 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/CallRRISC.java @@ -14,7 +14,7 @@ public class CallRRISC extends CallNode implements MachNode { ? riscv.RMASK // Function call target : riscv.callInMask(tfp(),i,fun()._maxArgSlot); // Normal argument } - @Override public RegMask outregmap() { return null; } + @Override public RegMask outregmap() { return riscv.RPC_MASK; } @Override public void encoding( Encoding enc ) { short rpc = enc.reg(this); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FltRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FltRISC.java index 51ddc9db..a110d156 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FltRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/FltRISC.java @@ -9,12 +9,13 @@ import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; public class FltRISC extends ConstantNode implements MachNode, RIPRelSize { - FltRISC(ConstantNode con) { super(con); } + final String _ext; + FltRISC( ConstantNode con, String ext ) { super(con); _ext = ext; } @Override public String op() { return "ld8"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return riscv.FMASK; } @Override public boolean isClone() { return true; } - @Override public FltRISC copy() { return new FltRISC(this); } + @Override public FltRISC copy() { return new FltRISC(this,_ext); } @Override public void encoding( Encoding enc ) { enc.largeConstant(this,_con, 0, -1); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java index 23ac8481..dd4a02a3 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/Int8RISC.java @@ -5,21 +5,23 @@ import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.ConstantNode; import com.seaofnodes.simple.node.MachNode; import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; // Special instruction for loading 8 byte constants from the constant pool. public class Int8RISC extends ConstantNode implements MachNode, RIPRelSize { - public Int8RISC(ConstantNode con) { super(con); } - + final String _ext; + Int8RISC( ConstantNode con, String ext ) { super(con); _ext = ext; } @Override public String op() { return "ld8"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return riscv.WMASK; } @Override public boolean isClone() { return true; } - @Override public Int8RISC copy() { return new Int8RISC(this); } + @Override public Int8RISC copy() { return new Int8RISC(this,_ext); } @Override public byte encSize(int delta) { return 8; } @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); short dst = enc.reg(this); short tmp = (short)riscv.T6; enc.largeConstant(this,_con, 0, -1); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java index 9088abbd..35d2bc65 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/IntRISC.java @@ -11,7 +11,7 @@ import com.seaofnodes.simple.util.SB; // 12-bit integer constant. Larger constants are made up in the instruction // selection by adding with a LUI. public class IntRISC extends ConstantNode implements MachNode { - public IntRISC(ConstantNode con) { super(con); } + IntRISC(ConstantNode con) { super(con); } @Override public String op() { return "ldi"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return riscv.WMASK; } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java index 2e3dfd8d..e9f3ec02 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/LoadRISC.java @@ -3,7 +3,9 @@ package com.seaofnodes.simple.node.cpus.riscv; import com.seaofnodes.isa.RiscV; import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; // Load memory addressing on RISC // Support imm, reg(direct), or reg+off(indirect) addressing @@ -22,6 +24,39 @@ public class LoadRISC extends MemOpRISC { if( dst >= riscv.F_OFFSET ) dst -= riscv.F_OFFSET; enc.add4(RiscV.i_type(op, dst, func3(), ptr, _off)); } + @Override int func3() { + int func3 = -1; + // no unsigned flavour for store, so both signed and unsigned trigger the same + Type declType = _con; + if( declType == TypeInteger. I8 ) func3=0; // LB + if( declType == TypeInteger.I16 ) func3=1; // LH + if( declType == TypeInteger.I32 ) func3=2; // LW + if( declType == TypeInteger.BOT ) func3=3; // LD + if( declType == TypeInteger. U8 ) func3=4; // LBU + if( declType == TypeInteger.BOOL) func3=4; // LBU + if( declType == TypeInteger.U16 ) func3=5; // LHU + if( declType == TypeInteger.U32 ) func3=6; // LWU + if( declType instanceof TypeInteger ti ) { + if( -128 <= ti._min && ti._max < 128 ) func3 = 0; // LB + } + + // float + if( declType == TypeFloat.F32) func3 = 2; // fLW fSW + if( declType == TypeFloat.F64) func3 = 3; // fLD fSD + + if( declType instanceof TypeMemPtr) func3 = 3; // 8 byte pointers (pick ld) + if( declType instanceof TypeFunPtr) func3 = 3; // 8 byte function pointers + if( declType instanceof TypeConAry ary ) + func3 = switch( ary.log_size() ) { + case 0 -> 4; // LBU + case 1 -> 5; // LHU + case 2 -> 2; // LW + case 3 -> 3; // LD + default -> throw Utils.TODO("RISC-V constant-array load for "+declType); + }; + if( func3 == -1 ) throw Utils.TODO("RISC-V load for "+declType); + return func3; + } @Override public void asm(CodeGen code, SB sb) { sb.p(code.reg(this)).p(","); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java index adf21848..e0bbc219 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/MemOpRISC.java @@ -11,6 +11,7 @@ import com.seaofnodes.simple.type.*; public abstract class MemOpRISC extends MemOpNode implements MachNode { final int _off; // Limit 12 bits final char _sz; + final int _bytes; MemOpRISC(MemOpNode mop, Node base, int off, Node val) { super(mop,mop); assert base._type instanceof TypeMemPtr; @@ -18,7 +19,8 @@ public abstract class MemOpRISC extends MemOpNode implements MachNode { _inputs.setX(3, null); // Never an index _inputs.setX(4, val ); _off = off; - _sz = (char)('0'+(1<<_declaredType.log_size())); + _bytes = mop instanceof StoreNode st ? st.storeSize() : 1<<_con.log_size(); + _sz = (char)('0'+_bytes); } @Override public String label() { return op(); } @@ -26,42 +28,11 @@ public abstract class MemOpRISC extends MemOpNode implements MachNode { @Override protected ExprPrinter<Node> _printMach(ExprPrinter<Node> p) { return p.p(".").p(_name); } - @Override public Type compute() { throw Utils.TODO(); } - @Override public Node idealize() { throw Utils.TODO(); } + @Override public Type compute() { throw new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } // func3 is based on load/store size and extend - int func3() { - int func3 = -1; - // no unsigned flavour for store, so both signed and unsigned trigger the same - if(this instanceof StoreRISC) { - if( _declaredType == TypeInteger. I8 || _declaredType == TypeInteger.U8 || _declaredType == TypeInteger.BOOL) func3=0; // SB - if( _declaredType == TypeInteger.I16 || _declaredType == TypeInteger.U16 ) func3=1; // SH - if( _declaredType == TypeInteger.I32 || _declaredType == TypeInteger.U32 ) func3=2; // SW - if( _declaredType instanceof TypeMemPtr) func3=3; // SD - if( _declaredType == TypeInteger.BOT ) func3=3; // SD - if( func3 == -1 ) throw Utils.TODO(); - return func3; - } - if( _declaredType == TypeInteger. I8 ) func3=0; // LB - if( _declaredType == TypeInteger.I16 ) func3=1; // LH - if( _declaredType == TypeInteger.I32 ) func3=2; // LW - if( _declaredType == TypeInteger.BOT ) func3=3; // LD - if( _declaredType == TypeInteger. U8 ) func3=4; // LBU - if( _declaredType == TypeInteger.BOOL) func3=4; // LBU - if( _declaredType == TypeInteger.U16 ) func3=5; // LHU - if( _declaredType == TypeInteger.U32 ) func3=6; // LWU - if( _declaredType instanceof TypeInteger ti ) { - if( -128 <= ti._min && ti._max < 128 ) func3 = 0; // LB - } - - // float - if( _declaredType == TypeFloat.F32) func3 = 2; // fLW fSW - if( _declaredType == TypeFloat.F64) func3 = 3; // fLD fSD - - if( _declaredType instanceof TypeMemPtr) func3 = 3; // 8 byte pointers (pick ld) - if( func3 == -1 ) throw Utils.TODO(); - return func3; - } + abstract int func3(); // Register mask allowed on input i. @Override public RegMask regmap(int i) { diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java index 965a3c7c..40d0359c 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/NewRISC.java @@ -41,8 +41,9 @@ public class NewRISC extends NewNode implements MachNode, RIPRelSize { // General form: "alloc #bytes PC" @Override public void asm(CodeGen code, SB sb) { - sb.p("ldi a1=#1\n"); + sb.p("\n"); // Multi-op trigger + sb.p("ldi ").p(code._mach.regs()[_arg2Reg]).p(" = #1\n"); sb.p("auipc a2=#calloc\n"); - sb.p("call a2+#calloc, a0"); + sb.p("call a2+#calloc"); } } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java index 8a16f6b7..d455337b 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/SplitRISC.java @@ -9,7 +9,7 @@ import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; public class SplitRISC extends SplitNode { - SplitRISC( String kind, byte round ) { super(kind,round, new Node[2]); } + SplitRISC( LRG lrg, String kind, byte round ) { super(lrg, kind,round, new Node[2]); } @Override public RegMask regmap(int i) { return riscv.SPLIT_MASK; } @Override public RegMask outregmap() { return riscv.SPLIT_MASK; } @Override public void encoding( Encoding enc ) { @@ -25,7 +25,7 @@ public class SplitRISC extends SplitNode { if( src >= riscv.MAX_REG ) { throw Utils.TODO(); // Very rare stack-stack move } - int off = enc._fun.computeStackOffset(enc._code,dst); + int off = fun(enc).computeStackOffset(enc._code,dst); int op = srcX ? RiscV.OP_STOREFP : RiscV.OP_STORE; if( srcX ) src -= riscv.F_OFFSET; enc.add4(RiscV.s_type(op, 0b011, riscv.RSP, src, off)); @@ -33,7 +33,7 @@ public class SplitRISC extends SplitNode { } if( src >= riscv.MAX_REG ) { // Load from SP - int off = enc._fun.computeStackOffset(enc._code,src); + int off = fun(enc).computeStackOffset(enc._code,src); int op = dstX ? RiscV.OP_LOADFP : RiscV.OP_LOAD; if( dstX ) dst -= riscv.F_OFFSET; enc.add4(RiscV.i_type(op, dst, 0b011, riscv.RSP, off)); @@ -58,10 +58,4 @@ public class SplitRISC extends SplitNode { enc.add4(RiscV.r_type(RiscV.OP_FP, dst, 0, src - riscv.F_OFFSET, 0, 0b1110001)); } } - - // General form: "mov dst = src" - @Override public void asm(CodeGen code, SB sb) { - FunNode fun = code._encoding==null ? null : code._encoding._fun; - sb.p(code.reg(this,fun)).p(" = ").p(code.reg(in(1),fun)); - } } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java index 48cb7c78..c3fcf4d3 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/StoreRISC.java @@ -4,6 +4,7 @@ import com.seaofnodes.isa.RiscV; import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.codegen.RegMask; import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; @@ -27,7 +28,7 @@ public class StoreRISC extends MemOpRISC { if( i==1 ) return null; // mem if( i==2 ) return riscv.RMASK; // ptr // 2 - index - if( i==4 ) return _sz >= '4' + if( i==4 ) return _bytes >= 4 ? riscv.MEM_MASK // Word/dword value in GPR or FPR : riscv.RMASK; // Byte/short stores require a GPR return null; // Anti-dependence @@ -42,6 +43,11 @@ public class StoreRISC extends MemOpRISC { enc.add4(RiscV.s_type(op, func3()&7, ptr, val == -1 ? riscv.ZERO : val, _off)); } + // func3 is based on load/store size and extend + @Override int func3() { + return Integer.numberOfTrailingZeros(_bytes); + } + @Override public void asm(CodeGen code, SB sb) { asm_address(code,sb).p(","); if( val()==null ) sb.p("#").p("0"); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java index e57f6e09..44ef1a95 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TFPRISC.java @@ -2,25 +2,27 @@ package com.seaofnodes.simple.node.cpus.riscv; import com.seaofnodes.isa.RiscV; import com.seaofnodes.simple.codegen.*; -import com.seaofnodes.simple.node.ConstantNode; -import com.seaofnodes.simple.node.MachNode; -import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.TypeFunPtr; import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; -public class TFPRISC extends ConstantNode implements MachNode, RIPRelSize { - TFPRISC(ConstantNode con) { super(con); } +public class TFPRISC extends FunPtrNode implements MachNode, RIPRelSize { + final String _ext; + // Pointer to a Simple function + TFPRISC( FunPtrNode fptr ) { super(fptr); _ext = null; } + // Pointer to an Extern "C" function + TFPRISC( TypeFunPtr fptr, String ext ) { super(fptr,CodeGen.CODE._start,null); _type = fptr; _ext = ext; } @Override public String op() { return "ldx"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return riscv.WMASK; } @Override public boolean isClone() { return true; } - @Override public TFPRISC copy() { return new TFPRISC(this); } + @Override public TFPRISC copy() { return new TFPRISC((TypeFunPtr)_con,_ext); } @Override public void encoding( Encoding enc ) { - enc.relo(this); + if( _ext==null ) enc.relo(this); // Internal patch + else enc.external(this,_ext); // ELF-file external patch // TODO: 1 op encoding, plus a TODO if it does not fit short dst = enc.reg(this); - TypeFunPtr tfp = (TypeFunPtr)_con; // auipc t0,0 enc.add4(RiscV.u_type(RiscV.OP_AUIPC, dst, 0)); // addi t1,t0 + #0 @@ -33,9 +35,14 @@ public class TFPRISC extends ConstantNode implements MachNode, RIPRelSize { @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { short rpc = enc.reg(this); if( opLen==8 ) { - // ADDI sign-extends its low 12 bits, so round the AUIPC high part. - enc.patch4(opStart ,RiscV.u_type(RiscV.OP_AUIPC, rpc, (delta+0x800)>>12)); - enc.patch4(opStart+4,RiscV.i_type(RiscV.OP_IMM, rpc, 0, rpc, delta & 0xFFF)); + int hi20 = (delta + 0x800) >> 12; + int lo12 = delta - (hi20 << 12); + // AUIPC (upper 20 bits) + // opstart of add + enc.patch4(opStart ,RiscV.u_type(RiscV.OP_AUIPC, rpc, hi20)); + // addi(low 12 bits) + enc.patch4(opStart+4,RiscV.i_type(RiscV.OP_IMM, rpc, 0, rpc, lo12 & 0xFFF)); + // addi } else { // should not happen as one instruction is 4 byte, and TFP arm encodes 2. throw Utils.TODO(); @@ -43,7 +50,7 @@ public class TFPRISC extends ConstantNode implements MachNode, RIPRelSize { } @Override public void asm(CodeGen code, SB sb) { - _con.print(sb.p(code.reg(this)).p(" #")); + _type.print(sb.p(code.reg(this)).p(" #")); } @Override public boolean eq(Node n) { return this==n; } } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java index 7bc2db7e..a5ff3753 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/TMPRISC.java @@ -9,14 +9,16 @@ import com.seaofnodes.simple.type.TypeMemPtr; import com.seaofnodes.simple.util.SB; public class TMPRISC extends ConstantNode implements MachNode, RIPRelSize { - TMPRISC(ConstantNode con) { super(con); } + final String _ext; + TMPRISC( ConstantNode con, String ext ) { super(con); _ext = ext; } @Override public String op() { return "ldp"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return riscv.WMASK; } @Override public boolean isClone() { return true; } - @Override public TMPRISC copy() { return new TMPRISC(this); } + @Override public TMPRISC copy() { return new TMPRISC(this,_ext); } @Override public void encoding( Encoding enc ) { - enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1); + if( _ext!=null ) enc.externalData(this,_ext); + else enc.largeConstant(this,((TypeMemPtr)_con)._obj,0,-1); short dst = enc.reg(this); // AUIPC dst,#hi20_constant_pool enc.add4(RiscV.u_type(RiscV.OP_AUIPC, dst, 0)); @@ -29,9 +31,9 @@ public class TMPRISC extends ConstantNode implements MachNode, RIPRelSize { // Delta is from opcode start @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { short dst = enc.reg(this); - // Round the high part because ADDI's low 12 bits are signed. + // AUIPC dst,#hi20_constant_pool enc.patch4(opStart , RiscV.u_type(RiscV.OP_AUIPC, dst, (delta+0x800)>>12)); - // ADDI dst,dst,#low12_constant_pool + // ADDI's low 12 bits are signed; round the AUIPC page accordingly. enc.patch4(opStart+4, RiscV.i_type(RiscV.OP_IMM, dst, 0, dst, delta & 0xFFF)); } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java index 0309bfcd..0195542c 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java @@ -15,7 +15,6 @@ public class riscv extends Machine { } @Override public String name() {return "riscv";} - //@Override public int maxReg() { return MAX_REG; } // Using ABI names instead of register names public static int ZERO = 0, RPC= 1, RSP= 2, S12= 3, S13= 4, T0 = 5, T1 = 6, T2 = 7; @@ -34,24 +33,24 @@ public class riscv extends Machine { public static final int F_OFFSET = 32; static final String[] REGS = new String[] { - "zero","rpc" , "rsp" , "s12" , "s13" , "t0" , "t1" , "t2" , - "s0" , "s1" , "a0" , "a1" , "a2" , "a3" , "a4" , "a5" , - "a6" , "a7" , "s2" , "s3" , "s4" , "s5" , "s6" , "s7" , - "s8" , "s9" , "s10" , "s11" , "t3" , "t4" , "t5" , "t6" , - "f0" , "f1" , "f2" , "f3" , "f4" , "f5" , "f6" , "f7" , - "fs0" , "fs1" , "fa0" , "fa1" , "fa2" , "fa3" , "fa4" , "fa5" , - "fa6" , "fa7" , "fs2" , "fs3" , "fs4" , "fs5" , "fs6" , "fs7" , - "fs8" , "fs9" , "fs10", "fs11", "ft8" , "ft9" , "ft10", "ft11" - // Register-based naming hella easier to debug than "official" register - // names when single-stepping in debugger. - //"zero", "r1" , "rsp" , "r3" , "r4" , "r5" , "r6" , "r7" , - //"r8" , "r9" , "r10" , "r11", "r12", "r13", "r14", "r15", - //"r16" , "r17" , "r18" , "r19", "r20", "r21", "r22", "r23", - //"r24" , "r25" , "r26" , "r27", "r28", "r29", "r30", "r31", + //"zero","rpc" , "rsp" , "s12" , "s13" , "t0" , "t1" , "t2" , + //"s0" , "s1" , "a0" , "a1" , "a2" , "a3" , "a4" , "a5" , + //"a6" , "a7" , "s2" , "s3" , "s4" , "s5" , "s6" , "s7" , + //"s8" , "s9" , "s10" , "s11" , "t3" , "t4" , "t5" , "t6" , //"f0" , "f1" , "f2" , "f3" , "f4" , "f5" , "f6" , "f7" , //"fs0" , "fs1" , "fa0" , "fa1" , "fa2" , "fa3" , "fa4" , "fa5" , //"fa6" , "fa7" , "fs2" , "fs3" , "fs4" , "fs5" , "fs6" , "fs7" , //"fs8" , "fs9" , "fs10", "fs11", "ft8" , "ft9" , "ft10", "ft11" + // Register-based naming hella easier to debug than "official" register + // names when single-stepping in debugger. + "zero", "r1" , "rsp" , "r3" , "r4" , "r5" , "r6" , "r7" , + "r8" , "r9" , "r10" , "r11", "r12", "r13", "r14", "r15", + "r16" , "r17" , "r18" , "r19", "r20", "r21", "r22", "r23", + "r24" , "r25" , "r26" , "r27", "r28", "r29", "r30", "r31", + "f0" , "f1" , "f2" , "f3" , "f4" , "f5" , "f6" , "f7" , + "fs0" , "fs1" , "fa0" , "fa1" , "fa2" , "fa3" , "fa4" , "fa5" , + "fa6" , "fa7" , "fs2" , "fs3" , "fs4" , "fs5" , "fs6" , "fs7" , + "fs8" , "fs9" , "fs10", "fs11", "ft8" , "ft9" , "ft10", "ft11" }; @Override public String[] regs() { return REGS; } @@ -92,7 +91,7 @@ public class riscv extends Machine { public static RegMask FA6_MASK = new RegMask(FA6); public static RegMask FA7_MASK = new RegMask(FA7); - // Int arguments calling conv + // Int/ptr arguments calling convention static RegMask[] CALLINMASK = new RegMask[] { A0_MASK, A1_MASK, @@ -138,11 +137,16 @@ public class riscv extends Machine { if( idx==0 ) return RPC_MASK; if( idx==1 ) return null; if( idx-2 >= tfp.nargs() ) return null; // Anti-dependence - if( tfp.arg(idx-2) instanceof TypeMem ) return null; // Hidden private memory + boolean hidden = hiddenSelf(tfp); + int sigidx = idx-2; + if( hidden ) { + if( sigidx==0 ) return null; + sigidx--; + } // Count floats in signature up to index int fcnt=0; - for( int i=2; i<idx; i++ ) - if( tfp.arg(i-2) instanceof TypeFloat ) + for( int i=hidden ? 1 : 0; i<idx-2; i++ ) + if( tfp.arg(i) instanceof TypeFloat ) fcnt++; // Floats up to XMMS in XMM registers if( tfp.arg(idx-2) instanceof TypeFloat ) { @@ -150,23 +154,26 @@ public class riscv extends Machine { return XMMS[fcnt]; } else { RegMask[] cargs = CALLINMASK; - if( idx-2-fcnt < cargs.length ) - return cargs[idx-2-fcnt]; + if( sigidx-fcnt < cargs.length ) + return cargs[sigidx-fcnt]; } // Pass on stack slot(8 and higher) if( maxArgSlot>0 ) throw Utils.TODO(); - return new RegMask(MAX_REG + 1 + (idx - 2)); + return new RegMask(MAX_REG + 1 + sigidx); } @Override public short maxArgSlot( TypeFunPtr tfp ) { int icnt=0, fcnt=0; // Count of ints, floats - for( int i=0; i<tfp.nargs(); i++ ) { + for( int i=hiddenSelf(tfp) ? 1 : 0; i<tfp.nargs(); i++ ) { if( tfp.arg(i) instanceof TypeFloat ) fcnt++; else icnt++; } int nstk = Math.max(icnt-8,0)+Math.max(fcnt-8,0); return (short)nstk; } + private static boolean hiddenSelf(TypeFunPtr tfp) { + return tfp.nargs() > 0 && tfp.arg(0) == TypePtr.PTR; + } static final long CALLER_SAVE = (1L<<RPC) | @@ -179,7 +186,7 @@ public class riscv extends Machine { @Override public long callerSave() { return CALLER_SAVE; } @Override public long neverSave() { return (1L<<RSP) | (1L<<ZERO); } @Override public RegMask retMask( TypeFunPtr tfp ) { - return tfp.ret() instanceof TypeMem ? null : tfp.ret() instanceof TypeFloat ? FA0_MASK : A0_MASK; + return tfp.ret() instanceof TypeFloat ? FA0_MASK : A0_MASK; } @Override public int rpc() { return RPC; } @@ -187,7 +194,7 @@ public class riscv extends Machine { @Override public CFGNode jump() { return new UJmpRISC(); } // Create a split op; any register to any register, including stack slots - @Override public SplitNode split(String kind, byte round, LRG lrg) { return new SplitRISC(kind,round); } + @Override public SplitNode split(LRG lrg, String kind, byte round ) { return new SplitRISC(lrg, kind,round); } // True if signed 12-bit immediate public static boolean imm12(TypeInteger ti) { @@ -204,30 +211,34 @@ public class riscv extends Machine { @Override public Node instSelect( Node n ) { return switch (n) { - case AddFNode addf -> addf(addf); case AddNode add -> add(add); case AndNode and -> and(and); case BoolNode bool -> cmp(bool); case CallNode call -> call(call); - case CastNode cast -> new CastMach(cast); + case PtrToIntNode ptr -> new PtrToIntMach(ptr); + case GuardNode guard -> new GuardMach(guard); case CallEndNode cend -> new CallEndMach(cend); + case CLInitNode init -> new CLInitNode(init); case CProjNode c -> new CProjNode(c); case ConstantNode con -> con(con); - case DivFNode divf -> new DivFRISC(divf); - case DivNode div -> new DivRISC(div); + case DivNode div -> div.mode()==2 ? new DivFRISC(div) : new DivRISC(div); + case EscapeNode esc -> new EscapeNode(esc); case FunNode fun -> new FunRISC(fun); + case FunPtrNode fptr -> fptr(fptr); case IfNode iff -> jmp(iff); case LoadNode ld -> ld(ld); case MemMergeNode mem -> new MemMergeNode(mem); - case MinusNode neg -> new NegRISC(neg); - case MulFNode mulf -> new MulFRISC(mulf); - case MulNode mul -> new MulRISC(mul); + case MinusNode neg -> { + if( neg.mode()==2 ) throw Utils.TODO(); + yield new NegRISC(neg); + } + case MulNode mul -> mul.mode()==2 ? new MulFRISC(mul) : new MulRISC(mul); case NewNode nnn -> nnn(nnn); case NotNode not -> new NotRISC(not); case OrNode or -> or(or); case ParmNode parm -> new ParmRISC(parm); - case BulkMemPhiNode phi -> new BulkMemPhiNode(phi); - case MemPhiNode phi -> new MemPhiNode(phi); + case BulkMemPhiNode phi-> new BulkMemPhiNode(phi); + case MemPhiNode phi -> new MemPhiNode(phi); case PhiNode phi -> new PhiNode(phi); case ProjNode prj -> prj(prj); case ReadOnlyNode read-> new ReadOnlyMach(read); @@ -235,11 +246,12 @@ public class riscv extends Machine { case SarNode sar -> sra(sar); case ShlNode shl -> sll(shl); case ShrNode shr -> srl(shr); + case StartCUNode start -> new StartCUNode(start); + case StopCUNode stop -> new StopCUNode(stop); case StartNode start -> new StartNode(start); case StopNode stop -> new StopNode(stop); case StoreNode st -> st(st); - case SubFNode subf -> new SubFRISC(subf); - case SubNode sub -> sub(sub); + case SubNode sub -> sub.mode()==2 ? new SubFRISC(sub) : sub(sub); case ToFloatNode tfn -> i2f8(tfn); case ToIntegerNode cvt -> new F8ToIRISC(cvt); case XorNode xor -> xor(xor); @@ -250,11 +262,9 @@ public class riscv extends Machine { }; } - private Node addf(AddFNode addf) { - return new AddFRISC(addf); - } - private Node add(AddNode add) { + if( add.mode()==2 ) + return new AddFRISC(add); if( add.in(2) instanceof ConstantNode off2 && off2._con instanceof TypeInteger ti && imm12(ti) ) return new AddIRISC(add, (int)ti.value(),true); return new AddRISC(add); @@ -272,8 +282,10 @@ public class riscv extends Machine { } private Node call(CallNode call) { - return call.fptr() instanceof ConstantNode con && con._con instanceof TypeFunPtr tfp - ? new CallRISC(call, tfp) + return call.fptr() instanceof FunPtrNode fptr + ? new CallRISC(call, (TypeFunPtr)fptr._type) + : call.fptr() instanceof ConstantNode con + ? new CallRISC(call, (TypeFunPtr)con._type) : new CallRRISC(call); } private Node nnn(NewNode nnn) { @@ -312,9 +324,13 @@ public class riscv extends Machine { } private Node con( ConstantNode con ) { - if( !con._con.isConstant() ) return new ConstantNode( con ); // Default unknown caller inputs + if( !con._con.isConstant() ) + return ConstantNode.raw(con); // Default unknown caller inputs + String ext = con instanceof ExternNode ext0 ? ext0._extern : null; return switch( con._con ) { case TypeInteger ti -> { + // External; without knowing the size assume we have to load something + if( ext!=null ) yield new Int8RISC(con,ext); if( imm12(ti) ) yield new IntRISC(con); long x = ti.value(); if( imm20Exact(ti) ) yield new LUI((int)x); @@ -327,18 +343,22 @@ public class riscv extends Machine { } // Need more complex sequence for larger constants... or a load // from a constant pool, which does not need an extra register - yield new Int8RISC(con); + yield new Int8RISC(con,null); } // Load from constant pool - case TypeFloat tf -> new FltRISC(con); - case TypeFunPtr tfp -> new TFPRISC(con); - case TypeMemPtr tmp -> new TMPRISC(con); + case TypeFloat tf -> new FltRISC(con,ext); + case TypeMemPtr tmp -> new TMPRISC(con,ext); + case TypeFunPtr tfp -> new TFPRISC(tfp,ext); case TypeNil tn -> throw Utils.TODO(); // TOP, BOTTOM, XCtrl, Ctrl, etc. Never any executable code. - case Type t -> t==Type.NIL ? new IntRISC(con) : new ConstantNode(con); + case Type t -> t==Type.NIL ? new IntRISC(con) : ConstantNode.raw(con); }; } + private Node fptr( FunPtrNode con ) { + return new TFPRISC(con); + } + private Node jmp( IfNode iff ) { if( iff.in(1) instanceof BoolNode bool && !bool.isFloat() ) { // if less than or equal switch inputs @@ -393,9 +413,10 @@ public class riscv extends Machine { } private Node prj(ProjNode prj) { + if( prj.in(0) instanceof CLInitNode ) return new ProjNode(prj); TypeInteger ret = cReturn(prj); if( ret!=null ) return new ExtendRISC(prj,ret); - return new ProjRISC(prj); + return prj.in(0) instanceof StartNode ? new ProjNode(prj) : new ProjRISC(prj); } private Node ld(LoadNode ld) { diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java index 974f690f..74af9266 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/AddFMemX86.java @@ -7,7 +7,7 @@ import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; public class AddFMemX86 extends MemOpX86 { - AddFMemX86( AddFNode add, LoadNode ld , Node base, Node idx, int off, int scale, Node val ) { + AddFMemX86( AddNode add, LoadNode ld , Node base, Node idx, int off, int scale, Node val ) { super(add,ld, base, idx, off, scale, 0, val ); } @Override public String op() { return "addf"+_sz; } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java index cd4d2452..6bafbf37 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CallX86.java @@ -9,21 +9,27 @@ import com.seaofnodes.simple.util.SB; public class CallX86 extends CallNode implements MachNode, RIPRelSize { final TypeFunPtr _tfp; final String _name; + final boolean _extern; CallX86( CallNode call, TypeFunPtr tfp ) { super(call); _inputs.pop(); // Pop constant target assert tfp.isConstant(); _tfp = tfp; - FunNode fun = CodeGen.CODE.link(tfp); - _name = fun==null ? CodeGen.CODE.externFunc(tfp) : fun._name; + _name = CodeGen.CODE.funcName(tfp.fidx()); + _extern = CodeGen.CODE.externFunc(tfp.fidx()) != null; } @Override public String op() { return "call"; } @Override public String label() { return op(); } @Override public String name() { return _name; } @Override public TypeFunPtr tfp() { return _tfp; } - @Override public RegMask regmap(int i) { return x86_64_v2.callInMask(_tfp,i,fun()._maxArgSlot); } + @Override public RegMask regmap(int i) { + return _extern + ? x86_64_v2.callInMask(_tfp,i,fun()._maxArgSlot,CodeGen.CODE._cCallingConv) + : x86_64_v2.callInMask(_tfp,i,fun()._maxArgSlot); + } @Override public RegMask outregmap() { return null; } @Override public int nargs() { return nIns()-2; } // Minus control, memory, fptr + @Override public boolean external() { return _extern; } @Override public void encoding( Encoding enc ) { FunNode fun = CodeGen.CODE.link(_tfp); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java index 7b7d98d5..4014045b 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/CmpMemX86.java @@ -20,8 +20,8 @@ public class CmpMemX86 extends MemOpX86 { short ptr=enc.reg(ptr()), idx=enc.reg(idx()), src=enc.reg(val()); if( src==-1 ) { assert !_swap; - X86.cmpImm(enc,_declaredType.log_size(),_imm,ptr,idx,_off,_scale); - } else encVal(enc,_declaredType,ptr,idx,src,_off,_scale,_swap); + X86.cmpImm(enc,_con.log_size(),_imm,ptr,idx,_off,_scale); + } else encVal(enc,_con,ptr,idx,src,_off,_scale,_swap); } static void encVal(Encoding enc, Type decl, short ptr, short idx, short src, int off, int scale, boolean _swap) { diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java index da9bdce7..59f06341 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FltX86.java @@ -4,17 +4,20 @@ import com.seaofnodes.isa.X86; import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; public class FltX86 extends ConstantNode implements MachNode, RIPRelSize{ private byte _encSize; - FltX86(ConstantNode con ) { super(con); } + final String _ext; + FltX86( ConstantNode con, String ext ) { super(con); _ext = ext; } @Override public String op() { return "ld8"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return x86_64_v2.XMASK; } @Override public boolean isClone() { return true; } - @Override public Node copy() { return new FltX86(this); } + @Override public Node copy() { return new FltX86(this,_ext); } @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); _encSize=X86.rip(enc,0xF2,0x0F10,false,enc.reg(this)-x86_64_v2.XMM_OFFSET); enc.largeConstant(this,_con,_encSize-4,2/*ELF PC32*/); } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java index c7a20e4d..0e44aee7 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/FunX86.java @@ -11,6 +11,7 @@ public class FunX86 extends FunNode implements MachNode { FunX86( FunNode fun ) { super(fun); } @Override public void postSelect(CodeGen code) { super.postSelect(code); + _maxArgSlot = code._mach.maxArgSlot(sig(),code._callingConv); // One more for RPC pre-pushed; X86 special _maxArgSlot++; } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java index 656b1543..2ffaa3d1 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/IntX86.java @@ -6,15 +6,17 @@ import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; // Integer constants public class IntX86 extends ConstantNode implements MachNode { - IntX86( ConstantNode con ) { super(con); } + final String _ext; + IntX86( ConstantNode con, String ext ) { super(con); _ext = ext; } @Override public String op() { return _con == Type.NIL || _con == TypeInteger.ZERO ? "xor" : "ldi"; } @Override public boolean isClone() { return true; } - @Override public Node copy() { return new IntX86(this); } + @Override public Node copy() { return new IntX86(this,_ext); } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return x86_64_v2.WMASK; } // Zero-set uses XOR kills flags @@ -23,6 +25,7 @@ public class IntX86 extends ConstantNode implements MachNode { } @Override public void encoding( Encoding enc ) { + if( _ext!=null ) throw Utils.TODO(); X86.constant(enc,enc.reg(this),_con==Type.NIL ? 0 : ((TypeInteger)_con).value()); } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java index d4f1413e..5c472dbe 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/LoadX86.java @@ -13,7 +13,7 @@ public class LoadX86 extends MemOpX86 { super(ld,ld, base, idx, off, scale, 0); } @Override public String op() { return "ld"+_sz; } - @Override public RegMask outregmap() { return x86_64_v2.MEM_MASK; } + @Override public RegMask outregmap() { return _con instanceof TypeFloat ? x86_64_v2.XMASK : x86_64_v2.WMASK; } @Override public void encoding( Encoding enc ) { // REX.W + 8B /r MOV r64, r/m64 // Zero extension for u8, u16 and u32 but sign extension i8, i16, i32 @@ -21,7 +21,7 @@ public class LoadX86 extends MemOpX86 { short dst = enc.reg(this ); short ptr = enc.reg(ptr()); short idx = enc.reg(idx()); - enc(enc, _declaredType, dst, ptr, idx, _off, _scale); + enc(enc, _con, dst, ptr, idx, _off, _scale); } static void enc( Encoding enc, Type decl, short dst, short ptr, short idx, int off, int scale ) { diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java index 309e62d9..fcca7f8f 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/MemOpX86.java @@ -6,7 +6,6 @@ import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; import java.lang.StringBuilder; // Generic X86 memory operand base. @@ -42,7 +41,8 @@ public abstract class MemOpX86 extends MemOpNode implements MachNode { _off = off; _scale = scale; _imm = imm; - _sz = (char)('0'+(1<<_declaredType.log_size())); + int size = mop instanceof StoreNode st ? st.storeSize() : 1<<_con.log_size(); + _sz = (char)('0'+size); } // Store-based flavors have a value edge @@ -59,8 +59,8 @@ public abstract class MemOpX86 extends MemOpNode implements MachNode { } @Override public String label() { return op(); } - @Override public Type compute() { throw Utils.TODO(); } - @Override public Node idealize() { throw Utils.TODO(); } + @Override public Type compute() { throw new AssertionError("Do not call compute on "+getClass().getSimpleName()); } + @Override public Node idealize() { throw new AssertionError("Do not call idealize on "+getClass().getSimpleName()); } // Register mask allowed on input i. @Override public RegMask regmap(int i) { diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java index 1cfc1c42..d6a06c2f 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/NewX86.java @@ -16,7 +16,8 @@ public class NewX86 extends NewNode implements MachNode { } // General form: "alloc #bytes PC" @Override public void asm(CodeGen code, SB sb) { - sb.p("ldi rcx = #1\n"); + sb.p("\n"); // Multi-op trigger + sb.p("ldi ").p(code._mach.regs()[_arg2Reg]).p(" = #1\n"); sb.p("call #calloc"); } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java index 4650b535..cb7694f9 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitFlagsX86.java @@ -6,7 +6,7 @@ import com.seaofnodes.simple.codegen.*; // Flag snapshots can use GPRs or stack slots, but never XMM registers. public class SplitFlagsX86 extends SplitX86 { private static final RegMask MASK = new RegMask(x86_64_v2.WR_BITS | (1L<<x86_64_v2.FLAGS) | x86_64_v2.SPILLS,-1L); - SplitFlagsX86(String kind,byte round) { super(kind,round); } + SplitFlagsX86(LRG lrg,String kind,byte round) { super(lrg,kind,round); } @Override public RegMask regmap(int i) { return MASK; } @Override public RegMask outregmap() { return MASK; } @@ -17,7 +17,7 @@ public class SplitFlagsX86 extends SplitX86 { boolean toFlags = dst==x86_64_v2.FLAGS; int reg = toFlags ? src : dst; if( reg>=x86_64_v2.MAX_REG ) - X86.flagsStack(enc,enc._fun.computeStackOffset(enc._code,reg),toFlags); + X86.flagsStack(enc,fun(enc).computeStackOffset(enc._code,reg),toFlags); else X86.flags(enc,reg,toFlags); } else super.encoding(enc); diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java index 4e9eda7d..46f2cb16 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/SplitX86.java @@ -12,22 +12,21 @@ import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; public class SplitX86 extends SplitNode { - SplitX86( String kind, byte round ) { super(kind,round, new Node[2]); } - @Override public String op() { return "mov"; } + SplitX86( LRG lrg, String kind, byte round ) { super(lrg,kind,round, new Node[2]); } @Override public RegMask regmap(int i) { return x86_64_v2.SPLIT_MASK; } @Override public RegMask outregmap() { return x86_64_v2.SPLIT_MASK; } - // Ordinary GPR, XMM, and stack moves. Flags use SplitFlagsX86. + // Moves among GPRs, XMM registers, and stack slots. @Override public void encoding( Encoding enc ) { short dst=enc.reg(this), src=enc.reg(in(1)); assert dst!=x86_64_v2.FLAGS && src!=x86_64_v2.FLAGS; boolean dstX=dst>=x86_64_v2.XMM_OFFSET, srcX=src>=x86_64_v2.XMM_OFFSET; // Stack spills if( dst >= x86_64_v2.MAX_REG ) { - int off = enc._fun.computeStackOffset(enc._code,dst); + int off = fun(enc).computeStackOffset(enc._code,dst); if( src >= x86_64_v2.MAX_REG ) { // Rare stack-stack move. push [RSP+soff]; pop [RSP+doff] - int soff = enc._fun.computeStackOffset(enc._code,src); + int soff = fun(enc).computeStackOffset(enc._code,src); X86.stackCopy(enc,soff,off); return; } @@ -35,7 +34,7 @@ public class SplitX86 extends SplitNode { return; } if( src >= x86_64_v2.MAX_REG ) { - int off = enc._fun.computeStackOffset(enc._code,src); + int off = fun(enc).computeStackOffset(enc._code,src); LoadX86.enc(enc, dstX ? TypeFloat.F64 : TypeInteger.BOT, dst, (short)x86_64_v2.RSP, (short)-1, off, 0); return; } @@ -43,10 +42,4 @@ public class SplitX86 extends SplitNode { X86.move(enc,dstX,srcX,dstX ? dst-x86_64_v2.XMM_OFFSET : dst, srcX ? src-x86_64_v2.XMM_OFFSET : src); } - - // General form: "mov dst = src" - @Override public void asm(CodeGen code, SB sb) { - FunNode fun = code._encoding==null ? null : code._encoding._fun; - sb.p(code.reg(this,fun)).p(" = ").p(code.reg(in(1),fun)); - } } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java index c83dfd44..9ffa25fc 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/StoreX86.java @@ -27,8 +27,8 @@ public class StoreX86 extends MemOpX86 { @Override public RegMask outregmap() { return null; } @Override public void encoding( Encoding enc ) { short ptr=enc.reg(ptr()), idx=enc.reg(idx()), src=enc.reg(val()); - if( src==-1 ) X86.storeImm(enc,_declaredType.log_size(),_imm,ptr,idx,_off,_scale); - else encVal(enc,_declaredType,ptr,idx,src,_off,_scale); + if( src==-1 ) X86.storeImm(enc,Integer.numberOfTrailingZeros(_sz-'0'),_imm,ptr,idx,_off,_scale); + else encVal(enc,_con,ptr,idx,src,_off,_scale); } // Non-immediate encoding diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java index fff2fb04..e3563713 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TFPX86.java @@ -3,39 +3,39 @@ package com.seaofnodes.simple.node.cpus.x86_64_v2; import com.seaofnodes.isa.X86; import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.node.cpus.riscv.riscv; import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.type.TypeFunPtr; import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.Utils; // Function constants -public class TFPX86 extends ConstantNode implements MachNode, RIPRelSize { - TFPX86( ConstantNode con ) { super(con); } - @Override public String op() { - return _con == Type.NIL ? "xor" : "ldx"; - } +public class TFPX86 extends FunPtrNode implements MachNode, RIPRelSize { + private byte _opLen; + final String _ext; // External name + // Pointer to a Simple function + TFPX86( FunPtrNode fptr ) { this((TypeFunPtr)fptr._type,null); } + // Spill clone: copy inputs without registering their reverse edges; + // insertBefore will place and register the clone in its use block. + private TFPX86( TFPX86 fptr ) { super(fptr); _ext = fptr._ext; } + // Pointer to an Extern "C" function + TFPX86( TypeFunPtr fptr, String ext ) { super(fptr,CodeGen.CODE._start,null); _type = fptr; _ext = ext; } + @Override public String op() { return "ldx"; } + @Override public String label() { return op(); } @Override public boolean isClone() { return true; } - @Override public Node copy() { return new TFPX86(this); } + @Override public TFPX86 copy() { return new TFPX86(this); } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return x86_64_v2.WMASK; } - // Zero-set uses XOR kills flags - @Override public RegMask killmap() { - return _con == Type.NIL ? x86_64_v2.FLAGS_MASK : null; - } - @Override public void encoding( Encoding enc ) { - if( _con==Type.NIL ) { X86.constant(enc,enc.reg(this),0); return; } - enc.relo(this); - X86.rip(enc,0,0x8D,true,enc.reg(this)); + if( _ext==null ) enc.relo(this); + else enc.external(this,_ext); + _opLen=X86.rip(enc,0,0x8D,true,enc.reg(this)); } - @Override public byte encSize(int delta) { - return 7; - } + @Override public byte encSize(int delta) { return _opLen; } - // Delta is from opcode start + // Delta is from opcode start, X86 expects it from opcode end @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { - enc.patch4(opStart + 3, delta - 7); + enc.patch4(opStart + opLen - 4, delta - opLen); } // Human-readable form appended to the SB. Things like the encoding, @@ -43,11 +43,9 @@ public class TFPX86 extends ConstantNode implements MachNode, RIPRelSize { // Just something like "ld4\tR17=[R18+12] // Load array base". // General form: "op\tdst=src+src" @Override public void asm(CodeGen code, SB sb) { - String reg = code.reg(this); - if( _con == Type.NIL ) - sb.p(reg).p(",").p(reg); - else - _con.print(sb.p(reg).p(" = #")); + sb.p(code.reg(this)); + if( _ext == null ) _type.print(sb.p(" #")); + else sb.p(_ext); } @Override public boolean eq(Node n) { return this==n; } } diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java index c78ac6de..682597f4 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/TMPX86.java @@ -7,30 +7,29 @@ import com.seaofnodes.simple.type.TypeMemPtr; import com.seaofnodes.simple.util.SB; public class TMPX86 extends ConstantNode implements MachNode, RIPRelSize{ - TMPX86(ConstantNode con ) { super(con); } + private byte _opLen; + final String _ext; + TMPX86( ConstantNode con, String ext ) { super(con); _ext = ext; } @Override public String op() { return "ldp"; } @Override public RegMask regmap(int i) { return null; } @Override public RegMask outregmap() { return x86_64_v2.WMASK; } @Override public boolean isClone() { return true; } - @Override public Node copy() { return new TMPX86(this); } + @Override public Node copy() { return new TMPX86(this,_ext); } @Override public void encoding( Encoding enc ) { short dst = enc.reg(this); - X86.rexF(dst, 0, 0, true, enc); - enc.add1(0x8D).add1(X86.modrm(X86.MOD.INDIRECT, dst, 0b101)).add4(0); - int opLen = 1/*rexF*/+1/*opcode 0x8D*/+1/*modrm*/+4/*pc rel offset*/; - // Base of constant array - TypeMemPtr tmp = (TypeMemPtr)_con; - int baseOff = tmp._obj.aryBase(); - enc.largeConstant(this,tmp._obj,opLen-baseOff,2/*ELF encoding PC32*/); + // 0 or 1 for REX depending on the dst. + _opLen = X86.rip(enc,0,0x8D,true,dst); + if( _ext!=null ) enc.externalData(this,_ext); + else enc.largeConstant(this,((TypeMemPtr)_con)._obj, _opLen-4, 2/*ELF encoding PC32*/); } // Delta is from opcode start - @Override public byte encSize(int delta) { return (byte)7; } + @Override public byte encSize(int delta) { return _opLen; } // Delta is from opcode start @Override public void patch( Encoding enc, int opStart, int opLen, int delta ) { - enc.patch4(opStart+opLen-4,delta-opLen); + enc.patch4(opStart + opLen - 4, delta - opLen); } @Override public void asm(CodeGen code, SB sb) { diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java index c854dd9d..3dc93bf6 100644 --- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java +++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java @@ -97,23 +97,29 @@ public class x86_64_v2 extends Machine { // Map from function signature and argument index to register. // Used to set input registers to CallNodes, and ParmNode outputs. @Override public RegMask callArgMask( TypeFunPtr tfp, int idx, int maxArgSlot ) { return callInMask(tfp,idx,maxArgSlot); } + @Override public RegMask callArgMask( TypeFunPtr tfp, int idx, int maxArgSlot, String conv ) { return callInMask(tfp,idx,maxArgSlot,conv); } static RegMask callInMask( TypeFunPtr tfp, int idx, int maxArgSlot ) { + return callInMask(tfp,idx,maxArgSlot,CodeGen.CODE._callingConv); + } + static RegMask callInMask( TypeFunPtr tfp, int idx, int maxArgSlot, String conv ) { if( idx==0 ) return RPC_MASK; if( idx==1 ) return null; if( idx-2 >= tfp.nargs() ) return null; // Anti-dependence - if( tfp.arg(idx-2) instanceof TypeMem ) return null; // Hidden private memory - return switch( CodeGen.CODE._callingConv ) { + return switch( conv ) { case "SystemV" -> callSys5 (tfp,idx,maxArgSlot); - case "Win64" -> callWin64(tfp,idx,maxArgSlot); + case "win64" -> callWin64(tfp,idx,maxArgSlot); default -> throw Utils.TODO(); }; } // Maximum stack args used by this signature @Override public short maxArgSlot( TypeFunPtr tfp ) { - return switch( CodeGen.CODE._callingConv ) { + return maxArgSlot(tfp,CodeGen.CODE._callingConv); + } + @Override public short maxArgSlot( TypeFunPtr tfp, String conv ) { + return switch( conv ) { case "SystemV" -> maxArgSlotSys5 (tfp); - case "Win64" -> maxArgSlotWin64(tfp); + case "win64" -> maxArgSlotWin64(tfp); default -> throw Utils.TODO(); }; } @@ -146,14 +152,22 @@ public class x86_64_v2 extends Machine { static RegMask callWin64(TypeFunPtr tfp, int idx, int maxArgSlot ) { // idx 2,3,4,5 passed in registers, with stack slot mirrors. // idx >= 6 passed on stack, starting at slot#1 (#0 reserved for RPC). - if( idx >= 6 ) - return new RegMask(MAX_REG+maxArgSlot+(idx-2)); + int sigidx = idx-2; + if( hiddenSelf(tfp) ) { + if( sigidx==0 ) return null; + sigidx--; + } + if( sigidx >= 4 ) + return new RegMask(MAX_REG+maxArgSlot+sigidx); return tfp.arg(idx-2) instanceof TypeFloat - ? XMMS8 [idx-2] - : WIN64_CALL[idx-2]; + ? XMMS8 [sigidx] + : WIN64_CALL[sigidx]; } static short maxArgSlotWin64(TypeFunPtr tfp) { - return (short)tfp.nargs(); + return (short)Math.max(4,tfp.nargs() - (hiddenSelf(tfp) ? 1 : 0)); + } + static short maxIncomingArgSlotWin64(TypeFunPtr tfp) { + return (short)Math.max(0,tfp.nargs() - (hiddenSelf(tfp) ? 1 : 0) - 4); } // Sys5: max 6 GPRs and 8 FPRS filled first. Extra args land in increasing @@ -187,9 +201,14 @@ public class x86_64_v2 extends Machine { static RegMask callSys5(TypeFunPtr tfp, int idx, int maxArgSlot ) { // First 6 integers passed in registers: rdi,rsi,rdx,rcx,r08,r09 // First 8 floats passed in registers: xmm0-xmm7 + int sigidx = idx-2; + if( hiddenSelf(tfp) ) { + if( sigidx==0 ) return null; + sigidx--; + } int icnt=0, fcnt=0; // Count of ints, floats - for( int i=2; i<idx; i++ ) { - if( tfp.arg(i-2) instanceof TypeFloat ) fcnt++; + for( int i=hiddenSelf(tfp) ? 1 : 0; i<idx-2; i++ ) { + if( tfp.arg(i) instanceof TypeFloat ) fcnt++; else icnt++; } int nstk = Math.max(icnt-6,0)+Math.max(fcnt-8,0); @@ -199,13 +218,16 @@ public class x86_64_v2 extends Machine { } static short maxArgSlotSys5(TypeFunPtr tfp) { int icnt=0, fcnt=0; // Count of ints, floats - for( int i=0; i<tfp.nargs(); i++ ) { + for( int i=hiddenSelf(tfp) ? 1 : 0; i<tfp.nargs(); i++ ) { if( tfp.arg(i) instanceof TypeFloat ) fcnt++; else icnt++; } int nstk = Math.max(icnt-6,0)+Math.max(fcnt-8,0); return (short)nstk; } + private static boolean hiddenSelf(TypeFunPtr tfp) { + return tfp.nargs() > 0 && tfp.arg(0) == TypePtr.PTR; + } static final long SYSTEM5_CALLER_SAVE = (1L << RAX) | (1L << RCX) | (1L << RDX) | @@ -224,24 +246,30 @@ public class x86_64_v2 extends Machine { (1L<<XMM4) | (1L<<XMM5) ; @Override public long callerSave() { - return switch (CodeGen.CODE._callingConv) { + return callerSave(CodeGen.CODE._callingConv); + } + @Override public long callerSave(String conv) { + return switch (conv) { case "SystemV" -> SYSTEM5_CALLER_SAVE; - case "Win64" -> WIN64_CALLER_SAVE; - default -> throw new IllegalArgumentException("Unknown calling convention: " + CodeGen.CODE._callingConv); + case "win64" -> WIN64_CALLER_SAVE; + default -> throw new IllegalArgumentException("Unknown calling convention: " + conv); }; } + @Override public String cCallingConv(String callingConv) { + return callingConv; + } @Override public long neverSave() { return 1L<<RSP; } @Override public RegMask retMask( TypeFunPtr tfp ) { - return tfp.ret() instanceof TypeMem ? null : tfp.ret() instanceof TypeFloat ? XMM0_MASK : RAX_MASK; + return tfp.ret() instanceof TypeFloat ? XMM0_MASK : RAX_MASK; } @Override public int rpc() { return RPC; } // Create a split op; any register to any register, including stack slots - @Override public SplitNode split(String kind, byte round, LRG lrg) { + @Override public SplitNode split(LRG lrg, String kind, byte round ) { return lrg._machDef!=null && lrg._machDef.outregmap().test(FLAGS) - ? new SplitFlagsX86(kind,round) - : new SplitX86(kind,round); + ? new SplitFlagsX86(lrg,kind,round) + : new SplitX86(lrg,kind,round); } // Return a MachNode unconditional branch @@ -253,30 +281,34 @@ public class x86_64_v2 extends Machine { @Override public Node instSelect(Node n) { return switch (n) { - case AddFNode addf -> addf(addf); case AddNode add -> add(add); case AndNode and -> and(and); case BoolNode bool -> cmp(bool); + case CLInitNode init -> new CLInitNode(init); case CProjNode c -> new CProjNode(c); case CallEndNode cend -> new CallEndMach(cend); case CallNode call -> call(call); - case CastNode cast -> new CastMach(cast); + case PtrToIntNode ptr -> new PtrToIntMach(ptr); + case GuardNode guard -> new GuardMach(guard); case ConstantNode con -> con(con); - case DivFNode divf -> new DivFX86(divf); - case DivNode div -> new DivX86(div); + case DivNode div -> div.mode()==2 ? new DivFX86(div) : new DivX86(div); + case EscapeNode esc -> new EscapeNode(esc); case FunNode fun -> new FunX86(fun); + case FunPtrNode fptr -> fptr(fptr); case IfNode iff -> jmp(iff); case LoadNode ld -> ld(ld); case MemMergeNode mem -> new MemMergeNode(mem); - case MinusNode neg -> new NegX86(neg); - case MulFNode mulf -> new MulFX86(mulf); - case MulNode mul -> mul(mul); + case MinusNode neg -> { + if( neg.mode()==2 ) throw Utils.TODO(); + yield new NegX86(neg); + } + case MulNode mul -> mul.mode()==2 ? new MulFX86(mul) : mul(mul); case NewNode nnn -> new NewX86(nnn); case NotNode not -> new NotX86(not); case OrNode or -> or(or); case ParmNode parm -> new ParmX86(parm); - case BulkMemPhiNode phi -> new BulkMemPhiNode(phi); - case MemPhiNode phi -> new MemPhiNode(phi); + case BulkMemPhiNode phi-> new BulkMemPhiNode(phi); + case MemPhiNode phi -> new MemPhiNode(phi); case PhiNode phi -> new PhiNode(phi); case ProjNode prj -> prj(prj); case ReadOnlyNode read-> new ReadOnlyMach(read); @@ -284,14 +316,15 @@ public class x86_64_v2 extends Machine { case SarNode sar -> sar(sar); case ShlNode shl -> shl(shl); case ShrNode shr -> shr(shr); + case StartCUNode start -> new StartCUNode(start); + case StopCUNode stop -> new StopCUNode(stop); case StartNode start -> new StartNode(start); case StopNode stop -> new StopNode(stop); case StoreNode st -> st(st); - case SubFNode subf -> new SubFX86(subf); - case SubNode sub -> sub(sub); + case SubNode sub -> sub.mode()==2 ? new SubFX86(sub) : sub(sub); case ToFloatNode tfn -> i2f8(tfn); case ToIntegerNode cvt -> new F8ToIX86(cvt); - case XCtrlNode x -> new ConstantNode(Type.XCONTROL); + case XCtrlNode x -> ConstantNode.raw(Type.XCONTROL); case XorNode xor -> xor(xor); case LoopNode loop -> new LoopNode(loop); @@ -306,14 +339,12 @@ public class x86_64_v2 extends Machine { // Attempt a full LEA-style break down. private Node add(AddNode add) { + if( add.mode()==2 ) + return new AddFX86(add); Node lhs = add.in(1); Node rhs = add.in(2); - if( lhs instanceof LoadNode ld && ld.nOuts() == 1 && ld._declaredType.log_size() >= 3) + if( lhs instanceof LoadNode ld && ld.nOuts() == 1 && ld.declaredType().log_size() >= 3) return new AddMemX86(add, address(ld), ld.ptr(), idx, off, scale, 0, rhs); - - if( rhs instanceof LoadNode ld && ld.nOuts()==1 && ld._declaredType.log_size()>=3 ) - return new AddMemX86(add,address(ld),ld.ptr(),idx,off,scale, 0,lhs); - // Attempt a full LEA-style break down. // Returns one of AddX86, AddIX86, LeaX86, or LHS if( rhs instanceof ConstantNode off2 && off2._con instanceof TypeInteger toff ) { @@ -336,17 +367,6 @@ public class x86_64_v2 extends Machine { } - private Node addf(AddFNode addf) { - if(addf.in(1) instanceof LoadNode ld && ld.nOuts() == 1) - return new AddFMemX86(addf, address(ld), ld.ptr(), idx, off, scale, addf.in(2)); - -// if(addf.in(2) instanceof LoadNode ld && ld.nOuts() == 1) -// throw Utils.TODO(); // Swap load sides - - return new AddFX86(addf); - } - - private Node _lea(Node add, Node base, Node idx, int off) { int scale = 0; if( base instanceof ShlNode && !(idx instanceof ShlNode) ) @@ -370,8 +390,10 @@ public class x86_64_v2 extends Machine { } private Node call(CallNode call) { - return call.fptr() instanceof ConstantNode con && con._con instanceof TypeFunPtr tfp - ? new CallX86(call, tfp) + return call.fptr() instanceof FunPtrNode con + ? new CallX86(call, (TypeFunPtr)con._type) + : call.fptr() instanceof ConstantNode con && con._type instanceof TypeFunPtr + ? new CallX86(call, (TypeFunPtr)con._type) : new CallRX86(call); } @@ -396,12 +418,13 @@ public class x86_64_v2 extends Machine { // Since cmp does not record a BOP, SetX/Jmp need to know if the CMP is swapped. // Vs memory - if( lhs instanceof LoadNode ld && ld.nOuts() == 1 && rhs._type.isa(ld._declaredType) ) + if( lhs instanceof LoadNode ld && ld.nOuts() == 1 && rhs._type.isa(ld.declaredType()) ) return new CmpMemX86(bool, address(ld), ld.ptr(), idx, off, scale, imm(rhs), val, false); // Operands swap in the encoding directly, no need for Set/Jmp to swap `bop` - if( rhs instanceof LoadNode ld && ld.nOuts() == 1 && lhs._type.isa(ld._declaredType) && - (val!=null || bool.op()=="==" || bool.op()=="!=") ) + if( rhs instanceof LoadNode ld && ld.nOuts() == 1 && lhs._type.isa(ld.declaredType()) && + (!(lhs instanceof ConstantNode con && con._con instanceof TypeInteger ti && imm32(ti.value())) || + bool.op()=="==" || bool.op()=="!=") ) return new CmpMemX86(bool, address(ld), ld.ptr(), idx, off, scale, imm(lhs), val, true ); // Vs immediate @@ -418,18 +441,23 @@ public class x86_64_v2 extends Machine { private Node con( ConstantNode con ) { if( !con._con.isConstant() ) - return new ConstantNode(con); // Default unknown caller inputs + return ConstantNode.raw(con); // Default unknown caller inputs + String ext = con instanceof ExternNode ext0 ? ext0._extern : null; return switch (con._con) { - case TypeInteger ti -> new IntX86(con); - case TypeFloat tf -> new FltX86(con); - case TypeFunPtr tfp -> new TFPX86(con); - case TypeMemPtr tmp -> new TMPX86(con); + case TypeInteger ti -> new IntX86(con,ext); + case TypeFloat tf -> new FltX86(con,ext); + case TypeMemPtr tmp -> new TMPX86(con,ext); + case TypeFunPtr tfp -> new TFPX86(tfp,ext); case TypeNil tn -> throw Utils.TODO(); // TOP, BOTTOM, XCtrl, Ctrl, etc. Never any executable code. - case Type t -> t == Type.NIL ? new IntX86(con) : new ConstantNode(con); + case Type t -> t == Type.NIL ? new IntX86(con,null) : ConstantNode.raw(con); }; } + private Node fptr( FunPtrNode con ) { + return new TFPX86(con); + } + private Node i2f8(ToFloatNode tfn) { assert tfn.in(1)._type instanceof TypeInteger ti; return new I2f8X86(tfn); @@ -442,7 +470,9 @@ public class x86_64_v2 extends Machine { String op = "!="; if( iff.in(1) instanceof BoolNode bool ) op = swap ? IfNode.swap(bool.op()) : bool.op(); else if( iff.in(1)==null ) op = "=="; // Never-node cutout - else iff.setDef(1, new BoolNode.NE(iff.in(1), new ConstantNode(TypeInteger.ZERO),(byte)1)); + else iff.setDef(1, new BoolNode.NE(iff.in(1), + ConstantNode.raw(TypeInteger.ZERO), + (byte)(iff.in(1)._type instanceof TypeFloat ? 2 : 1))); return new JmpX86(iff, op); } @@ -463,9 +493,10 @@ public class x86_64_v2 extends Machine { } private Node prj( ProjNode prj ) { + if( prj.in(0) instanceof CLInitNode ) return new ProjNode(prj); TypeInteger ret = cReturn(prj); if( ret!=null ) return new ExtendX86(prj,ret); - return new ProjX86(prj); + return prj.in(0) instanceof StartNode ? new ProjNode(prj) : new ProjX86(prj); } private Node sar(SarNode sar) { @@ -552,7 +583,7 @@ public class x86_64_v2 extends Machine { private int imm( Node xval ) { assert val == null && imm == 0; - if( xval instanceof ConstantNode con && con._con instanceof TypeInteger ti) { + if( xval instanceof ConstantNode con && con._con instanceof TypeInteger ti && ti.isConstant() && imm32(ti.value()) ) { val = null; imm = (int) ti.value(); assert imm == ti.value(); // In 32-bit range diff --git a/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java b/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java index 48296ce7..58506e9c 100644 --- a/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java +++ b/src/main/java/com/seaofnodes/simple/print/ASMPrinter.java @@ -7,8 +7,10 @@ import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.BAOS; import java.util.ArrayList; import com.seaofnodes.simple.codegen.Encoding; +import java.util.HashMap; /** Machine-specific facts; traversal, columns and byte display live in print/. */ public final class ASMPrinter extends AssemblyAdapter<Node> { @@ -24,7 +26,7 @@ public final class ASMPrinter extends AssemblyAdapter<Node> { } @Override public ArrayList<Node> blocks() { var blocks=new ArrayList<Node>(); - var cfg=_code._cfg; + var cfg=_enc==null ? _code._cfg : _enc._cfg; if( cfg!=null ) for( Node n : cfg ) blocks.add(n); return blocks; } @@ -50,26 +52,24 @@ public final class ASMPrinter extends AssemblyAdapter<Node> { if( n._type instanceof TypeMem || n._type instanceof TypeRPC ) return true; if( postAlloc() && n instanceof CalleeSaveNode ) return true; if( n instanceof MemMergeNode ) return true; + if( n instanceof EscapeNode ) return true; return n.getClass()==ConstantNode.class; } - @Override public boolean postAlloc() { return _code._phase.ordinal()>CodeGen.Phase.RegAlloc.ordinal(); } + @Override public boolean postAlloc() { return _code._phase.ordinal()>CodeGen.Phase.RegAlloc.ordinal() || + (_code._phase==CodeGen.Phase.RegAlloc && _code._regAlloc!=null && _code._regAlloc.done()); } @Override public boolean encoded() { return _enc!=null && _code._phase.ordinal()>=CodeGen.Phase.Encoding.ordinal(); } @Override public int defaultSize() { return _code._mach==null ? 2 : _code._mach.defaultOpSize(); } @Override public boolean littleEndian() { return _code._asmLittle; } @Override public boolean prologue(Node n) { return ((FunNode)n)._frameAdjust!=0; } @Override public byte[] bytes() { return encoded() ? _enc._bits.buf() : null; } - @Override public int offset(Node n) { - return encoded() && _enc._opStart!=null && n._nid<_enc._opStart.length ? _enc._opStart[n._nid] : -1; - } - @Override public int size(Node n) { - return encoded() && _enc._opLen!=null && n._nid<_enc._opLen.length ? _enc._opLen[n._nid]&255 : 0; - } - private static ArrayList<Data> entries(Iterable<Encoding.Relo> relos) { + @Override public int offset(Node n) { return encoded() ? _enc.opStart(n) : -1; } + @Override public int size(Node n) { return encoded() ? _enc.opLen(n)&255 : 0; } + private static ArrayList<Data> entries(Iterable<Encoding.Relo> relos, boolean ro) { var data=new ArrayList<Data>(); for( Encoding.Relo r : relos ) { + if( r.readOnly()!=ro ) continue; int align=1<<r._align; int size=r._t instanceof TypeStruct ? (r._structSize+align-1)&-align : align; - if( r._t instanceof TypeTuple t ) size=align*t._types.length; data.add(new Data(r._t,r._t.str(),r._align,size)); } return data; @@ -78,8 +78,24 @@ public final class ASMPrinter extends AssemblyAdapter<Node> { var pools=new ArrayList<Pool>(); if( !encoded() ) return pools; int base=(codeEnd+15)&-16; - if( !_enc._bigCons.isEmpty() && base<_enc._bits.size() ) - pools.add(new Pool("Constant Pool",_enc._bits.buf(),base,base,entries(_enc._bigCons.values()))); + if( _enc._cpool.size()>0 ) + pools.add(new Pool("Constant Pool",_enc._cpool.buf(),0,base,objects(true))); + base=(base+_enc._cpool.size()+15)&-16; + if( _enc._sdata.size()>0 ) + pools.add(new Pool("Static Data",_enc._sdata.buf(),0,base,objects(false))); return pools; } + private ArrayList<Data> objects(boolean ro) { + var data=new ArrayList<Data>(); + for( var obj : _enc._data.entries() ) + if( !obj.external && obj.readOnly==ro ) + data.add(new Data(obj,obj.symbol+": "+obj.type.str(),obj.alignment,obj.size)); + return data; + } + // Retain this diagnostic entry point for checking that pool printing never lays out types. + static int printConstantPool(int address, SB sb, BAOS bits, HashMap<Node,Encoding.Relo> relos, + boolean ro, String name) { + sb.p(com.seaofnodes.print.ASMPrinter.pool(new Pool(name,bits.buf(),0,address,entries(relos.values(),ro)))); + return address+bits.size(); + } } diff --git a/src/main/java/com/seaofnodes/simple/print/IRPrinter.java b/src/main/java/com/seaofnodes/simple/print/IRPrinter.java index c912ab77..531a549a 100644 --- a/src/main/java/com/seaofnodes/simple/print/IRPrinter.java +++ b/src/main/java/com/seaofnodes/simple/print/IRPrinter.java @@ -14,6 +14,8 @@ public final class IRPrinter extends IRAdapter<Node> { @Override public String type(Node n) { return n._type==null ? "" : n._type.str(); } @Override public String inputMark(Node n, Node def) { return n instanceof MemMergeNode && def instanceof MemMergeNode ? "^" : " "; } @Override public Kind kind(Node n) { + if( n instanceof StartCUNode ) return Kind.UNIT; + if( n instanceof StopCUNode ) return Kind.UNIT; if( n instanceof StartNode ) return Kind.START; if( n instanceof StopNode ) return Kind.STOP; if( n instanceof FunNode ) return Kind.FUN; @@ -47,15 +49,12 @@ public final class IRPrinter extends IRAdapter<Node> { public static String prettyPrint(CodeGen code) { var units=new ArrayList<com.seaofnodes.print.IRPrinter.Unit<Node>>(); - var funs=new ArrayList<Node>(); - for( Node n : code._start._outputs ) if( n instanceof FunNode ) funs.add(n); - units.add(new com.seaofnodes.print.IRPrinter.Unit<>(null,null,null,funs)); + for( var cu : code._compunits.values() ) { + var funs=new ArrayList<Node>(); + for( FunNode fun : code._linker ) + if( fun!=null && fun._compunit==cu ) funs.add(fun); + units.add(new com.seaofnodes.print.IRPrinter.Unit<>(cu._cname,cu._start,cu._stop,funs)); + } return PRINT.program(code._start,code._stop,units); } - public static String _prettyPrint(CodeGen code) { - if( code._cfg==null ) return prettyPrint(code); - var blocks=new ArrayList<Node>(); - for( Node n : code._cfg ) blocks.add(n); - return PRINT.scheduled(blocks); - } } diff --git a/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java index 8dc6ea47..9ca0ec51 100644 --- a/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ b/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -2,22 +2,30 @@ package com.seaofnodes.simple.print; import com.seaofnodes.graph.GraphAdapter; import com.seaofnodes.graph.GraphSnapshot.*; +import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.node.Node; import com.seaofnodes.simple.type.TypeMem; -/** Chapter 24's view of the IR; browser, transport and layout are shared. */ +/** Chapter 25's view of the IR, including partially constructed parse-time graphs. */ public class SimpleGraphAdapter extends GraphAdapter<Node> { + @Override protected boolean show(Node n) { + if( n != CodeGen.CODE.ZERO && n != CodeGen.CODE.XCTRL ) return true; + for( int i=0; i<n.nOuts(); i++ ) + if( n.out(i) != null ) return true; + return false; // The parser's keep is not a graph use. + } + @Override protected String edgeName(Node n, int i) { return n instanceof ScopeNode scope && i < scope._vars.size() - ? scope._vars.get(i)._name : null; + ? scope.var(i)._name : null; } @Override protected int edgeJump(Node n, int i) { return n instanceof CallEndNode && i>0 && n.in(i) instanceof ReturnNode ret ? ref(ret.fun()) : 0; } - @Override protected int projectionIndex(Node n) { return n instanceof ProjNode p ? p._idx : n instanceof CProjNode p ? p._idx : -1; } + @Override protected int projectionIndex(Node n) { return n instanceof Proj p ? p.idx() : -1; } @Override protected boolean folding(Node n) { if( n instanceof FunNode fun ) return fun.folding(); @@ -32,26 +40,41 @@ public class SimpleGraphAdapter extends GraphAdapter<Node> { @Override protected Kind kind(Node n) { if( n instanceof StopNode ) return Kind.STOP; if( n instanceof ScopeNode ) return Kind.SCOPE; + if( n instanceof StartCUNode ) return Kind.UNIT; if( n instanceof FunNode ) return Kind.FUN; - if( n instanceof StartNode ) return Kind.CTRL; + // Start inherits Loop for whole-program analysis, but is not a source loop. + if( n instanceof StartNode ) return Kind.START; if( n instanceof LoopNode ) return Kind.LOOP; if( n instanceof RegionNode ) return Kind.REGION; if( n instanceof PhiNode ) return Kind.PHI; - return n instanceof CFGNode ? Kind.CTRL : isMem(n) ? Kind.MEM : Kind.DATA; + if( n instanceof CFGNode ) return Kind.CTRL; + return isMem(n) ? Kind.MEM : Kind.DATA; } @Override protected Role role(Node n, int i) { - if( n instanceof ScopeNode || n instanceof ConstantNode || n instanceof CtrlNode || n instanceof XCtrlNode ) return Role.ASSOC; - if( n instanceof PhiNode ) return i == 0 ? Role.ASSOC : isMem(n) ? Role.MEM : Role.DATA; - if( n instanceof ProjNode || n instanceof CProjNode ) + if( n instanceof ScopeNode || n instanceof ConstantNode || n instanceof FunPtrNode || + n instanceof CtrlNode || n instanceof XCtrlNode ) + return Role.ASSOC; + if( n instanceof PhiNode ) + return i == 0 ? Role.ASSOC : isMem(n) ? Role.MEM : Role.DATA; + if( n instanceof Proj ) return n instanceof CFGNode ? Role.CTRL : isMem(n) ? Role.MEM : Role.DATA; + // StartCU inherits Region, but slot 0 really connects it to Start. + if( n instanceof StartCUNode && i == 0 ) return Role.CTRL; if( n instanceof RegionNode && i == 0 ) return Role.ASSOC; + // Preserve known slot roles even when an input is not attached/typed yet. if( i == 0 || n instanceof RegionNode || n instanceof StopNode ) return Role.CTRL; - if( n instanceof MemMergeNode || (i == 1 && (n instanceof MemOpNode || n instanceof ReturnNode || n instanceof CallNode || n instanceof NewNode)) ) return Role.MEM; + if( n instanceof MemMergeNode || + (i == 1 && (n instanceof MemOpNode || n instanceof ReturnNode || n instanceof CallNode)) || + (n instanceof EscapeNode && i >= 2) ) return Role.MEM; Node def = n.in(i); + if( def instanceof CFGNode ) return Role.CTRL; if( def != null && isMem(def) ) return Role.MEM; return Role.DATA; } - private boolean isMem(Node n) { return n.isMem() || n._type instanceof TypeMem; } + private boolean isMem(Node n) { + // These Phi subclasses are memory even before their first type computation. + return n instanceof MemPhiNode || n instanceof BulkMemPhiNode || n.isMem() || n._type instanceof TypeMem; + } } diff --git a/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java index b2c5492e..f2ed2f72 100644 --- a/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ b/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -7,7 +7,7 @@ import com.seaofnodes.simple.node.Node; import java.util.ArrayList; import java.io.IOException; -/** Chapter 24's compilation context; capture and event bookkeeping are shared. */ +/** Chapter 25's compilation context; capture and event bookkeeping are shared. */ public class SimpleGraphObserver extends GraphCapture<Node> { private CodeGen _code; public SimpleGraphObserver() { super(new SimpleGraphAdapter()); } diff --git a/src/main/java/com/seaofnodes/simple/type/Field.java b/src/main/java/com/seaofnodes/simple/type/Field.java index 9bd85ba6..ec4a588d 100644 --- a/src/main/java/com/seaofnodes/simple/type/Field.java +++ b/src/main/java/com/seaofnodes/simple/type/Field.java @@ -8,6 +8,7 @@ import java.util.*; * Represents a field in a struct. This is not a Type in the type system. */ public class Field extends Type { + static final Field[] EMPTY = new Field[0]; // The pair {fieldName,type} uniquely identifies a field. @@ -17,29 +18,28 @@ public class Field extends Type { public Type _t; // Unique memory alias, not sensibly part of a "type" but very convenient here. public int _alias; - // Field is immutable after construction; constructor locals may change + // Field is only written in the constructor public boolean _final; - // Shared function declaration, with the same value for every instance. - // A final data field is still per-instance, even with a constant default. - public boolean _one; + // Native C storage, outside the containing object's layout. + public boolean _extern; private static final Ary<Field> FREE = new Ary<>(Field.class); - private Field(String fname, Type type, int alias, boolean xfinal, boolean one ) { super(TFLD); init(fname,type,alias,xfinal,one); } - private Field init(String fname, Type type, int alias, boolean xfinal, boolean one ) { + private Field(String fname, Type type, int alias, boolean xfinal, boolean external ) { super(TFLD); init(fname,type,alias,xfinal,external); } + private Field init(String fname, Type type, int alias, boolean xfinal, boolean external ) { _fname = fname; _t = type; _alias = alias; _final = xfinal; - _one = one; + _extern = external; return this; } // Return a filled-in Field; either from free list or alloc new. - private static Field malloc(String fname, Type type, int alias, boolean xfinal, boolean one ) { - return FREE.isEmpty() ? new Field(fname,type,alias,xfinal,one) : FREE.pop().init(fname,type,alias,xfinal,one); + private static Field malloc(String fname, Type type, int alias, boolean xfinal, boolean external ) { + return FREE.isEmpty() ? new Field(fname,type,alias,xfinal,external) : FREE.pop().init(fname,type,alias,xfinal,external); } // Malloc-from - public Field malloc( ) { return malloc(_fname,null,_alias,_final,_one); } + public Field malloc( ) { return malloc(_fname,null,_alias,_final,_extern); } @Override Field free(Type t) { Field f = (Field)t; assert !f.isFree() && !f._terned; @@ -52,27 +52,31 @@ public class Field extends Type { @Override boolean isFree() { return _t ==null; } // Make and intern with listed fields - public static Field make( String fname, Type type, int alias, boolean xfinal, boolean one ) { - Field f = malloc(fname,type,alias,xfinal,one); + public static Field make( String fname, Type type, int alias, boolean xfinal ) { + return make(fname,type,alias,xfinal,false); + } + public static Field make( String fname, Type type, int alias, boolean xfinal, boolean external ) { + Field f = malloc(fname,type,alias,xfinal,external); Field f2 = f.intern(); if( f2==f ) return f; return VISIT.isEmpty() ? f2.free(f) : f2.delayFree(f); } public Field makeFrom( Type type ) { - return type == _t ? this : make(_fname,type,_alias,_final,_one); + return type == _t ? this : make(_fname,type,_alias,_final,_extern); } public Field makeFrom( boolean xfinal ) { - return xfinal == _final ? this : make(_fname,_t,_alias,xfinal,_one); + return xfinal == _final ? this : make(_fname,_t,_alias,xfinal,_extern); } // Cycle-making-breaking void setType(Type t) { assert _t ==null; _t = t; } - public static final Field BOT = make(" ",Type.BOTTOM,-999,true,false); - public static final Field TEST = make("test",Type.NIL,-2,false,false); - public static final Field TEST2 = make("test",Type.NIL,-2,true, false); - public static final Field FLT = make("flt ",TypeFloat.F32,-2,false,false); - public static void gather(ArrayList<Type> ts) { ts.add(TEST); ts.add(TEST2); ts.add(FLT); } + public static final Field BOT = make(" ",Type.BOTTOM,-999,false); + public static final Field TEST = make("test",Type.NIL,-2,false); + public static final Field TEST2 = make("test",Type.NIL,-2,true); + public static final Field FLT = make("flt ",TypeFloat.F32,-2,false); + public static final Field EXT = make("test",TypeInteger.I32,-2,false,true); + public static void gather(ArrayList<Type> ts) { ts.add(TEST); ts.add(TEST2); ts.add(FLT); ts.add(EXT); } @Override Field xmeet( Type that ) { Field fld = (Field)that; // Invariant @@ -81,17 +85,17 @@ public class Field extends Type { if( fld ==BOT.dual() ) return this; if( _fname!=fld._fname ) { assert !_fname.equals(fld._fname); return BOT; } assert _alias==fld._alias; - assert _one ==fld._one ; - return make(_fname, _t.meet(fld._t ),_alias,_final | fld._final, _one); + if( _extern!=fld._extern ) return BOT; + return make(_fname, _t.meet(fld._t ), _alias, _final | fld._final,_extern); } @Override - Field xdual() { return malloc(_fname, _t.dual(),_alias,!_final,_one); } + Field xdual() { return malloc(_fname, _t.dual(),_alias,!_final,_extern); } @Override Field rdual() { if( _dual!=null ) return dual(); assert !_terned; - Field d = malloc(_fname,null,_alias,!_final,_one); + Field d = malloc(_fname,null,_alias,!_final,_extern); (_dual = d)._dual = this; // Cross link duals d._t = _t._terned ? _t.dual() : _t.rdual(); return d; @@ -99,42 +103,40 @@ public class Field extends Type { @Override boolean _isConstant() { return _t._isConstant(); } @Override boolean _isFinal() { return _final && _t._isFinal(); } - @Override Field _makeRO() { return make(_fname, _t._makeRO(),_alias,true,_one); } - boolean isGLB2() { return _final && _t._isGLB(true); } - Field glb2() { - Type glb = _t._glb(true); - return (glb== _t && _final ) ? this : make(_fname,glb,_alias,true,_one); + @Override Field _makeRO() { return _final ? this : make(_fname, _t._makeRO(),_alias,true,_extern); } + @Override Field _close( String name, HashMap<String, Type> TYPES ) { return makeFrom( _t._close(name, TYPES )); } + + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + return makeFrom(_t._upgradeType(TYPES)); } - @Override Field _close(java.util.HashMap<String,Type> types) { return makeFrom( _t._close(types)); } // Override in subclasses - int hash() { return _fname.hashCode() ^ _t.hashCode() ^ _alias ^ (_final ? 1024 : 0) ^ (_one ? 2048 : 0); } + int hash() { return _fname.hashCode() ^ _t.hashCode() ^ _alias ^ (_final ? 1024 : 0) ^ (_extern ? 2048 : 0); } private boolean static_eq( Field f ) { - return _fname.equals(f._fname) && _alias==f._alias && _final==f._final && _one==f._one && _t !=null && f._t !=null; + return _fname.equals(f._fname) && _alias==f._alias && _final==f._final && _extern==f._extern && _t !=null && f._t !=null; } @Override boolean eq( Type t ) { Field f = (Field)t; return static_eq(f) && _t ==f._t; } @Override boolean cycle_eq(Type t) { + if( this==t ) return true; Field f = (Field)t; return static_eq(f) && _t.cycle_eq(f._t ); } - @Override int nkids() { return 1; } - @Override Type at( int idx ) { return _t; } - @Override void set( int idx, Type t ) { _t = t; } + @Override public int nkids() { return 1; } + @Override public Type at( int idx ) { return _t; } + @Override public void set( int idx, Type t ) { _t = t; } + // Tags: final/!final; +alias+name+external-storage flag + @Override public SB _print( SB sb, BitSet visit, boolean html ) { - sb.p(_final?"":"!").p(_one?"$":"").p(_fname).p(":").p(_alias).p(" : "); + sb.p(_extern ? "extern C " : "").p(_final?"":"!").p(_fname).p(":").p(_alias).p(" : "); return _t ==null ? sb.p("---") : _t.print(sb,visit,html); } - @Override public String str() { return (_final?"":"!")+(_one?"$":"")+_fname; } - - @Override Type _upgradeType(java.util.HashMap<String,Type> types) { - return makeFrom(_t._upgradeType(types)); - } + @Override public String str() { return (_final?"":"!")+_fname; } } diff --git a/src/main/java/com/seaofnodes/simple/type/Type.java b/src/main/java/com/seaofnodes/simple/type/Type.java index 9fad2767..1979368e 100644 --- a/src/main/java/com/seaofnodes/simple/type/Type.java +++ b/src/main/java/com/seaofnodes/simple/type/Type.java @@ -56,35 +56,38 @@ public class Type /*implements Cloneable*/ { static final byte TXCTRL = 3; // Ctrl flow top (mini-lattice: any-xctrl-ctrl-all) static final byte TNIL = 4; // low null of all flavors static final byte TXNIL = 5; // high or choice null - static final byte TSIMPLE = 6; // End of the Simple Types - static final byte TSCALAR = 6; // All register/field values, excluding control and memory + static final byte TSIMPLE = 5; // End of the Simple Types - static final byte TPTR = 7; // All nil-able scalar values + static final byte TSCALAR = 6; // Integer, float, memory pointer or function pointer + static final byte TPTR = 7; // All nil-able pointer values static final byte TINT = 8; // All Integers; see TypeInteger static final byte TFLT = 9; // All Floats ; see TypeFloat static final byte TCONARY =10; // Constant array static final byte TRPC =11; // Return Program Control (Return PC or RPC) - static final byte TTUPLE =12; // Tuples; finite collections of unrelated Types, kept in parallel - static final byte TCYCLIC =13; // Has internal pointers, needs recursive treatment - static final byte TMEMPTR =13; // Memory pointer to a struct type - static final byte TFUNPTR =14; // Function pointer; unique signature and code address (just a bit) - static final byte TMEM =15; // All memory (alias 0) or A slice of memory - with specific alias + static final byte TCYCLIC =12; // Has internal pointers, needs recursive treatment + static final byte TMEMPTR =12; // Memory pointer to a struct type + static final byte TFUNPTR =13; // Function pointer; unique signature and code address (just a bit) + static final byte TMEM =14; // All memory (alias 0) or A slice of memory - with specific alias + static final byte TFLD =15; // Named fields in structs static final byte TSTRUCT =16; // Structs; tuples with named fields - static final byte TFLD =17; // Named fields in structs + static final byte TTUPLE =17; // Tuples; finite collections of unrelated Types, kept in parallel + + static final byte TMAX =18; // Basic RTTI, useful for a lot of fast tests. public final byte _type; public boolean _terned; - public boolean is_simple() { return _type < TSIMPLE; } + public boolean is_simple() { return _type <= TSIMPLE; } + public boolean is_nokids() { return _type < TCYCLIC; } private static final String[] STRS = new String[]{"Bot","Top","Ctrl","~Ctrl","null","~nil"}; - static final int[] CNTS = new int[TFLD+1]; protected Type(byte type) { _type = type; // RTTI _uid = (char)UID++; // A unique ID for every type + if( UID > 32000 ) + type = type; assert _uid!=0; // Overflow - CNTS[type]++; } public static final Type BOTTOM = new Type( TBOT ).intern(); // ALL @@ -137,7 +140,7 @@ public class Type /*implements Cloneable*/ { // Factory method which interns "this" @SuppressWarnings("unchecked") - <T extends Type> T intern() { + public <T extends Type> T intern() { //assert check(); assert !_terned; // Do not ask for already-interned T t2 = (T)INTERN.get(this); @@ -218,16 +221,16 @@ public class Type /*implements Cloneable*/ { } // Overridden in subclasses; subclass can assume "this!=t" and java classes are same boolean eq(Type t) { return this==t; } - boolean cycle_eq(Type t) { assert _type < TCYCLIC; return _type==t._type && eq(t); } + boolean cycle_eq(Type t) { assert is_nokids(); return _type==t._type && eq(t); } // A pair of uids int pid( Type that ) { return _uid < that._uid ? (_uid<<16 | that._uid) : (that._uid<<16 | _uid); } // At/Set child at 'idx' to t - Type at ( int idx ) { throw Utils.TODO(); } - void set( int idx, Type t ) { throw Utils.TODO(); } - int nkids() { assert _type < TTUPLE; return 0; } // Number of kids + public Type at( int idx ) { throw Utils.TODO("Should not reach here: type has no children"); } + public void set( int idx, Type t ) { throw Utils.TODO("Should not reach here: type has no children"); } + public int nkids() { return 0; } // Number of kids // Clear and re-insert the basic Type INTERN table @@ -256,6 +259,7 @@ public class Type /*implements Cloneable*/ { // ---------------------------------------------------------- public final Type meet(Type t) { + assert _terned && t._terned; // Shortcut for the self case if( t == this ) return this; // Same-type is always safe in the subclasses @@ -266,12 +270,14 @@ public class Type /*implements Cloneable*/ { // Reverse; xmeet 2nd arg is never "is_simple" and never equal to "this". if( is_simple() ) return this.xmeet(t ); if( t.is_simple() ) return t .xmeet(this); - // Constant arrays meet their integer element types. + // TypeConAry meet its element type is OK if( this instanceof TypeConAry tcon && t instanceof TypeInteger ti ) return tcon.ymeet(ti); if( t instanceof TypeConAry tcon && this instanceof TypeInteger ti ) return tcon.ymeet(ti); + // Two distinct scalar families meet at the scalar envelope. if( this instanceof TypeScalar scalar0 && t instanceof TypeScalar scalar1 ) return scalar0.smeet(scalar1); - return Type.BOTTOM; // Mixing 2 unrelated types + // Mixing 2 unrelated types + return Type.BOTTOM; } // Compute meet right now. Overridden in subclasses. @@ -286,7 +292,7 @@ public class Type /*implements Cloneable*/ { // RHS TypeNil vs NIL/XNIL if( _type==TNIL || _type==TXNIL ) { if( t instanceof TypeNil ptr ) return _type==TNIL ? ptr.meet0() : ptr.meetX(); - if( t instanceof TypeScalar ) { + if( t instanceof TypeScalar scalar ) { if( t._type==TSCALAR ) return t.isHigh() ? this : t; return TypeScalar.BOT; } @@ -315,7 +321,7 @@ public class Type /*implements Cloneable*/ { case TXCTRL-> TCTRL; case TNIL -> TXNIL; case TXNIL -> TNIL; - default -> throw Utils.TODO(); // Should not reach here + default -> throw Utils.TODO("Should not reach here: unknown simple type"); }); } @@ -329,24 +335,13 @@ public class Type /*implements Cloneable*/ { } // True if this "isa" t; e.g. 17 isa TypeInteger.BOT - // Applies for pessimistic case - public boolean isa( Type t ) { return meet(t)==t; } - - // True if this "isa" t up to named structures - public boolean shallowISA( Type t ) { return isa(t); } + public boolean isa( Type t ) { return meet(t)==t; } - public Type nonZero() { return TypePtr.NPTR; } + public Type nonZero() { return this==NIL ? TypePtr.XNPTR : TypePtr.NPTR; } // Make a zero version of this type, 0 for integers and null for pointers. public Type makeZero() { return Type.NIL; } - // Writable array slots are invariant in their element access permissions. - // Otherwise an alias could insert a readonly pointer into a writable slot. - public boolean accessISA(Type dst) { return true; } - - // Is forward-reference - public boolean isFRef() { return false; } - // Cap at limits public Type oob() { return isHigh() ? TOP : BOTTOM; } @@ -354,7 +349,7 @@ public class Type /*implements Cloneable*/ { // Cyclic types! Flag the start of a cyclic type by putting a sentinel in // VISIT (and eventually clearing VISIT when done). Then do a normal - // recursive descent visit of all types; this will accumilate types without + // recursive descent visit of all types; this will accumulate types without // interning them. When done we have to visit the possibly cyclic type and // intern the whole cycle, possibly hitting the entire cycle on a prior // interned cycle. Any sub-part might also be interned, including whole @@ -367,7 +362,8 @@ public class Type /*implements Cloneable*/ { // "C0<->D0" cycle. Type recurOpen() { assert VISIT.isEmpty(); VISIT.put(0L,BOTTOM); return this; } - Type recurClose() { + Type recurClose() { return recurClose(null); } + Type recurClose(Type[] types) { VISIT.remove(0L); // Just ignore the sentinel Ary<Type> ts = new Ary<>(Type.class); ts.addAll(VISIT.values()); @@ -391,6 +387,9 @@ public class Type /*implements Cloneable*/ { // Upgrade the result Type rez = INTERN.get(this); assert rez!=null; + if( types!=null ) + for( int i=0; i<types.length; i++ ) + types[i] = INTERN.get(types[i]); // Pass#4?: Free up any created-but-already interned for( Type t : FREES ) @@ -432,10 +431,12 @@ public class Type /*implements Cloneable*/ { free(this); } - Type free(Type free) { return this; } + Type free(Type free) { + return this; + } boolean isFree() { return false; } - <T extends Type> T delayFree(Type free) { + final <T extends Type> T delayFree(Type free) { assert !free._terned; FREES.push(free); return (T)this; @@ -444,7 +445,7 @@ public class Type /*implements Cloneable*/ { final Type install() { if( this instanceof TypeStruct ) { Type x = _intern(); // Stop the recursion - assert x==this; + if( x!=this ) return x.delayFree(this); } int nkids = nkids(); for( int i=0; i<nkids; i++ ) { @@ -457,7 +458,7 @@ public class Type /*implements Cloneable*/ { } } } - return this instanceof TypeStruct ? this : _intern(); + return this instanceof TypeStruct || _terned ? this : _intern(); } // Strict constant values, things on the lattice centerline. @@ -466,8 +467,12 @@ public class Type /*implements Cloneable*/ { boolean _isConstant() { return _type==TNIL; } // Are all reachable struct Fields are final? + // Writable array slots are invariant in their element access permissions. + // Otherwise an alias could insert a readonly pointer into a writable slot. + public boolean accessISA(Type dst) { return true; } + public final boolean isFinal() { return recurClose(recurOpen()._isFinal()); } - boolean _isFinal() { assert _type < TCYCLIC; return true; } + boolean _isFinal() { assert is_nokids(); return true; } public final Type makeRO() { if( isFinal() ) return this; @@ -475,31 +480,38 @@ public class Type /*implements Cloneable*/ { } Type _makeRO() { return this; } - // Compute greatest lower bound in the lattice. If values are in memory, - // ints and floats cannot widen. - public final boolean isGLB(boolean mem) { return recurClose(recurOpen()._isGLB(mem)); }; - boolean _isGLB(boolean mem) { - return switch(_type) { - case TBOT -> false; - case TNIL -> false; - case TCTRL -> true; - case TXCTRL -> false; - case TXNIL -> false; - case TTOP -> false; - default -> throw Utils.TODO(); - }; + /** Declared shape used when a value is published into mutable memory. */ + public final Type makeStorage() { + return recurOpen()._makeStorage().recurClose(); } - public final Type glb(boolean mem) { - if( isGLB(mem) ) return this; - Type glb = recurOpen()._glb(mem).recurClose(); - assert this.isa(glb); - return glb; + Type _makeStorage() { assert is_simple(); return BOTTOM; } + + // Bulk close-over all recursive types + public static TypeStruct[] closeOver(TypeStruct[] ts, HashMap<String,Type> TYPES) { + BOTTOM.recurOpen(); + for( TypeStruct t : ts ) + t._close(null, TYPES); + for( int i=0; i<ts.length; i++ ) { + // Parser only tracks the mutable name of arrays, so the closeOver + // returns that one. However, internal types might have immutable + // versions. + String name = ts[i].isAry() ? (ts[i]._name+"!").intern() : ts[i]._name; + ts[i] = (TypeStruct)VISIT.get(name); + } + BOTTOM.recurClose(ts); + return ts; } - Type _glb(boolean mem) { assert is_simple(); return Type.BOTTOM; } - Type _close(HashMap<String,Type> types) { return this; } + Type _close( String name, HashMap<String, Type> TYPES ) { return this; } public Type widen() { return this; } + // Replace recursively all TypeBuilders with cyclic TypeStructs + public Type upgradeType(HashMap<String,Type> TYPES) { + return recurOpen()._upgradeType(TYPES).recurClose(); + } + Type _upgradeType(HashMap<String,Type> TYPES) { assert is_nokids(); return this; } + + // ---------------------------------------------------------- // Size in bits to hold an instance of this type. @@ -533,11 +545,4 @@ public class Type /*implements Cloneable*/ { // This is used by error messages, and is a shorted print. public String str() { return STRS[_type]; } - - // Closing declarations changes the meaning of forward shapes. Upgrade - // the whole graph together before starting the pessimistic fixed point. - public final Type upgradeType(HashMap<String,Type> types) { - return recurOpen()._upgradeType(types).recurClose(); - } - Type _upgradeType(HashMap<String,Type> types) { return this; } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAry.java b/src/main/java/com/seaofnodes/simple/type/TypeConAry.java index d7ce8503..860cefc5 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeConAry.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeConAry.java @@ -1,8 +1,9 @@ package com.seaofnodes.simple.type; -import java.io.ByteArrayOutputStream; +import com.seaofnodes.simple.util.BAOS; import com.seaofnodes.simple.util.Utils; import java.util.ArrayList; +import java.util.HashMap; /** * Represents a constant array of primitives @@ -55,8 +56,7 @@ public abstract class TypeConAry<A> extends TypeScalar { @Override public boolean isHigh() { return this==TOP; } @Override boolean _isConstant() { return true; } - @Override Type _glb(boolean mem) { return this; } - @Override boolean _isGLB(boolean mem) { return true; } + @Override Type _makeStorage() { return this; } // Meet-over-elements type public Type elem() { @@ -74,6 +74,7 @@ public abstract class TypeConAry<A> extends TypeScalar { public abstract long at8(int idx); public abstract int len(); @Override public abstract int log_size(); - public void write( ByteArrayOutputStream baos ) { throw Utils.TODO("Should not reach here: abstract constant array cannot be written"); } + + public void write( BAOS baos ) { throw Utils.TODO("Should not reach here: abstract constant array cannot be written"); } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java b/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java index 095cdeca..bddcb1fb 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java @@ -1,6 +1,6 @@ package com.seaofnodes.simple.type; -import java.io.ByteArrayOutputStream; +import com.seaofnodes.simple.util.BAOS; import java.util.Arrays; public class TypeConAryB extends TypeConAry<byte[]> { @@ -16,5 +16,6 @@ public class TypeConAryB extends TypeConAry<byte[]> { @Override public String str() { return "[\""+new String(_ary)+"\"]"; } @Override boolean eq(Type t) { return t instanceof TypeConAryB ary && _widen==ary._widen && Arrays.equals(_ary,ary._ary); } @Override int hash() { return Arrays.hashCode(_ary); } - @Override public void write( ByteArrayOutputStream baos ) { baos.write(_ary,0,_ary.length); } + + @Override public void write( BAOS baos ) { baos.write(_ary); } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeFloat.java b/src/main/java/com/seaofnodes/simple/type/TypeFloat.java index bb00ee91..8638b8c9 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeFloat.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeFloat.java @@ -1,9 +1,10 @@ package com.seaofnodes.simple.type; -import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.AryInt; +import com.seaofnodes.simple.util.Utils; import java.util.ArrayList; +import java.util.HashMap; /** * Float Type @@ -51,7 +52,7 @@ public class TypeFloat extends TypeScalar { case 0 -> ""+_con+((float)_con==_con ? "f" : ""); case 32 -> "f32"; case 64 -> "flt"; - default -> throw Utils.TODO(); + default -> throw Utils.TODO("Should not reach here: unknown float size"); }; } private boolean isF32() { return ((float)_con)==_con; } @@ -88,16 +89,16 @@ public class TypeFloat extends TypeScalar { return _isConstant() ? this : new TypeFloat((byte)-_sz,0); // Constants are a self-dual } - @Override boolean _isGLB(boolean mem) { return _glb(mem)==this; } - @Override Type _glb(boolean mem) { - if( !mem ) return F64; + @Override boolean _isConstant() { return _sz==0; } + + @Override Type _makeStorage() { if( _isConstant() ) return isF32() ? F32 : F64; return isHigh() ? dual() : this; } - @Override boolean _isConstant() { return _sz==0; } - @Override public Type makeZero() { return FZERO; } + + @Override int hash() { return (int)(Double.hashCode(_con) ^ _sz ^ (1<<17)); } @Override diff --git a/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java b/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java index 9acf5c9f..301fcc8b 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeFunPtr.java @@ -1,8 +1,6 @@ package com.seaofnodes.simple.type; -import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; -import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.*; import java.util.*; /** @@ -21,26 +19,25 @@ public class TypeFunPtr extends TypeNil { // A TypeFunPtr is Signature and a set of functions. public Type[] _sig; public Type _ret; - // TODO: - boolean _open; // Extra args are true:BOTTOM, false:TOP - // Hidden initializers and allocators can exceed a machine word's worth - // of functions. Use the expandable sets from the later chapters, without - // adding their escape summaries or cross-file identity remapping. + boolean _open; // Extra args are true:scalar BOT, false:scalar TOP + // Set of functions. Bit zero is used for mutable/immutable safety. Sign + // bit repeats infinitely. public int[] _fidxs; private static final Ary<TypeFunPtr> FREE = new Ary<>(TypeFunPtr.class); - private TypeFunPtr(byte nil, boolean open, Type[] sig, Type ret, int[] fidxs) { super(TFUNPTR,nil); init(nil,open,sig,ret,fidxs); } + private TypeFunPtr(byte nil) { super(TFUNPTR,nil); } private TypeFunPtr init(byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { assert sig != null; _nil = nil; _open = open; _sig = sig; _ret = ret; + assert _fidxs == null; _fidxs= XInt.intern(fidxs); return this; } static TypeFunPtr malloc( byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { - return FREE.isEmpty() ? new TypeFunPtr(nil,open,sig,ret,fidxs) : FREE.pop().init(nil,open,sig,ret,fidxs); + return (FREE.isEmpty() ? new TypeFunPtr(nil) : FREE.pop()).init(nil,open,sig,ret,fidxs); } @Override TypeFunPtr free(Type t) { TypeFunPtr fun = (TypeFunPtr)t; @@ -49,78 +46,106 @@ public class TypeFunPtr extends TypeNil { fun._ret = null; fun._dual = null; fun._hash = 0; + fun._fidxs = null; FREE.push(fun); return this; } @Override boolean isFree() { return _sig==null; } // All fields directly listed - public static TypeFunPtr make(byte nil, boolean open, Type[] sig, Type ret, long bits) { - int[] set=bits<0 ? XInt.FULL : XInt.EMPTY; - if( bits>=0 ) for( int bit=0; bit<63; bit++ ) - if( (bits&(1L<<bit))!=0 ) set=XInt.make(set,bit); - return make(nil,open,sig,ret,set); - } public static TypeFunPtr make( byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { - // Trim trailing args which match the implicit default. - Type def = open ? Type.BOTTOM : Type.TOP; - int nargs = sig.length; - while( nargs > 0 && sig[nargs-1] == def ) - nargs--; - if( nargs != sig.length ) - sig = nargs == 0 ? TEMPTY : Arrays.copyOf(sig,nargs); TypeFunPtr fun = malloc(nil,open,sig,ret,fidxs); TypeFunPtr f2 = fun.intern(); if( f2==fun ) return fun; return VISIT.isEmpty() ? f2.free(fun) : f2.delayFree(fun); } - public static TypeFunPtr make( boolean nil, boolean open, Type[] sig, Type ret ) { return make((byte)(nil ? 3 : 2),open,sig,ret,-1); } + // Trim trailing args which match the default. + private static TypeFunPtr makeAdjusted( byte nil, boolean open, Type[] sig, Type ret, int[] fidxs ) { + Type def = open ? TypeScalar.BOT : TypeScalar.TOP; + int nargs = sig.length; + while( nargs > 0 && sig[nargs-1] == def ) + nargs--; + if( nargs != sig.length ) + sig = nargs == 0 ? ARG_EMPTY : Arrays.copyOf(sig,nargs); + return make(nil,open,sig,ret,fidxs); + } + public static TypeFunPtr make( boolean nil, Type[] sig, Type ret ) { return makeAdjusted((byte)(nil ? 3 : 2),true,sig,ret, XInt.FULL); } + public static TypeFunPtr make1( byte nil, boolean open, Type[] sig, Type ret, int fidx ) { + return makeAdjusted(nil,open,sig,ret,XInt.make(fidx)); + } @Override TypeFunPtr makeFrom( byte nil ) { return nil ==_nil ? this : make( nil,_open,_sig,_ret, _fidxs); } public TypeFunPtr makeFrom( Type ret ) { return ret ==_ret ? this : make( _nil,_open,_sig, ret, _fidxs); } - public TypeFunPtr makeFrom( int fidx ) { return make((byte)2, _open, _sig,_ret,XInt.make(fidx) ); } - + public TypeFunPtr makeFrom( int fidx ) { return make1(_nil,_open,_sig,_ret,fidx); } + public TypeFunPtr makeFrom( Type arg, int idx ) { + // Alter named arg + Type[] sig = _sig.clone(); + sig[idx] = arg; + return make(_nil,_open,sig,_ret,_fidxs); + } + @Override public Type nonZero() { return makeFrom((byte)(_nil <= 1 ? 1 : 2)); } public static final Type[] TEMPTY = new Type[0]; static final Type[] TINT = new Type[]{TypeInteger.BOT}; static final Type[] TINTMEM = new Type[]{TypeInteger.BOT,TypeFloat.F32}; static final Type[] TINTINT = new Type[]{TypeInteger.BOT,TypeInteger.BOT}; - public static TypeFunPtr BOT = make((byte)3,true ,TEMPTY,Type.BOTTOM,-1); - public static TypeFunPtr TEST = make((byte)2,false,TINTMEM,TypeInteger.BOT, 1); - public static TypeFunPtr TEST0 = make((byte)3,false,TINTMEM,TypeInteger.BOT, 3); - public static TypeFunPtr MAIN = make((byte)3,false,TINT ,Type.BOTTOM,-1); // Main can return anything - public static TypeFunPtr CALLOC= make((byte)3,false,TINTINT,TypeMemPtr .BOT,-1); + public static TypeFunPtr BOT = make((byte)3,true,TEMPTY,Type.BOTTOM, XInt.FULL); + public static TypeFunPtr TEST = make((byte)2,true,TINTMEM,TypeInteger.BOT, XInt.SET1); + public static TypeFunPtr TEST0 = make((byte)3,true,TINTMEM,TypeInteger.BOT, XInt.SET3); + public static TypeFunPtr MAIN = make((byte)3,true,TINT ,Type.BOTTOM, XInt.FULL); // Main can return anything + public static TypeFunPtr CALLOC= make((byte)3,true,TINTINT,TypeMemPtr .BOT, XInt.FULL); public static void gather(ArrayList<Type> ts) { - ts.add(TEST); ts.add(TEST0); ts.add(BOT); ts.add(MAIN); ts.add(CALLOC); + ts.add(TEST); + ts.add(TEST0); + ts.add(BOT); + ts.add(MAIN); + ts.add(CALLOC); + ts.add(make((byte)2,false,TINT,TypeInteger.I32, XInt.SET1)); + ts.add(make((byte)2,true ,TINT,TypeInteger.BOT, XInt.SET3)); + ts.add(make1((byte)2,true, + new Type[]{TypeInteger.BOT}, + TypeInteger.BOT, 65)); + ts.add(make1((byte)2,true, + new Type[]{TypeInteger.BOT, TypeScalar.BOT}, + TypeInteger.BOT, 65)); // Explicit trailing defaults must agree with the implicit argument tail. for( boolean open : new boolean[]{false,true} ) { - Type def = open ? Type.BOTTOM : Type.TOP; - ts.add(make((byte)2,open,TINT,TypeInteger.I32,1)); - ts.add(make((byte)2,open,new Type[]{TypeInteger.BOT,def,def},TypeInteger.I32,1)); - ts.add(make((byte)2,open,new Type[]{def,def},TypeInteger.I32,1)); - ts.add(make((byte)2,open,new Type[]{def,TypeInteger.BOT},TypeInteger.I32,1)); + Type def = open ? TypeScalar.BOT : TypeScalar.TOP; + ts.add(make1((byte)2,open,TINT,TypeInteger.I32,1)); + ts.add(make1((byte)2,open,new Type[]{TypeInteger.BOT,def,def},TypeInteger.I32,1)); + ts.add(make1((byte)2,open,new Type[]{def,def},TypeInteger.I32,1)); + ts.add(make1((byte)2,open,new Type[]{def,TypeInteger.BOT},TypeInteger.I32,1)); } } + private static final Type[] ARG_EMPTY = new Type[0]; @Override Type xmeet(Type t) { TypeFunPtr that = (TypeFunPtr) t; - // if equal, no matters. - // if short is BOT, chop ; recurPre on short. - // if short is TOP, copy long; recurPre on long. - TypeFunPtr min = _sig.length < that._sig.length ? this : that; - if( _sig.length != that._sig.length && (min._open ^ min==this) ) - return that.xmeet(this); - // Recurse all common fields - Type[] args = new Type[_sig.length]; - for( int i=0; i<min._sig.length; i++ ) - args[i] = _sig[i].meet(that._sig[i]); - // Handle extra args - for( int i=min._sig.length; i<_sig.length; i++ ) - args[i] = _sig[i]; - return make(xmeet0(that),_open | that._open, args, _ret.meet(that._ret), XInt.meet(_fidxs,that._fidxs)); + boolean open = _open | that._open; + Type def0 = this._open ? TypeScalar.BOT : TypeScalar.TOP; + Type def1 = that._open ? TypeScalar.BOT : TypeScalar.TOP; + Type def = open ? TypeScalar.BOT : TypeScalar.TOP; + + int nargs = Math.max(_sig.length,that._sig.length); + Type[] args = new Type[nargs]; + + for( int i=0; i<nargs; i++ ) { + Type t0 = i < this._sig.length ? this._sig[i] : def0; + Type t1 = i < that._sig.length ? that._sig[i] : def1; + args[i] = t0.meet(t1); + } + + while( nargs>0 && args[nargs-1] == def ) + nargs--; + + if( nargs != args.length ) + args = nargs == 0 ? ARG_EMPTY : Arrays.copyOf(args,nargs); + + return make(xmeet0(that), open, args, _ret.meet(that._ret), XInt.meet(_fidxs,that._fidxs)); } + static Type[] xmeet( Type[] ts0, Type[] ts1) { if( ts0==ts1 ) return ts0; assert ts0.length==ts1.length; @@ -142,7 +167,7 @@ public class TypeFunPtr extends TypeNil { if( _dual!=null ) return dual(); assert !_terned; Type[] sigs = new Type[_sig.length]; - TypeFunPtr d = malloc(dual0(),!_open,sigs,null,XInt.dual(_fidxs)); + TypeFunPtr d = malloc(dual0(),!_open,sigs,null, XInt.dual(_fidxs)); (_dual = d)._dual = this; // Cross link duals // Return rdual d._ret = _ret._terned ? _ret.dual() : _ret.rdual(); @@ -152,24 +177,39 @@ public class TypeFunPtr extends TypeNil { return d; } - // RHS is NIL; do not deep-dual when crossing the centerline - @Override public Type meet0() { return _nil==3 ? this : make((byte)3,_open,_sig,_ret,_fidxs); } + // RHS is NIL; deep-dual when crossing the centerline + @Override public Type meet0() { + if( _nil==3 ) return this; + if( _nil==2 ) return make((byte)3,_open,_sig,_ret,_fidxs); - @Override public boolean isHigh() { return _nil <= 1 || (_nil==2 && _fidxs==XInt.EMPTY); } + return make((byte)3,_open,_sig,_ret,_fidxs); + } - @Override public boolean _isConstant() { return (_nil==2 && XInt.isConstant(_fidxs)) || (_nil==3 && _fidxs==XInt.EMPTY); } + @Override public boolean isHigh() { return _nil <= 1 || (_nil==2 && _fidxs== XInt.EMPTY); } + + @Override public boolean _isConstant() { return (_nil==2 && XInt.isConstant(_fidxs)) || (_nil==3 && _fidxs== XInt.EMPTY); } @Override boolean _isFinal() { return true; } - @Override boolean _isGLB(boolean mem) { return true; } - @Override TypeFunPtr _glb(boolean mem) { return this; } - @Override TypeFunPtr _close(java.util.HashMap<String,Type> types) { + @Override TypeFunPtr _makeStorage() { return this; } + + @Override TypeFunPtr _close( String name, HashMap<String, Type> TYPES ) { Type[] sig = new Type[_sig.length]; - TypeFunPtr fun = malloc(_nil,false,sig,null,_fidxs); + TypeFunPtr fun = malloc(_nil,true,sig,null,_fidxs); // Now start the recursion - fun._ret = _ret._close(types); + fun._ret = _ret._close(name, TYPES ); for( int i=0; i<sig.length; i++ ) - sig[i] = _sig[i]._close(types); + sig[i] = _sig[i]._close(name, TYPES ); + return fun; + } + // Replace recursively all TypeBuilders with cyclic TypeStructs + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + Type[] sig = new Type[_sig.length]; + TypeFunPtr fun = malloc(_nil,_open,sig,null,_fidxs); + // Now start the recursion + fun._ret = _ret._upgradeType(TYPES); + for( int i=0; i<sig.length; i++ ) + sig[i] = _sig[i]._upgradeType(TYPES); return fun; } @@ -179,7 +219,7 @@ public class TypeFunPtr extends TypeNil { public int[] fidxs() { return _fidxs; } public Type ret() { return _ret; } public int nargs() { return _sig.length; } - public int fidx() { assert XInt.isConstant(_fidxs); return XInt.onlyBit(_fidxs); } + public int fidx() { return XInt.onlyBit(_fidxs); } public int nfcns() { if( XInt.isHigh(_fidxs) ) return Integer.MAX_VALUE; // Infinite choices return XInt.bitCount(_fidxs); @@ -187,15 +227,16 @@ public class TypeFunPtr extends TypeNil { // Used to walk over type structures - @Override int nkids() { return _sig.length+1; } - @Override Type at( int idx ) { + @Override public int nkids() { return _sig.length+1; } + @Override public Type at( int idx ) { return idx == _sig.length ? _ret : _sig[idx]; } - @Override void set( int idx, Type t ) { + @Override public void set( int idx, Type t ) { if( idx == _sig.length ) _ret = t; else _sig[idx] = t; } + @Override int hash() { long hash = _ret.hashCode() ^ XInt.hash(_fidxs) ^ super.hash(); @@ -247,36 +288,26 @@ public class TypeFunPtr extends TypeNil { return "{"+printFIDX()+"}"; } + @Override SB _print(SB sb, BitSet visit, boolean html ) { if( this==BOT ) return sb.p("FunBot"); if( this==BOT.dual() ) return sb.p("FunTop"); sb.p(x()).p("{ "); if( _sig!=null ) for( Type t : _sig ) - sb.p(t.str()).p(" "); // Short form in signature + sb.p(t==null ? "---" : t.str()).p(" "); // Short form in signature + if( !_open ) sb.p("^ "); _ret.print(sb.p(html ? "→ " : "-> "),visit,html).p(" #"); // Print fidxs return sb.p(printFIDX()).p("}").p(q()); } String printFIDX() { String tilde = isHigh() ? "~" : ""; - int[] fidxs = isHigh() ? XInt.dual(_fidxs) : _fidxs; - String fidx = fidxs==XInt.EMPTY ? "" : XInt.isConstant(fidxs) ? ""+XInt.onlyBit(fidxs) - : fidxs==XInt.FULL ? "ALL" : XInt.str(fidxs); + String fidx = XInt.str(_fidxs); return tilde+fidx; } + // Usage: for( long fidxs=fidxs(); fidxs!=0; fidxs=nextFIDX(fidxs) ) { int fidx = Long.numberOfTrailingZeros(fidxs); ... } + //public static long nextFIDX(long fidxs) { return fidxs & (fidxs-1); } - - - @Override Type _upgradeType(java.util.HashMap<String,Type> types) { - TypeFunPtr old = (TypeFunPtr)VISIT.get(_uid); - if( old!=null ) return old; - Type[] sig = new Type[_sig.length]; - TypeFunPtr fun = malloc(_nil,_open,sig,null,_fidxs); - VISIT.put(_uid,fun); - fun._ret = _ret._upgradeType(types); - for( int i=0; i<sig.length; i++ ) sig[i] = _sig[i]._upgradeType(types); - return fun; - } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java index 1b0019d4..a2cf08b0 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java @@ -1,9 +1,10 @@ package com.seaofnodes.simple.type; import com.seaofnodes.simple.util.Ary; -import com.seaofnodes.simple.util.SB; +import com.seaofnodes.simple.util.AryInt; import com.seaofnodes.simple.util.Utils; import java.util.ArrayList; +import java.util.HashMap; /** * Integer Type @@ -50,8 +51,8 @@ public class TypeInteger extends TypeScalar { return this; } - public Type same_but_slightly_wider_than( Type minType ) { - return _widen < 3 ? make(_min,_max, (byte)(_widen+1)) : minType; + public Type same_but_slightly_wider_than( ) { + return _widen < 3 ? make(_min,_max, (byte)(_widen+1)) : BOT; } // Force max widen @Override public Type widen() { @@ -121,14 +122,17 @@ public class TypeInteger extends TypeScalar { if( this==I16 || this==U16 ) return 1; // 1<<1 == 2 bytes if( this==I32 || this==U32 ) return 2; // 1<<2 == 4 bytes if( this==BOT ) return 3; // 1<<3 == 8 bytes - if( _isConstant() ) { // just const here - if (-0xFF <= _min && _min <= 0xFF) return 0; - else if (-0xFFFF <= _min && _min <= 0xFFFF) return 1; - else if (-0xFFFFFFFFL <= _min && _min <= 0xFFFFFFFFL) return 2; - else return 3; - } - if( -128 <= _min && _max < 128 ) return 0; // 1 byte - throw Utils.TODO(); + if( _isConstant() ) return log(_min); // just const here + int lo = log(_min); + int hi = log(_max); + return Math.max(lo,hi); + } + + private static int log(long x) { + if( -0xFFL <= x && x <= 0xFFL) return 0; + if( -0xFFFFL <= x && x <= 0xFFFFL) return 1; + if(-0xFFFFFFFFL <= x && x <= 0xFFFFFFFFL) return 2; + return 3; } public long value() { assert _isConstant(); return _min; } @@ -157,7 +161,7 @@ public class TypeInteger extends TypeScalar { return make(Math.min(_min,i._min), Math.max(_max,i._max), (byte)Math.max(_widen,i._widen)); } - @Override TypeInteger xdual() { return malloc(_max,_min,(byte)(3-_widen)); } + @Override TypeInteger xdual() { return malloc(_max,_min,(byte)(3-_widen)); } @Override public TypeInteger nonZero() { if( isHigh() ) return this; @@ -168,14 +172,12 @@ public class TypeInteger extends TypeScalar { } @Override public Type makeZero() { return ZERO; } @Override boolean _isConstant() { return _min == _max; } - @Override boolean _isGLB(boolean mem) { return _glb(mem)==this; } - @Override Type _glb(boolean mem) { - if( !mem ) return BOT; - // GLB is not well-defined in memory; depends on the size of the memory field + @Override Type _makeStorage() { if( _isConstant() ) return BOT; return isHigh() ? dual() : this; } + @Override int hash() { return Utils.fold(_min) * Utils.fold(_max) + _widen; } @Override public boolean eq( Type t ) { return t instanceof TypeInteger i && _min==i._min && _max==i._max && _widen==i._widen; diff --git a/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/src/main/java/com/seaofnodes/simple/type/TypeMem.java index cc287922..4234a5e5 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeMem.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeMem.java @@ -1,37 +1,51 @@ package com.seaofnodes.simple.type; -import com.seaofnodes.simple.util.Ary; -import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.util.*; import java.util.ArrayList; import java.util.BitSet; +import java.util.HashMap; /** - * Public memory holds one field alias; private memory holds one whole object. + * Represents a slice of memory corresponding to a set of aliases */ public class TypeMem extends Type { // Which slice of memory? // 1 and TOP means no slice. // 1 and BOT means all memory. - // 1 and TypeStruct means current fields of the private allocation. // N means alias slice#N. public int _alias; - public Type _t; // Scalar public contents, or private field values. + public Type _t; // Memory contents, some scalar type + public boolean _one; // Singleton memory, corresponding to TypeMemPtr._one. + public boolean _clz; // Class-object singleton memory; true singletons stay constant after construction. + public boolean _final;// Memory is only set in the constructor - // Addresses reachable through this memory. EMPTY means no exposed address; - // FULL means any address. These discovery sets join by union, independently - // of the field contents. They use the same interned sets as function types. - public int[] _escFs, _escAs; + // Escaped aliases and functions - escaped into this memory. These are + // reachable from some chain of indirections from `_t`. Bit zero is + // used for mutable/immutable safety. Sign bit repeats infinitely. + public int[] _escFs; + public int[] _escAs; + + static public int CNT; private static final Ary<TypeMem> FREE = new Ary<>(TypeMem.class); - private TypeMem(int alias, Type t, int[] fs, int[] as) { super(TMEM); init(alias,t,fs,as); } - private TypeMem init(int alias, Type t, int[] fs, int[] as) { - assert alias>1 || (t==null || t==Type.TOP || t==Type.BOTTOM || t instanceof TypeStruct); + + private TypeMem() { super(TMEM); CNT++; } + private TypeMem init(int alias, Type t, boolean one, boolean clz, boolean xfinal, int[] fidxs, int[] aliases) { _alias = alias; _t = t; - _escFs=fs; _escAs=as; + _one = one; + _clz = clz; + // Public bulk memory is an aggregate of otherwise-unsplit aliases; + // a single final bit cannot describe it. Keep the lattice-dual high + // form distinct, and retain finality for private singleton memory and + // precise alias slices. + _final = t!=null && alias==1 && !one && !t.isHigh() ? false : xfinal; + + assert _escFs == null && _escAs == null; + _escFs = XInt.intern(fidxs); + _escAs = XInt.intern(aliases); return this; } @Override TypeMem free(Type t) { @@ -39,82 +53,61 @@ public class TypeMem extends Type { mem._alias= -99; mem._dual = null; mem._hash = 0; + mem._t = null; + mem._one = false; + mem._clz = false; + mem._final = false; + mem._escFs = null; + mem._escAs = null; FREE.push(mem); return this; } @Override boolean isFree() { return _alias == -99; } - public static TypeMem malloc(int alias, Type t, int[] fs, int[] as) { - return FREE.isEmpty() ? new TypeMem(alias,t,fs,as) : FREE.pop().init(alias,t,fs,as); - } - public static TypeMem make(int alias, Type t) { - return make(alias,t,t.isHigh() ? XInt.EMPTY : XInt.FULL,t.isHigh() ? XInt.EMPTY : XInt.FULL); + public static TypeMem malloc(int alias, Type t, boolean one, boolean clz, boolean xfinal, int[] fidxs, int[] aliases) { + return (FREE.isEmpty() ? new TypeMem() : FREE.pop()).init(alias,t,one,clz,xfinal,fidxs,aliases); } - public static TypeMem make(int alias, Type t, int[] fs, int[] as) { - TypeMem f = malloc(alias,t,fs,as); + public static TypeMem make(int alias, Type t, boolean one, boolean clz, boolean xfinal, int[] fidxs, int[] aliases) { + if( fidxs ==null ) fidxs = XInt.EMPTY; + if( aliases==null ) aliases= XInt.EMPTY; + TypeMem f = malloc(alias,t,one,clz,xfinal, fidxs, aliases); TypeMem f2 = f.intern(); if( f2==f ) return f; return VISIT.isEmpty() ? f2.free(f) : f2.delayFree(f); } + public static TypeMem make(int alias, Type t) { return make(alias,t,false,false,false,null,null); } + // Make a private memory + public static TypeMem makePrivate(Type t) { return make(1,t,true,false,false,escapeFIDX(XInt.EMPTY,t), escapeAlias(XInt.EMPTY,t)); } + + public TypeMem makeFrom(int alias) { return make(alias,_t,_one,_clz,_final,_escFs,_escAs); } + public TypeMem makeFrom(int[] fidxes, int[] aliases) { + return make(_alias,_t,_one,_clz,_final,fidxes,aliases); + } + // Existing escapes plus `t` escapes + public TypeMem escapes(Type t) { + return make(_alias,_t,_one,_clz,_final, escapeFIDX(_escFs,t), escapeAlias(_escAs,t)); + } + // Existing escapes plus `t` escapes + public TypeMem escapesFrom(Type t) { + return make(_alias,_t,_one,_clz,_final, escapeFIDX(_escFs,t), escapeAlias(_escAs,t)); + } + public TypeMem escapesAliases(Type t) { + return make(_alias,_t,_one,_clz,_final, _escFs, escapeAlias(_escAs,t)); + } - // Private memory describes one allocation. It is passed explicitly to - // initializers; it never joins the public heap until EscapeNode. - public static TypeMem makePrivate(TypeStruct shape) { - return make(1,shape,XInt.EMPTY,XInt.EMPTY).escapes(shape); - } - - public TypeMem escapes(Type value) { - int[] fs=_escFs, as=_escAs; - if( value instanceof TypeMem m ) { - fs=XInt.meet(fs,m._escFs); as=XInt.meet(as,m._escAs); - } else if( value instanceof TypeFunPtr f ) { - fs=XInt.meet(fs,f.fidxs()); - } else if( value instanceof TypeMemPtr p ) { - return escapes(p._obj); - } else if( value instanceof TypeStruct s ) { - if( s._open && !s.isAry() ) return make(_alias,_t,XInt.FULL,XInt.FULL); - for( Field f : s._fields ) { - // A constructor has a hidden memory result, never a public - // callable address. Constant methods need no memory Store. - if( f._t instanceof TypeFunPtr fn && fn.ret() instanceof TypeMem ) continue; - as=XInt.make(as,f._alias); - if( f._one && f._t instanceof TypeFunPtr fn ) fs=XInt.meet(fs,fn.fidxs()); - } - } else if( value==Type.BOTTOM || value==TypeScalar.BOT ) { - fs=XInt.FULL; as=XInt.FULL; - } - return make(_alias,_t,fs,as); - } - - public TypeMem withContents(int alias, Type contents) { - return make(alias,contents,_escFs,_escAs); - } - - public static TypeStruct empty(TypeStruct shape) { - Field[] fields = new Field[shape._fields.length]; - for( int i=0; i<fields.length; i++ ) - fields[i] = shape._fields[i].makeFrom(Type.BOTTOM); - return TypeStruct.make(shape._name,shape._open,fields); - } - - public static TypeStruct zeroed(TypeStruct shape) { - Field[] fields=new Field[shape._fields.length]; - for( int i=0; i<fields.length; i++ ) { - Field f=shape._fields[i]; - Type zero=f._t.makeZero(); - fields[i]=f.makeFrom(!f._final && f._t!=Type.BOTTOM && zero.isa(f._t) ? zero : Type.BOTTOM); - } - return TypeStruct.make(shape._name,shape._open,fields); - } - - public static final TypeMem TOP = make(1, Type.TOP ); - public static final TypeMem BOT = make(1, Type.BOTTOM); - public static final TypeMem START = make(1,Type.BOTTOM,XInt.EMPTY,XInt.EMPTY); + public static final TypeMem TOP = make(1, Type.TOP ,true ,true, true , XInt.EMPTY, XInt.EMPTY); + public static final TypeMem BOT = make(1, Type.BOTTOM,false,false,true , XInt.FULL , XInt.FULL ); + public static final TypeMem START= make(1,Type.BOTTOM,false,false,false, XInt.EMPTY, XInt.EMPTY); + public static final TypeMem SELF_MEM = make(1, TypeStruct.BOT); public static void gather(ArrayList<Type> ts) { - ts.add(make(2,Type.NIL)); ts.add(make(2,TypeInteger.ZERO)); ts.add(BOT); - ts.add(START); ts.add(make(2,TypeInteger.ZERO,XInt.make(3),XInt.make(5))); - ts.add(makePrivate(TypeStruct.TEST)); ts.add(makePrivate(empty(TypeStruct.TEST))); + ts.add(make(2,Type.NIL)); + ts.add(make(2,TypeInteger.ZERO)); + ts.add(BOT); + ts.add(SELF_MEM); + ts.add(START); + ts.add(make(2,TypeInteger.ZERO,true,false,true,XInt.EMPTY,XInt.make(2))); + ts.add(make(2,TypeInteger.BOT,false,false,false,XInt.EMPTY,XInt.FULL)); } @Override @@ -124,74 +117,113 @@ public class TypeMem extends Type { if( that==TOP ) return this; if( this==BOT ) return BOT; if( that==BOT ) return BOT; - // Different slices lose their alias and contents together. Their - // scalar meet alone cannot describe the unsplit bulk memory. - int alias = _alias==that._alias ? _alias : - (_alias==1 && _t==Type.TOP) ? that._alias : - (that._alias==1 && that._t==Type.TOP) ? _alias : 1; - Type mt = _alias==that._alias || alias!=1 ? _t.meet(that._t) : Type.BOTTOM; - return make(alias,mt,XInt.meet(_escFs,that._escFs),XInt.meet(_escAs,that._escAs)); + // Treat a alias-1 has either very-high or very-low, according to "_t" + int alias = _alias==that._alias ? _alias // Same-same + : this._alias==1 && this._t.isHigh() ? that._alias // this is high-1 alias, so take that + : that._alias==1 && that._t.isHigh() ? this._alias // that is high-1 alias, so take this + : 1; + Type mt = _t.meet(that._t); + int[] fidxs = XInt.meet( _escFs, that._escFs ); + int[] aliases= XInt.meet( _escAs, that._escAs ); + // Singleton and class facts are kept only when both sides agree. + // Finality is a low-side fact, and flips on the dual side. + boolean highAlias1 = (this._alias==1 && this._t.isHigh()) || (that._alias==1 && that._t.isHigh()); + boolean xfinal = highAlias1 ? (_final & that._final) : (_final | that._final); + return make(alias, mt, _one & that._one, _clz & that._clz, xfinal, fidxs, aliases); } @Override - Type xdual() { return malloc(_alias,_t.dual(),XInt.dual(_escFs),XInt.dual(_escAs)); } + Type xdual() { return malloc(_alias,_t.dual(),!_one,!_clz,!_final, XInt.dual(_escFs), XInt.dual(_escAs)); } @Override TypeMem rdual() { if( _dual!=null ) return dual(); assert !_terned; - TypeMem d = malloc(_alias,null,XInt.dual(_escFs),XInt.dual(_escAs)); + TypeMem d = malloc(_alias,null,!_one,!_clz,!_final, XInt.dual(_escFs), XInt.dual(_escAs)); (_dual = d)._dual = this; // Cross link duals d._t = _t._terned ? _t.dual() : _t.rdual(); + if( d._alias==1 && !d._one ) { assert !d._t.isHigh(); d._final = false; } return d; } + @Override public TypeMem makeZero() { return TOP; } @Override public boolean isHigh() { return _t.isHigh(); } - @Override public int log_size() { throw Utils.TODO(); } - @Override boolean _isFinal() { return _t._isFinal(); } - @Override boolean _isGLB(boolean mem) { return _t instanceof TypeStruct s ? s.isGLB2() : _t._isGLB(true); } - @Override public Type _glb(boolean mem) { return make(_alias,_t instanceof TypeStruct s ? s.glb2() : _t._glb(true)); } + @Override boolean _isConstant() { return _one && _clz && _alias!= 1 && _t._isConstant(); } + @Override public int log_size() { throw Utils.TODO("Should not reach here: memory has no scalar storage size"); } + @Override boolean _isFinal() { throw Utils.TODO("Should not reach here"); } + @Override TypeMem _makeStorage() { throw Utils.TODO("Should not reach here"); /*return make(_alias,_t._makeStorage(),_one,_clz,_final,_escFs,_escAs);*/ } + + @Override TypeMem _close( String name, HashMap<String, Type> TYPES ) { return malloc(_alias,_t._close(name, TYPES ),_one,_clz,_final,_escFs,_escAs); } - @Override int hash() { return 9876543 + _alias + _t.hashCode() + XInt.hash(_escFs) + XInt.hash(_escAs); } + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + return make(_alias,_t._upgradeType(TYPES),_one,_clz,_final,_escFs,_escAs); + } + + @Override int hash() { return 9876543 + _alias + _t.hashCode() + (_one ? 8196 : 0) + (_clz ? 4098 : 0) + (_final ? 16392 : 0) + XInt.hash(_escFs) + XInt.hash(_escAs); } @Override boolean eq(Type t) { TypeMem that = (TypeMem) t; // Invariant - return _alias == that._alias && _t == that._t && _escFs==that._escFs && _escAs==that._escAs; + return _alias == that._alias && _t == that._t && _one == that._one && _clz == that._clz && _final == that._final && _escFs == that._escFs && _escAs == that._escAs; } @Override boolean cycle_eq(Type t) { TypeMem that = (TypeMem) t; // Invariant - return _alias == that._alias && _escFs==that._escFs && _escAs==that._escAs && _t.cycle_eq(that._t); + return _alias == that._alias && _one == that._one && _clz == that._clz && _final == that._final && _escFs == that._escFs && _escAs == that._escAs && _t.cycle_eq(that._t); } - @Override int nkids() { return 1; } - @Override Type at( int idx ) { return _t; } - @Override void set( int idx, Type t ) { _t = t; } + @Override public int nkids() { return 1; } + @Override public Type at( int idx ) { return _t; } + @Override public void set( int idx, Type t ) { _t = t; } + @Override public SB _print(SB sb, BitSet visit, boolean html) { if( this==BOT ) return sb.p("MemBot"); if( this==TOP ) return sb.p("MemTop"); - sb.p("#"); + sb.p('#'); + if( !_final ) sb.p('!'); // Mutable memory + if( _one ) sb.p(_clz ? 'z' : '-'); if( _alias==0 ) return sb.p(_t._type==TTOP ? "TOP" : "BOT"); - _t.print(sb.p(_alias).p(":"),visit,html); - XInt.str(sb.p(" f="),_escFs); - return XInt.str(sb.p(" a="),_escAs); - } - - @Override public String str() { return toString(); } - - @Override Type _upgradeType(java.util.HashMap<String,Type> types) { - if( _t instanceof TypeStruct old && types.get(old._name) instanceof TypeMemPtr ptr ) { - // This struct records current contents, not a forward declaration. - // Complete its field list without replacing writes with declared - // types (or turning an unwritten field into an initialized one). - TypeStruct shape=old.isAry() ? old : ptr._obj; - Field[] fields=new Field[shape._fields.length]; - for( int i=0; i<fields.length; i++ ) { - Field field=shape._fields[i], value=old.field(field._fname); - fields[i]=field.makeFrom(value==null ? Type.BOTTOM : value._t._upgradeType(types)); - } - return makePrivate(TypeStruct.make(shape._name,shape._open,fields)); - } - return make(_alias,_t._upgradeType(types),_escFs,_escAs); + sb.p(_alias).p(":["); + _t.print(sb,visit,html).p(","); + XInt.str(sb,_escFs).p(","); + return XInt.str(sb,_escAs).p("]"); + } + + @Override public String str() { + if( this==BOT ) return "MemBot"; + if( this==TOP ) return "MemTop"; + SB sb = new SB().p('#'); + if( !_final ) sb.p('!'); // Mutable memory + if( _one ) sb.p(_clz ? 'z' : '-'); + if( _alias==0 ) return sb.p(_t._type==TTOP ? "TOP" : "BOT").toString(); + sb.p(_alias).p(":[").p(_t.str()).p(","); + XInt.str(sb,_escFs).p(","); + return XInt.str(sb,_escAs).p("]").toString(); + } + + + // Tracking bulk escaped aliases and fidxs + + // Escape all FIDXs from t + private static int[] escapeFIDX( int[] xs, Type t ) { + return switch(t) { + case TypeMemPtr tmp -> escapeFIDX(xs,tmp._obj); + case TypeStruct ts -> ts._open && !ts.isAry() + ? XInt.FULL // Open structs can have any fields + : XInt.meet(xs,ts.fidxs()); + case TypeFunPtr tfp -> XInt.meet(xs,tfp.fidxs()); + case TypeMem mem -> XInt.meet(xs,mem._escFs); + case Type tt -> tt == BOTTOM ? XInt.FULL : xs; + }; + } + + private static int[] escapeAlias( int[] xs, Type t ) { + return switch(t) { + case TypeMemPtr tmp -> escapeAlias(xs,tmp._obj); + case TypeStruct ts -> ts._open && !ts.isAry() + ? XInt.FULL // Open structs can have any fields + : XInt.meet(xs,ts.aliases()); + case TypeMem mem -> XInt.meet(xs,mem._escAs); + case Type tt -> tt == BOTTOM ? XInt.FULL : xs; + }; } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java index 4a28b919..73ca6190 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java @@ -1,8 +1,8 @@ package com.seaofnodes.simple.type; import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.simple.util.AryInt; import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; import java.util.*; /** @@ -29,22 +29,26 @@ public class TypeMemPtr extends TypeNil { // Deep read-only access through this pointer; independent of slot finality. public boolean _ro; - public boolean _one; // Singleton instance + public boolean _one; // Singleton pointer value; the pointer itself is constant. + public boolean _pub; // Escaped/public pointer. False means constructor-private. private static final Ary<TypeMemPtr> FREE = new Ary<>(TypeMemPtr.class); - private TypeMemPtr(byte nil, TypeStruct obj, boolean one) { super(TMEMPTR,nil); init(nil,obj,one); } - private TypeMemPtr init(byte nil, TypeStruct obj, boolean one) { _nil=nil; _obj=obj; _ro=obj==TypeStruct.BOT; _one=one; return this; } + private TypeMemPtr(byte nil, TypeStruct obj, boolean one, boolean pub) { super(TMEMPTR,nil); init(nil,obj,one,pub); } + private TypeMemPtr init(byte nil, TypeStruct obj, boolean one, boolean pub) { _nil=nil; _obj=obj; _ro=obj==TypeStruct.BOT; _one=one; _pub=pub; return this; } // Return a filled-in TypeMemPtr; either from free list or alloc new. - private static TypeMemPtr malloc(byte nil, TypeStruct obj, boolean one) { - return FREE.isEmpty() ? new TypeMemPtr(nil,obj,one) : FREE.pop().init(nil,obj,one); + static TypeMemPtr malloc(byte nil, TypeStruct obj, boolean one, boolean pub) { + return FREE.isEmpty() ? new TypeMemPtr(nil,obj,one,pub) : FREE.pop().init(nil,obj,one,pub); } // All fields directly listed public static TypeMemPtr make(byte nil, TypeStruct obj, boolean one) { - return make(nil,obj,one,obj==TypeStruct.BOT); + return make(nil,obj,one,true); } - private static TypeMemPtr make(byte nil, TypeStruct obj, boolean one, boolean ro) { - TypeMemPtr tmp = malloc(nil, obj, one).access(ro); + public static TypeMemPtr make(byte nil, TypeStruct obj, boolean one, boolean pub) { + return make(nil,obj,one, pub,obj==TypeStruct.BOT); + } + private static TypeMemPtr make(byte nil, TypeStruct obj, boolean one, boolean pub, boolean ro) { + TypeMemPtr tmp = malloc(nil, obj, one, pub).access(ro); TypeMemPtr t2 = tmp.intern(); if( t2==tmp ) return tmp; return VISIT.isEmpty() ? t2.free(tmp) : t2.delayFree(tmp); @@ -53,6 +57,8 @@ public class TypeMemPtr extends TypeNil { TypeMemPtr tmp = (TypeMemPtr)t; tmp._nil = -99; tmp._obj = null; + tmp._one = false; + tmp._pub = true; tmp._dual = null; tmp._hash = 0; FREE.push(tmp); @@ -63,17 +69,22 @@ public class TypeMemPtr extends TypeNil { static TypeMemPtr make(byte nil, TypeStruct obj) { return make(nil, obj, false); } public static TypeMemPtr make (TypeStruct obj) { return make((byte)2, obj); } public static TypeMemPtr makeNullable(TypeStruct obj) { return make((byte)3, obj); } + public static TypeMemPtr makePrivate (TypeStruct obj) { return make((byte)2, obj, false, false); } - public TypeMemPtr makeFrom(TypeStruct obj) { return obj==_obj ? this : make(_nil, obj, _one,_ro); } + public TypeMemPtr makeFrom(TypeStruct obj) { return obj==_obj ? this : make(_nil, obj, _one, _pub,_ro); } public TypeMemPtr makeNullable() { return makeFrom((byte)3); } - @Override TypeMemPtr makeFrom(byte nil) { return nil==_nil ? this : make(nil, _obj, _one,_ro); } - public TypeMemPtr makeHigh() { return make((byte)0,_obj.makeHigh(),false); } + /** Inferred mutable local: nullable, public and non-singleton. */ + public TypeMemPtr makeVar() { return (TypeMemPtr)makeStorage(); } + /** Public-memory shape: nullable and non-singleton, preserving the referent. */ + @Override TypeMemPtr _makeStorage() { return make(_nil,_obj._makeStorage(),false,true,_ro); } + @Override TypeMemPtr makeFrom(byte nil) { return nil==_nil ? this : make(nil, _obj, _one, _pub,_ro); } + @Override public Type nonZero() { return makeFrom((byte)(_nil <= 1 ? 1 : 2)); } private TypeMemPtr access(boolean ro) { _ro=ro; return this; } public TypeMemPtr withAccess(boolean ro) { if( _ro==ro ) return this; - TypeMemPtr ptr = malloc(_nil, _obj, _one); + TypeMemPtr ptr = malloc(_nil, _obj, _one, _pub); ptr._ro = ro; TypeMemPtr res = ptr.intern(); if( res==ptr ) return ptr; @@ -103,69 +114,60 @@ public class TypeMemPtr extends TypeNil { public TypeNil xmeet(Type t) { TypeMemPtr that = (TypeMemPtr) t; assert !isFree() && !that.isFree(); - return make(xmeet0(that), (TypeStruct)_obj.meet(that._obj), _one & that._one,_ro | that._ro); + // Can't keep _one if mixing two unequal singletons; the result is not + // either singleton. Once either side is public, the meet is public. + boolean one = _one && that._one && (_obj==that._obj || !(_obj.isConAry() || that._obj.isConAry())); + return make(xmeet0(that), (TypeStruct)_obj.meet(that._obj), one, _pub | that._pub,_ro | that._ro); } @Override - TypeMemPtr xdual() { return malloc( dual0(), _obj.dual(), _one).access(!_ro); } + TypeMemPtr xdual() { return malloc( dual0(), _obj.dual(), !_one, !_pub).access(!_ro); } @Override TypeMemPtr rdual() { if( _dual!=null ) return dual(); assert !_terned; - TypeMemPtr d = malloc(dual0(), null, _one).access(!_ro); + TypeMemPtr d = malloc(dual0(), null, !_one, !_pub).access(!_ro); (_dual = d)._dual = this; // Cross link duals d._obj = _obj._terned ? _obj.dual() : _obj.rdual(); return d; } // RHS is NIL; do not deep-dual when crossing the center line - @Override Type meet0() { return _nil==3 ? this : make((byte)3,_obj,false,_ro); } + @Override Type meet0() { return _nil==3 ? this : make((byte)3,_obj,false,true,_ro); } - @Override boolean _isConstant() { return _one && _obj._isConstant(); } + @Override boolean _isConstant() { return _one; } @Override boolean _isFinal() { return _ro; } @Override TypeMemPtr _makeRO() { return withAccess(true); } - @Override boolean _isGLB(boolean mem) { return _ro && _nil==3 && !_one && _obj.isGLB2(); } - @Override TypeMemPtr _glb(boolean mem) { return make((byte)3,_obj.glb2(),false,true); } - @Override TypeMemPtr _close(HashMap<String,Type> types) { return makeFrom(_obj._close(types)); } - - - // True if this "isa" t up to named structures - @Override public boolean shallowISA( Type t ) { - if( !(t instanceof TypeMemPtr that) ) return false; - if( this==that ) return true; - if( xmeet0(that)!=that._nil || (_ro && !that._ro) ) return false; - if( _obj==that._obj ) return true; - if( _obj._name.equals(that._obj._name) ) - return true; // Shallow, do not follow matching names, just assume ok - throw Utils.TODO(); // return _obj.shallowISA(that._obj); - } + @Override TypeMemPtr _close( String name, HashMap<String, Type> TYPES ) { return malloc(_nil,_obj._close(name, TYPES ),_one,_pub).access(_ro); } - // Is forward-reference - @Override public boolean isFRef() { return _obj.isFRef(); } + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + return makeFrom((TypeStruct)_obj._upgradeType(TYPES)); + } @Override public int log_size() { return 3; } // (1<<3)==8-byte pointers - @Override int hash() { return (_ro ? 8192 : 0) ^ _obj.hashCode() ^ super.hash() ^ (_one ? 2048 : 0); } + @Override int hash() { return (_ro ? 8192 : 0) ^ _obj.hashCode() ^ super.hash() ^ (_one ? 2048 : 0) ^ (_pub ? 4096 : 0); } @Override boolean eq(Type t) { TypeMemPtr ptr = (TypeMemPtr)t; // Invariant - return _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _obj == ptr._obj; + return _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _pub == ptr._pub && _obj == ptr._obj; } @Override boolean cycle_eq(Type t) { if( t._type != TMEMPTR ) return false; TypeMemPtr ptr = (TypeMemPtr)t; // Invariant - return _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _obj.cycle_eq(ptr._obj); + return _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _pub == ptr._pub && _obj.cycle_eq(ptr._obj); } - @Override int nkids() { return 1; } - @Override Type at( int idx ) { return _obj; } - @Override void set( int idx, Type t ) { _obj = (TypeStruct)t; } + @Override public int nkids() { return 1; } + @Override public Type at( int idx ) { return _obj; } + @Override public void set( int idx, Type t ) { _obj = (TypeStruct)t; } + @Override public String str() { if( this==BOT ) return "PtrBot"; if( this==TOP ) return "PtrTop"; if( this== NOTBOT) return "*void"; - return x()+"*"+_obj.str()+q(); + return x()+"*"+(_obj==null?"---":_obj.str())+q(); } @Override SB _print(SB sb, BitSet visit, boolean html ) { @@ -174,8 +176,4 @@ public class TypeMemPtr extends TypeNil { if( this== NOTBOT) return sb.p("*void"); return _obj.print(sb.p(x()).p("*"),visit,html).p(q()); } - - @Override Type _upgradeType(java.util.HashMap<String,Type> types) { - return makeFrom((TypeStruct)_obj._upgradeType(types)); - } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeNil.java b/src/main/java/com/seaofnodes/simple/type/TypeNil.java index 32505aa2..2c1c1d76 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeNil.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeNil.java @@ -51,7 +51,6 @@ public abstract class TypeNil extends TypeScalar { public boolean nullable() { return _nil==3; } @Override boolean _isConstant() { return false; } - @Override Type _glb(boolean mem) { return Type.NIL; } final String q() { return _nil == 1 || _nil == 2 ? "" : "?"; } final String x() { return isHigh() ? "~" : ""; } diff --git a/src/main/java/com/seaofnodes/simple/type/TypePtr.java b/src/main/java/com/seaofnodes/simple/type/TypePtr.java index 75478119..7e9e70e4 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypePtr.java +++ b/src/main/java/com/seaofnodes/simple/type/TypePtr.java @@ -1,9 +1,11 @@ package com.seaofnodes.simple.type; +import com.seaofnodes.simple.util.AryInt; import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; import java.util.ArrayList; import java.util.BitSet; +import java.util.HashMap; /** * Represents a Scalar; a single register-sized value. @@ -23,7 +25,7 @@ public class TypePtr extends TypeNil { public static void gather(ArrayList<Type> ts) { ts.add(PTR); ts.add(NPTR); } - TypeNil makeFrom(byte nil) { throw Utils.TODO(); } + TypeNil makeFrom(byte nil) { throw Utils.TODO("Should not reach here: plain pointer nil state is canonical"); } @Override public TypeNil xmeet(Type t) { TypePtr that = (TypePtr) t; @@ -47,8 +49,8 @@ public class TypePtr extends TypeNil { // 0->xscalar, 1->nscalar, 2->nscalar, 3->scalar @Override Type meetX() { return _nil==0 ? XNIL : (_nil==3 ? PTR : NPTR); } - boolean _isGLB(boolean mem) { return this==PTR; } - @Override TypePtr _glb(boolean mem) { return PTR; } + @Override TypePtr _makeStorage() { return PTR; } + @Override public String str() { return STRS[_nil]; } @Override public SB _print(SB sb, BitSet visit, boolean html) { return sb.p(str()); } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeRPC.java b/src/main/java/com/seaofnodes/simple/type/TypeRPC.java index a1389540..7acc99de 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeRPC.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeRPC.java @@ -1,32 +1,25 @@ package com.seaofnodes.simple.type; import com.seaofnodes.simple.util.Ary; -import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; -import java.util.ArrayList; -import java.util.BitSet; -import java.util.HashSet; +import java.util.*; /** * Return Program Control or Return PC or RPC */ public class TypeRPC extends Type { - // A set of CallEndNode IDs (or StopNode); commonly just one. - // Basically a sparse bit set - HashSet<Integer> _rpcs; - - // If true, invert the meaning of the bits - boolean _any; + // A set of CallEndNode IDs (or StopNode); commonly just one. High-bit + // XInt sets represent complements/infinite sets. + public int[] _rpcs; private static final Ary<TypeRPC> FREE = new Ary<>(TypeRPC.class); - private TypeRPC init(boolean any, HashSet<Integer> rpcs) { _any=any; _rpcs=rpcs; return this; } + private TypeRPC init(int[] rpcs) { _rpcs = XInt.intern(rpcs); return this; } private TypeRPC() { super(TRPC); } - private static TypeRPC malloc(boolean any, HashSet<Integer> rpcs) { - return (FREE.isEmpty() ? new TypeRPC() : FREE.pop()).init(any,rpcs); + private static TypeRPC malloc(int[] rpcs) { + return (FREE.isEmpty() ? new TypeRPC() : FREE.pop()).init(rpcs); } - private static TypeRPC make(boolean any, HashSet<Integer> rpcs) { - TypeRPC rpc = (FREE.isEmpty() ? new TypeRPC() : FREE.pop()).init(any,rpcs); + private static TypeRPC make(int[] rpcs) { + TypeRPC rpc = malloc(rpcs); TypeRPC t2 = rpc.intern(); return t2==rpc ? rpc : t2.free(rpc); } @@ -41,71 +34,53 @@ public class TypeRPC extends Type { @Override boolean isFree() { return _rpcs==null; } public static TypeRPC constant(int cend) { - HashSet<Integer> rpcs = new HashSet<>(); - rpcs.add(cend); - return make(false,rpcs); + return make(XInt.make(cend)); + } + + public int rpc() { + return XInt.onlyBit(_rpcs); } - public static final TypeRPC BOT = make(true,new HashSet<>()); + + public static final TypeRPC BOT = make(XInt.FULL); private static final TypeRPC TEST2 = constant(2); private static final TypeRPC TEST3 = constant(2); + public static final TypeRPC EMPTY = make(XInt.EMPTY); public static void gather(ArrayList<Type> ts) { ts.add(BOT); ts.add(TEST2); ts.add(TEST3); } + @Override public TypeRPC makeZero() { return EMPTY; } + + @Override public String str() { - if( _rpcs.isEmpty() ) - return _any ? "$[ALL]" : "$[]"; - if( _rpcs.size()==1 ) - for( Integer rpc : _rpcs ) - return _any ? "$[-"+rpc+"]" : "$["+rpc+"]"; - return "$["+(_any ? "-" : "")+_rpcs+"]"; + if( _rpcs == XInt.FULL ) return "$[ALL]"; + if( _rpcs == XInt.EMPTY ) return "$[]"; + return "$"+XInt.str(_rpcs); } @Override public TypeRPC xmeet(Type other) { TypeRPC rpc = (TypeRPC)other; - // If the two sets are equal, the _any must be unequal (invariant), - // so they cancel and all bits are set. - if( _rpcs.equals(rpc._rpcs) ) - return BOT; - // Classic union of bit sets (which might be infinite). - HashSet<Integer> lhs = _rpcs, rhs = rpc._rpcs; - // Smaller on left - if( lhs.size() > rhs.size() ) { lhs = rpc._rpcs; rhs = _rpcs; } - - HashSet<Integer> rpcs = new HashSet<>(); - boolean any = true; - // If both sets are infinite, intersect. - if( _any && rpc._any ) { - for( Integer i : lhs ) if( rhs.contains(i) ) rpcs.add(i); - - } else if( !_any && !rpc._any ) { - // if neither set is infinite, union. - rpcs.addAll( lhs ); - rpcs.addAll( rhs ); - any = false; - } else { - // If one is infinite, subtract the other from it. - HashSet<Integer> sub = _any ? rpc._rpcs : _rpcs; - HashSet<Integer> inf = _any ? _rpcs : rpc._rpcs; - for( Integer i : inf ) - if( inf.contains(i) && !sub.contains(i) ) - rpcs.add(i); - } - return make(any,rpcs); + return make(XInt.meet(_rpcs,rpc._rpcs)); + } + + @Override TypeRPC xdual() { return malloc(XInt.dual(_rpcs)); } + @Override Type rdual() { + TypeRPC d = xdual(); + (_dual = d)._dual = this; + return d; } - @Override Type xdual() { return malloc(!_any,_rpcs); } + @Override boolean _isConstant() { return XInt.isConstant(_rpcs); } + @Override TypeRPC _makeStorage() { return this; } - @Override boolean _isConstant() { return !_any && _rpcs.size()==1; } - @Override boolean _isGLB(boolean mem) { return true; } @Override - int hash() { return _rpcs.hashCode() ^ (_any ? -1 : 0) ; } + int hash() { return XInt.hash(_rpcs); } @Override public boolean eq( Type t ) { TypeRPC rpc = (TypeRPC)t; // Contract - return _any==rpc._any && _rpcs.equals(rpc._rpcs); + return _rpcs==rpc._rpcs; } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeScalar.java b/src/main/java/com/seaofnodes/simple/type/TypeScalar.java index 1b41db13..e369e6a5 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeScalar.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeScalar.java @@ -1,31 +1,45 @@ package com.seaofnodes.simple.type; import com.seaofnodes.simple.util.SB; + import java.util.ArrayList; import java.util.BitSet; +import java.util.HashMap; /** - * Values that fit in a register or field: integers, floats and pointers. - * These bounds exclude control and memory. An unresolved load can produce a - * scalar without making the parser guess which kind of value it will hold. + * The envelope for values which can live in an ordinary register or memory + * field: integers, floats, memory pointers, and function pointers. + * + * `BOT` means any scalar value and `TOP` means no scalar value. They sit + * strictly inside the global {@link Type#BOTTOM}/{@link Type#TOP} pair, which + * also includes non-values such as control and memory. */ public class TypeScalar extends Type { protected TypeScalar(byte type) { super(type); } private TypeScalar() { this(TSCALAR); } + public static final TypeScalar BOT = new TypeScalar().intern(); public static final TypeScalar TOP = BOT.dual(); + public static void gather(ArrayList<Type> ts) { ts.add(BOT); } + /** Meet two different scalar families. */ final Type smeet(TypeScalar scalar) { - if( _type==TSCALAR ) return isHigh() ? scalar : this; - if( scalar._type==TSCALAR ) return scalar.isHigh() ? this : scalar; + if( _type == TSCALAR ) return isHigh() ? scalar : this; + if( scalar._type == TSCALAR ) return scalar.isHigh() ? this : scalar; return BOT; } - @Override Type xmeet(Type t) { return isHigh() ? t : this; } + + @Override Type xmeet(Type t) { + TypeScalar scalar = (TypeScalar)t; + return isHigh() ? scalar : this; + } + @Override TypeScalar xdual() { return new TypeScalar(); } - @Override public boolean isHigh() { return this==TOP; } - @Override boolean _isGLB(boolean mem) { return this==BOT; } - @Override Type _glb(boolean mem) { return BOT; } + @Override public boolean isHigh() { return this == TOP; } + @Override Type _makeStorage() { return BOT; } + + @Override public String str() { return isHigh() ? "~scalar" : "scalar"; } @Override SB _print(SB sb, BitSet visit, boolean html) { return sb.p(str()); } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java index 8eec6dd4..67169009 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java @@ -10,42 +10,65 @@ import java.util.*; public class TypeStruct extends Type { // A Struct has a name and a set of fields; the fields themselves have - // names, types and aliases. The name has no semantic meaning, but is - // useful for debugging. - - // During parsing a mid-declaration struct is flagged as a "open". It is - // treated as having infinite fields with correct name and type BOTTOM. + // names, types and aliases. The name is essentially another field, + // immutable and type ignored (BOT). + + /** + * `_open` and `_fref` are independent lattice coordinates. + * + * `_open` says the structural tail is not known yet. On the low side an + * open struct has infinitely many implicit BOT fields; its dual has an + * implicit TOP tail. A real declaration is therefore open while its body + * is being parsed: + * + * ```text + * struct S { int x; /* more declared fields may follow *\/ } + * ``` + * + * `_fref` says no actual declaration has contributed to the type. The + * parser may still invent fields demanded by uses, leaving a useful open + * structural constraint: + * + * ```text + * S? p; p.x; // fref S { BOT x; ... } + * ``` + * + * Meeting that constraint with a real definition ANDs `_fref` off while + * retaining and checking the invented field. Closing recursive types must + * not by itself clear `_fref`; closure discovers shape, not a declaration. + */ public String _name; // Struct name - public boolean _open; // infinite fields are all true:BOTTOM, false:TOP + public boolean _open; + public boolean _fref; public Field[] _fields; - private TypeStruct(String name, boolean open, Field[] fields) { - super(TSTRUCT); + private TypeStruct() { super(TSTRUCT); } + private static final Ary<TypeStruct> FREE = new Ary<>(TypeStruct.class); + private TypeStruct init(String name, boolean open, boolean fref, Field[] fields) { + // Check for const array element name with mutable field + //assert !name.startsWith("[~]") || fields.length!=2 || fields[1]._t==null || (fields[1]._final != fields[1]._t.isHigh()); _name = name; _open = open; + _fref = fref; _fields = fields; + return this; } - - private static final Ary<TypeStruct> FREE = new Ary<>(TypeStruct.class); // Return a filled-in TypeStruct; either from free list or alloc new. - private static TypeStruct malloc(String name, boolean open, Field[] fields) { - if( FREE.isEmpty() ) return new TypeStruct(name,open,fields); - TypeStruct ts = FREE.pop(); - assert ts.isFree(); - ts._name = name; - ts._open = open; - ts._fields = fields; - return ts; + private static TypeStruct malloc(String name, boolean open, boolean fref, Field[] fields) { + return (FREE.isEmpty() ? new TypeStruct() : FREE.pop()).init(name,open,fref,fields); } // Free ts; return this. @Override TypeStruct free(Type t) { TypeStruct ts = (TypeStruct)t; assert !ts.isFree() && !ts._terned; + ts._name = null; ts._fields = null; ts._offs = null; ts._dual = null; ts._hash = 0; + ts._aliases = null; + ts._fidxs = null; FREE.push(ts); return this; } @@ -54,83 +77,196 @@ public class TypeStruct extends Type { // All fields directly listed public static TypeStruct make(String name, boolean open, Field... fields) { - TypeStruct ts = malloc(name, open, fields); + return make(name,open,false,fields); + } + public static TypeStruct make(String name, boolean open, boolean fref, Field... fields) { + TypeStruct ts = malloc(name, open, fref, fields); TypeStruct t2 = ts.intern(); if( t2==ts ) return ts; return VISIT.isEmpty() ? t2.free(ts) : ts.delayFree(ts); } + + // Summary aliases, after interning. Skips private + // fields which need to be explicitly loaded. + private int[] _aliases; + public int[] aliases() { + if( _aliases != null ) return _aliases; + if( _fields.length==0 ) return (_aliases=XInt.EMPTY); + assert _terned; + int maxAlias = 0; + for( Field f : _fields ) + maxAlias = Math.max(maxAlias,f._alias); + int len = XInt.idx(maxAlias)+1; + if( (maxAlias&31)==31 ) len++; + int[] xs = XInt.free(len); + for( Field f : _fields ) + if( f._fname.charAt(0) != '_' && // Skip private fields. + !hiddenInit(f) && // Skip private allocation helper. + f._t != Type.TOP ) // Skip uninit fields + xs[XInt.idx(f._alias)] |= XInt.mask(f._alias); + return (_aliases=XInt.intern(xs)); + } + + // Summary fidxs, after interning. Skips private + // fields which need to be explicitly loaded. + private int[] _fidxs; + public int[] fidxs() { + if( _fidxs != null ) return _fidxs; + assert _terned; + int[] xs = XInt.EMPTY; + for( Field f : _fields ) + if( f._fname.charAt(0) != '_' && // Skip private fields. + !hiddenInit(f) && // Skip private allocation helper. + f._t instanceof TypeFunPtr tfp ) // Looking for TFP + xs = XInt.meet(xs,tfp._fidxs); + return (_fidxs = xs); + } + + // With a user constructor, the same-named function field is the hidden + // default <init> allocation helper, not part of the public class surface. + public boolean hiddenInit(Field fld) { + if( field("<ctor>") == null || !_name.startsWith("class:") ) return false; + int idx = _name.lastIndexOf('.'); + String name = _name.substring(idx<0 ? "class:".length() : idx+1); + return fld._fname.equals(name); + } + // New open struct with no fields public static TypeStruct open( String name ) { return make(name,true); } + public static TypeStruct forward( String name ) { return make(name,true,true); } + public TypeStruct defined() { return _fref ? make(_name,_open,false,_fields) : this; } - // Array + // Array, variant body public static TypeStruct makeAry(String name, TypeInteger len, int lenAlias, Type body, int bodyAlias, boolean efinal) { return make(name,false, - Field.make("#" ,len , lenAlias,true ,false), - Field.make("[]",body,bodyAlias,efinal,false)); + Field.make("#" , len, lenAlias, true ), + Field.make("[]",body,bodyAlias,efinal)); + } + // Array, known constant body + public static TypeStruct makeAry(String name, int lenAlias, int bodyAlias, TypeConAry con) { + Type len = TypeInteger.constant(con.len()); + return make(name,false, + Field.make("#" ,len , lenAlias,true), + Field.make("[]" ,con.elem(),bodyAlias,true), + Field.make("[con]",con ,bodyAlias,true)); } - public TypeStruct makeHigh() { + + public TypeStruct makeInit() { Field[] fs = new Field[_fields.length]; - for( int i=0; i<_fields.length; i++ ) - fs[i] = _fields[i].makeFrom(Type.TOP); - return make(_name,false,fs); + for( int i=0; i<_fields.length; i++ ) { + if( _fields[i]._extern ) { fs[i] = _fields[i]; continue; } + Type zero = _fields[i]._t.makeZero(); + fs[i] = _fields[i].makeFrom(_fields[i]._t != Type.BOTTOM && zero.isa(_fields[i]._t) ? zero : Type.TOP); + } + return make(_name,_open,_fref,fs); } public TypeStruct add( Field f ) { - assert _open && find(f._fname)==-1; // No double field names + assert find(f._fname)==-1; // No double field names Field[] flds = Arrays.copyOf(_fields,_fields.length+1); flds[_fields.length] = f; - return make(_name,true,flds); + return make(_name,_open,_fref,flds); } public TypeStruct replace( Field f ) { - assert !_open; Field[] flds = Arrays.copyOf(_fields,_fields.length); flds[find(f._fname)] = f; - return make(_name,false,flds); + return make(_name,_open,_fref,flds); + } + public TypeStruct remove( int i ) { + Field[] flds = new Field[_fields.length-1]; + System.arraycopy(_fields, 0,flds,0,i); + System.arraycopy(_fields,i+1,flds,i,flds.length-i); + return make(_name,_open,_fref,flds); } + public TypeStruct addOrUpdate(Field fld) { + int idx = find(fld._fname); + int len = idx == -1 ? (idx=_fields.length)+1 : _fields.length; + Field[] flds = Arrays.copyOf(_fields,len); + flds[idx] = fld; + return make(_name,_open,_fref,flds); + } public final TypeStruct close() { - return close(null); + return (TypeStruct)recurOpen()._close(_name, null).recurClose(); } - public final TypeStruct close(HashMap<String,Type> types) { - return (TypeStruct)recurOpen()._close(types).recurClose(); - } - @Override TypeStruct _close(HashMap<String,Type> types) { - Object key = isAry() ? _uid : _name; - TypeStruct ts = (TypeStruct)VISIT.get(key); + @Override TypeStruct _close( String name, HashMap<String, Type> TYPES ) { + assert (name==null) != (TYPES==null); // One or the other + String namePlusFinal = isAry() ? (_name+(_fields[1]._final ? "~" : "!")).intern() : _name; + TypeStruct ts = (TypeStruct)VISIT.get(namePlusFinal); if( ts!=null ) return ts; - if( _open && types!=null && types.get(_name) instanceof TypeMemPtr ptr && !ptr._obj._open ) - return ptr._obj._close(types); - // Closing A must not pretend that an undeclared B is an empty - // definition. The root is visited first; self references find it above. - // Keep other empty forward shapes open until their own declaration. - if( _open && _fields.length==0 && VISIT.size()>1 ) return this; - ts = recurPre(key,false); + TypeStruct base = name==null ? (TypeStruct)TYPES.get(_name) : this; + assert base != null; + boolean open = base._fref ? base._open : name != _name && name != null && _open; + ts = base.recurPre(namePlusFinal, _name, open, base._fref ); Field[] flds = ts._fields; - // Now start the recursion for( int i=0; i<flds.length; i++ ) - flds[i].setType(_fields[i]._t._close(types)); + flds[i].setType(base._fields[i]._t._close(name, TYPES)); + + // Note: this whole complex shenanigans here is because there are + // actually two flavors of any array (non-/final contents) but I + // allow the parser to represent both with the same name string. + if( isAry() && _fields[1]._final && !flds[1]._final ) // This is a final array variant + flds[1] = flds[1].makeFrom(true); // Add final to the base TYPES non-final array + + return ts; + } + // Needs full recursive treatment; e.g. + // struct LL { LL? next; int !q; } // Q is mutable field. + // Stored into a not-mutable, so need that version also: + // struct LL { LL? next; int q; } // Q is NOT mutable field. + // Now upgrade: + // it is not the default version from TYPES. + // it is fully cyclic + // + // Evil question: has no *open* internal fields, so can I assume it is fully correct and never needs upgrade? + // + // upgrades are for well-formed types with internal ptrs to early open + // versions of other types, as happens in all the CoRecur tests. + // During the co-recur tests, can I make non-default cycles with + // weaker versions of other early types? + // Seems plausible... + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + TypeStruct base = (TypeStruct)TYPES.get(_name); + if( this == base ) return this; // Already the good form + TypeStruct ts = (TypeStruct)VISIT.get(_uid); + if( ts!=null ) return ts; + // Main work: replace open with closed + if( _open ) + return base; + // Build recursive version + ts = recurPre(_uid,_name,false,_fref); + Field[] flds = ts._fields; + // Now start the recursion + for( int i=0; i<flds.length; i++ ) + flds[i].setType(_fields[i]._t._upgradeType(TYPES)); return ts; } static final AryInt CEQUALS = new AryInt(); - public static final TypeStruct BOT = open("$STRUCT"); - public static final TypeStruct TOP = BOT.dual(); + public static final String TOPNAME = "$TOP", BOTNAME = "$BOT"; + public static final TypeStruct TOP = make("$TOP",false); + public static final TypeStruct BOT = TOP.dual(); public static final TypeStruct TEST= make("test",false,Field.TEST); private static final TypeStruct ARY = makeAry("[]i64",TypeInteger.U32,-1,TypeInteger.BOT,-2,false); private static final TypeStruct STR = makeAry("[]u8" ,TypeInteger.U32,-1,TypeInteger.U8 ,-4,false); - private static final TypeStruct ABC = makeAry("[]u8",TypeInteger.constant(3),-1,TypeConAryB.ABC,-4,true); + private static final TypeStruct ABC = makeAry("[]u8",-1,-4,TypeConAryB.ABC); + // Representative unresolved forms for the exhaustive lattice tests: + // bare forward ref, forward ref constrained by a field use, and definition. + private static final TypeStruct FREF0 = forward("%FREF"); + private static final TypeStruct FREF1 = FREF0.add(Field.make("x",Type.BOTTOM,-20,false)); + private static final TypeStruct FDEF1 = make("%FREF",false,Field.make("x",TypeInteger.BOT,-20,false)); // A pair of self-cyclic types - private static final TypeStruct SINT0 = open("%SINT"); - private static final TypeStruct SFLT0 = open("%SFLT"); - private static final TypeStruct SINT1 = SINT0.add(Field.make("a", TypeInteger.U32, -1, false, false)).add(Field.make("s2",TypeMemPtr.make((byte)2,SFLT0),-2, false, false)); - private static final TypeStruct SFLT1 = SFLT0.add(Field.make("b", TypeFloat .F32, -3, false, false)).add(Field.make("s1",TypeMemPtr.make((byte)2,SINT1),-4, false, false)); - public static final TypeStruct SFLT2 = SFLT1.close(); + private static final TypeStruct SINT0 = make("%SINT",false); + private static final TypeStruct SFLT0 = make("%SFLT",false); + public static final TypeStruct SINT1 = SINT0.add(Field.make("a", TypeInteger.U32, -1, false)).add(Field.make("s2",TypeMemPtr.make((byte)2,SFLT0),-2, false)); + public static final TypeStruct SFLT1 = SFLT0.add(Field.make("b", TypeFloat .F32, -3, false)).add(Field.make("s1",TypeMemPtr.make((byte)2,SINT1),-4, false)); + @Override public boolean isHigh() { return _name==TOPNAME; } public static void gather(ArrayList<Type> ts) { ts.add(BOT); @@ -138,14 +274,15 @@ public class TypeStruct extends Type { ts.add(ARY); ts.add(STR); ts.add(ABC); + ts.add(FREF0); + ts.add(FREF1); + ts.add(FDEF1); ts.add(SINT0); ts.add(SFLT0); ts.add(SINT1); ts.add(SFLT1); - ts.add(SFLT2); - ts.add(((TypeMemPtr)(SFLT2.field("s1")._t))._obj); // Break cyclic init: built a struct - Field fcalloc = Field.make("calloc",TypeFunPtr.CALLOC,-2,true,true); + Field fcalloc = Field.make("calloc",TypeFunPtr.CALLOC,-2,true); TypeStruct scalloc = make("calloc",false,fcalloc); ts.add(scalloc); @@ -154,7 +291,7 @@ public class TypeStruct extends Type { // Find field index by name public int find(String fname) { for( int i=0; i<_fields.length; i++ ) - if( _fields[i]._fname.equals(fname) ) + if( _fields[i]._fname==fname ) return i; return -1; } @@ -180,28 +317,27 @@ public class TypeStruct extends Type { if( this==BOT ) return BOT; if( that==BOT ) return BOT; - // Within the same compilation unit, struct names are unique. If the - // names differ, its different structs. Across many compilation units, - // structs with the same name but different field layouts can be - // interned... which begs the question: - // "What is the meet of structs from two different compilation units?" - // And the answer is: "don't ask". - if( !_name.equals(that._name) ) - return BOT; // It's a struct; that's about all we know - // if equal, no matters. // if short is BOT, chop ; recurPre on short. // if short is TOP, copy long; recurPre on long. TypeStruct min = _fields.length < that._fields.length ? this : that; - if( _fields.length != that._fields.length && (min._open ^ min==this) ) + if( _fields.length != that._fields.length && (!min._open ^ min!=this) ) return that.xmeet(this); + assert (_name==that._name) == (_name.equals(that._name)); // Strings are interned + String name = + that._name==TOPNAME ? _name : + _name==TOPNAME ? that._name : + that._name==_name ? _name : + BOTNAME; + if( name==BOTNAME ) return BOT; + // Check all other fields are sanely similar; same struct type but // different field contents (e.g. field "age" is either 'int' or '18'). int len = Math.min(_fields.length,that._fields.length); for( int i=0; i<len; i++ ) { Field f0 = _fields[i], f1 = that._fields[i]; - if( !f0._fname.equals(f1._fname) || f0._alias != f1._alias || f0._one != f1._one ) + if( !f0._fname.equals(f1._fname) || f0._alias != f1._alias || f0._extern != f1._extern ) return BOT; } @@ -215,7 +351,7 @@ public class TypeStruct extends Type { // Setup and install the type, prior to recursing, so we can find our // recursive self again. - ts = recurPre(pid,_open | that._open); + ts = recurPre(pid,name,_open | that._open,_fref & that._fref); // Recurse all common fields Field[] flds = ts._fields; @@ -236,12 +372,10 @@ public class TypeStruct extends Type { @Override TypeStruct xdual() { - if( _name=="$STRUCT" ) - return malloc("$STRUCT",false,new Field[0]); Field[] flds = new Field[_fields.length]; for( int i=0; i<_fields.length; i++ ) flds[i] = _fields[i].dual(); - return malloc(_name,!_open,flds); + return malloc(sdual(),!_open,!_fref,flds); } // Recursive dual @@ -249,18 +383,23 @@ public class TypeStruct extends Type { if( _dual!=null ) return dual(); assert !_terned; Field[] flds = new Field[_fields.length]; - TypeStruct d = malloc(_name,!_open,flds); + TypeStruct d = malloc(sdual(),!_open,!_fref,flds); (_dual = d)._dual = this; // Cross link duals for( int i=0; i<_fields.length; i++ ) flds[i] = _fields[i]._terned ? _fields[i].dual() : _fields[i].rdual(); return d; } - // Is forward-reference - @Override public boolean isFRef() { return _open; } + private String sdual() { + return + _name==TOPNAME ? BOTNAME : + _name==BOTNAME ? TOPNAME : + _name; + } @Override boolean _isConstant() { - if( VISIT.containsKey(_uid) ) return true; // Cycles assume constant + assert !_open; // who is asking about constant open structs? if( _open ) return false; // Infinite BOT fields + assert !VISIT.containsKey(_uid); //expect constants to not include self-cycles. if( VISIT.containsKey(_uid) ) return true; // Cycles assume constant VISIT.put(_uid,this); // Check all fields for being constant for( Field field : _fields ) @@ -271,89 +410,76 @@ public class TypeStruct extends Type { // All fields are final @Override boolean _isFinal() { - if( _open ) return false; // May have more more non-final fields + if( _open ) return false; // May have more non-final fields if( VISIT.containsKey(_uid) ) // Test: been here before? return true; // Cycles assume final - VISIT.put(_uid,this); // Set: dont do this again + VISIT.put(_uid,this); // Set: don't do this again for( Field fld : _fields ) if( !fld._isFinal() ) return false; return true; } - private TypeStruct recurPre(Object key, boolean open) { + private TypeStruct recurPre(Object key, String name, boolean open, boolean fref) { // Make a clone of original; suitable for hashing so can build // e.g. TMPs to clone as part of cycles - but will fail the 'eq' check // until the entire cycle is built. Field[] flds = new Field[_fields.length]; for( int i=0; i<flds.length; i++ ) flds[i] = _fields[i].malloc(); // Blank copy, but can be hashed - TypeStruct ts = malloc(_name, open, flds ); + TypeStruct ts = malloc(name, open, fref, flds ); + ts._aliases = aliases(); VISIT.put(key,ts); return ts; } - // Make a read-only version - @Override TypeStruct _makeRO() { - // Check for already visited - TypeStruct ts = (TypeStruct)VISIT.get(_name); + private interface FieldTransform { Type apply(Type t); } + + private TypeStruct recurTransform(Object key, String name, boolean open, boolean fref, boolean xfinal, FieldTransform transform) { + TypeStruct ts = (TypeStruct)VISIT.get(key); if( ts!=null ) return ts; // Already visited - ts = recurPre(_name,_open); // Make a new type with blank fields + ts = recurPre(key,name,open,fref); Field[] flds = ts._fields; - for( Field fld : flds ) fld._final = true; + for( Field fld : flds ) + fld._final |= xfinal; // Now start the recursion for( int i=0; i<flds.length; i++ ) - flds[i].setType(_fields[i]._t._makeRO()); + flds[i].setType(transform.apply(_fields[i]._t)); return ts; } - - // All fields are at GLB already - boolean isGLB2() { - if( VISIT.containsKey(_uid) ) return true; // Cycles assume GLB - VISIT.put(_uid,this); - for( Field fld : _fields ) - if( !fld.isGLB2() ) - return false; - return true; + // Make a read-only version + @Override TypeStruct _makeRO() { + return recurTransform(_name,_name,_open,_fref,true,Type::_makeRO); } - // Keeps the same struct, but lower-bounds all fields. - public TypeStruct glb2() { - TypeStruct ts = (TypeStruct)VISIT.get(_name); - if( ts!=null ) return ts; - ts = recurPre(_name,_open); - Field[] flds = ts._fields; - for( int i=0; i<flds.length; i++ ) - flds[i]._final = true ; - - // Now start the recursion - for( int i=0; i<flds.length; i++ ) - flds[i].setType(_fields[i]._t._glb(true)); - - return ts; + @Override TypeStruct _makeStorage() { + return recurTransform(_uid,_name,_open,false,false,Type::_makeStorage); } // log_size for a struct is not defined, unless its exactly some power of // 2. *Total size* is well-defined, and is available in the offsets. - @Override public int log_size() { throw Utils.TODO(); } + @Override public int log_size() { throw Utils.TODO("Should not reach here: structs have no scalar log size"); } @Override public int size() { return offset(_fields.length); } @Override public int alignment() { int align = 0; for( Field f : _fields ) - align = Math.max(align, f._t.alignment()); + if( !f._extern ) align = Math.max(align, f._t.alignment()); return align; } // If false, always false. // If true , maybe true, need to check recursive fields. private boolean static_eq( TypeStruct ts ) { - return _name.equals(ts._name) && _open==ts._open && _fields.length==ts._fields.length; + assert (_name==ts._name) == (_name.equals(ts._name)); // Strings are interned + return _name==ts._name && _open==ts._open && _fref==ts._fref && _fields.length==ts._fields.length; } @Override boolean eq(Type t) { + TypeStruct ts = (TypeStruct)t; // Invariant + // Recursive; so use cyclic equals if( !VISIT.isEmpty() ) { assert CEQUALS.isEmpty(); @@ -362,7 +488,6 @@ public class TypeStruct extends Type { return rez; } // Normal equals - TypeStruct ts = (TypeStruct)t; // Invariant if( !static_eq(ts) ) return false; if( _fields==ts._fields ) return true; for( int i = 0; i < _fields.length; i++ ) @@ -390,15 +515,20 @@ public class TypeStruct extends Type { @Override int hash() { - long hash = _name.hashCode() ^ (_open ? 4 : 0); + long hash = _name.hashCode() ^ (_open ? 4 : 0) ^ (_fref ? 8 : 0); for( Field f : _fields ) hash = Utils.rot(hash,13) ^ ((long)f._fname.hashCode() * f._alias); return Utils.fold(hash); } - @Override int nkids() { return _fields.length; } - @Override Type at( int idx ) { return _fields[idx]; } - @Override void set( int idx, Type t ) { _fields[idx] = (Field)t; } + @Override public int nkids() { return _fields.length; } + @Override public Field at( int idx ) { return _fields[idx]; } + @Override public void set( int idx, Type t ) { _fields[idx] = (Field)t; } + // Tags 0-4 - closed +#nflds (+name) + // Tags 5 - closed(+ nflds +name) + // Tags 6-10- open +#nflds (+name) + // Tags 11 - open (+ nflds +name) + @Override SB _print(SB sb, BitSet visit, boolean html ) { @@ -408,12 +538,17 @@ public class TypeStruct extends Type { if( isAry() && field("[]")._t instanceof TypeConAry con ) return sb.p(con.str()); sb.p(_name); - if( html || isAry() ) + if( isAry() ) + // Array names deliberately canonicalize mutable and immutable + // element variants. The verbose debugger print must retain the + // otherwise invisible lattice coordinate. + return sb.p(_fields[1]._final ? "[final]" : "[mutable]"); + if( html ) return sb; sb.p(" {"); for( Field f : _fields ) (f._t ==null ? sb.p("---") : f._t.print(sb,visit,html)).p(f._final ? " " : " !").p(f._fname).p("; "); - if( _open ) sb.p("... "); + if( _open && !_fref ) sb.p("... "); return sb.p("}"); } @@ -424,7 +559,10 @@ public class TypeStruct extends Type { } - public boolean isAry() { return _fields.length>=2 && _fields[_fields.length-1]._fname=="[]"; } + public boolean isAry() { return find("[]")!=-1; } + public boolean isConAry() { + return isAry() && _fields[1]._t instanceof TypeConAry; + } public int aryBase() { assert isAry(); @@ -462,7 +600,7 @@ public class TypeStruct extends Type { int[] cnts = new int[5]; // Count of fields at log field size int flen = _fields.length; for( int i=0; i<flen; i++ ) - if( !_fields[i]._one ) + if( !_fields[i]._extern ) cnts[_fields[i]._t.log_size()]++; // Log size is 0(byte), 1(i16/u16), 2(i32/f32), 3(i64/dbl) int off = 0, idx = 0; // Base common struct fields go here, e.g. Mark/Klass // Compute offsets to the start of each power-of-2 aligned fields. @@ -474,7 +612,7 @@ public class TypeStruct extends Type { // Assign offsets to all fields. // Really a hidden radix sort. for( int i=0; i<flen; i++ ) { - if( _fields[i]._one ) continue; + if( _fields[i]._extern ) { _offs[idx++] = -1; continue; } int log = _fields[i]._t.log_size(); _offs[idx++] = offs[log]; // Field offset offs[log] += 1<<log; // Next field offset at same alignment @@ -499,16 +637,4 @@ public class TypeStruct extends Type { return is; } - - @Override Type _upgradeType(HashMap<String,Type> types) { - Type def = types.get(_name); - if( def instanceof TypeMemPtr ptr && !ptr._obj._open && _open ) - return ptr._obj; - TypeStruct old = (TypeStruct)VISIT.get(_uid); - if( old!=null ) return old; - TypeStruct ts = recurPre(_uid,_open); - for( int i=0; i<_fields.length; i++ ) - ts._fields[i].setType(_fields[i]._t._upgradeType(types)); - return ts; - } } diff --git a/src/main/java/com/seaofnodes/simple/type/TypeTuple.java b/src/main/java/com/seaofnodes/simple/type/TypeTuple.java index ed137182..0d70bdf2 100644 --- a/src/main/java/com/seaofnodes/simple/type/TypeTuple.java +++ b/src/main/java/com/seaofnodes/simple/type/TypeTuple.java @@ -1,14 +1,10 @@ package com.seaofnodes.simple.type; -import com.seaofnodes.simple.util.Ary; -import com.seaofnodes.simple.util.SB; -import com.seaofnodes.simple.util.Utils; +import com.seaofnodes.simple.util.*; import java.util.*; public class TypeTuple extends Type { - public TypeMem mem() { return (TypeMem)_types[1]; } - public Type[] _types; private static final Ary<TypeTuple> FREE = new Ary<>(TypeTuple.class); @@ -20,7 +16,6 @@ public class TypeTuple extends Type { // All fields directly listed public static TypeTuple make(Type... types) { TypeTuple tt = malloc(types); - for( Type t : types ) if( !t._terned ) return tt; TypeTuple t2 = tt.intern(); return t2==tt ? tt : t2.free(tt); } @@ -34,20 +29,32 @@ public class TypeTuple extends Type { } @Override boolean isFree() { return _types==null; } + public TypeTuple makeFrom(int idx, Type tx ) { + if( _types[idx]==tx ) return this; + Type[] types = _types.clone(); + types[idx] = tx; + return make(types); + } + public TypeTuple meetFrom(int idx, Type t ) { return makeFrom(idx,_types[idx].meet(t)); } public static final TypeTuple BOT = malloc(new Type[0]).intern(); public static final TypeTuple TOP = BOT.dual(); + // The default state, a triple of CONTROL, Memory, and Value, with bottoms + // for all. This is the default StopNode type, when new functions are + // being created and we cannot tell what the future holds. This is the + // default ReturnNode type, returning what the worst a function can do. + public static final TypeTuple STATE = make(Type.CONTROL,TypeMem.BOT,Type.BOTTOM); + public static final TypeTuple STOP_HIGH = make(Type.CONTROL,TypeMem.START,Type.TOP); public static final TypeTuple TEST = make(TypeInteger.BOT,TypeMemPtr.TEST); - public static final TypeTuple START= make(Type.CONTROL,TypeMem.TOP,TypeInteger.BOT); - public static final TypeTuple RET = make(Type.CONTROL,TypeMem.BOT,Type.BOTTOM); + public static final TypeTuple DEAD_NEW = make(TypeMemPtr.TOP,TypeMem.TOP); public static final TypeTuple IF_BOTH = make(Type. CONTROL,Type. CONTROL); public static final TypeTuple IF_NEITHER = make(Type.XCONTROL,Type.XCONTROL); public static final TypeTuple IF_TRUE = make(Type. CONTROL,Type.XCONTROL); public static final TypeTuple IF_FALSE = make(Type.XCONTROL,Type. CONTROL); - public static void gather(ArrayList<Type> ts) { ts.add(BOT); ts.add(TEST); ts.add(START); ts.add(IF_TRUE); } + public static void gather(ArrayList<Type> ts) { ts.add(BOT); ts.add(TEST); ts.add(STATE); ts.add(IF_TRUE); } @Override Type xmeet(Type other) { @@ -61,18 +68,23 @@ public class TypeTuple extends Type { @Override TypeTuple xdual() { if( _types.length==0 ) return malloc(null); - return malloc(TypeFunPtr.xdual(_types)); + boolean isCon=true; + for( Type t : _types ) + if( t._dual!=t ) + // Not a constant + return malloc(TypeFunPtr.xdual(_types)); + return this; } - - @Override int nkids() { return _types==null ? 0 : _types.length; } - @Override Type at(int idx) { return _types[idx]; } - @Override void set(int idx, Type t) { _types[idx]=t; } @Override TypeTuple rdual() { if( _dual!=null ) return dual(); - TypeTuple d = malloc(new Type[_types.length]); - (_dual=d)._dual=this; + assert !_terned; + assert _types.length>0; + Type[] ts = new Type[_types.length]; + TypeTuple d = malloc(ts); + (_dual = d)._dual = this; // Cross link duals + // Collection rdual for( int i=0; i<_types.length; i++ ) - d._types[i] = _types[i]._terned ? _types[i].dual() : _types[i].rdual(); + ts[i] = _types[i]._terned ? _types[i].dual() : _types[i].rdual(); return d; } @@ -82,11 +94,18 @@ public class TypeTuple extends Type { return false; return true; } - @Override boolean _isFinal() { throw Utils.TODO(); } + @Override boolean _isFinal() { throw Utils.TODO("Should not reach here: tuples are not stored object values"); } - @Override TypeMemPtr _makeRO() { throw Utils.TODO(); } + @Override TypeMemPtr _makeRO() { throw Utils.TODO("Should not reach here: tuples cannot be made read-only"); } + + @Override Type _upgradeType(HashMap<String,Type> TYPES) { + Type[] ts = new Type[_types.length]; + for( int i=0; i<ts.length; i++ ) + ts[i] = _types[i]._upgradeType(TYPES); + return make(ts); + } - @Override public int log_size() { throw Utils.TODO(); } + @Override public int log_size() { throw Utils.TODO("Should not reach here: tuples have no scalar storage size"); } @Override public int alignment() { assert isConstant(); int align = 0; @@ -95,7 +114,16 @@ public class TypeTuple extends Type { return align; } - public Type ret() { assert _types.length==3; return _types[2]; } + public Type ctl() { assert _types.length>=3; return _types[0]; } + public Type ret() { assert _types.length>=3; return _types[2]; } + public TypeMem mem() { assert _types.length>=3; + return _types[1] instanceof TypeMem tmem ? tmem : (_types[1].isHigh() ? TypeMem.START : TypeMem.BOT); + } + + @Override public int nkids() { return _types.length; } + @Override public Type at( int idx ) { return _types[idx]; } + @Override public void set( int idx, Type t ) { _types[idx] = t; } + @Override SB _print(SB sb, BitSet visit, boolean html ) { if( this==TOP ) return sb.p("[TOP]"); @@ -127,10 +155,4 @@ public class TypeTuple extends Type { return true; } - @Override Type _upgradeType(java.util.HashMap<String,Type> types) { - if( _types==null ) return this; - Type[] ts = new Type[_types.length]; - for( int i=0; i<ts.length; i++ ) ts[i] = _types[i]._upgradeType(types); - return make(ts); - } } diff --git a/src/main/java/com/seaofnodes/simple/type/XInt.java b/src/main/java/com/seaofnodes/simple/type/XInt.java index 17fc4bd3..126a4e64 100644 --- a/src/main/java/com/seaofnodes/simple/type/XInt.java +++ b/src/main/java/com/seaofnodes/simple/type/XInt.java @@ -197,7 +197,7 @@ public abstract class XInt { // Next set bit, or -1 - // for( int bit = XInt.next(bits,-1); bit >=0; bit = XInt.next(bits,bit) ) {...bit...} + // for( int bit = XInt.next(bits,0); bit >=0; bit = XInt.next(bits,bit) ) {...bit...} public static int next(int[] xs, int x) { assert !isHigh(xs); // No iterating infinite bitsets x++; // Skip current @@ -207,6 +207,9 @@ public abstract class XInt { return x; } + // Remap all bits + + public static String str(int[] xs) { return str(new SB(),xs).toString(); } public static SB str(SB sb, int[] xs) { sb.p("[ "); @@ -215,13 +218,13 @@ public abstract class XInt { // 1,2,4 prints 1,2,4 // int max = (xs.length-1)<<5; - int low = -1; // No low range; function identity 0 is main here. - for( int i=0; i<max; i++ ) { + int low = 0; // No low range + for( int i=1; i<max; i++ ) { if( bit(xs,i) ) { // Set bit, start or extend range - if( low==-1 ) low=i; // No range, start one + if( low==0 ) low=i; // No range, start one } else { // Break in sequence, ending a range range(sb,low,i-1); // Print prior range, and start new one - low=-1; // Reset range + low=0; // Reset range } } // Print any partial range @@ -232,7 +235,7 @@ public abstract class XInt { } private static SB range( SB sb, int low, int mid ) { - if( low==-1 ) ; + if( low==0 ) ; else if( low==mid ) sb.p(low).p(','); else if( low==mid-1 ) sb.p(low).p(',').p(mid).p(','); else sb.p(low).p('-').p(mid).p(','); diff --git a/src/main/java/com/seaofnodes/simple/util/BAOS.java b/src/main/java/com/seaofnodes/simple/util/BAOS.java new file mode 100644 index 00000000..61ce7387 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/util/BAOS.java @@ -0,0 +1,162 @@ +package com.seaofnodes.simple.util; + +import java.util.HashMap; +import java.util.Arrays; + +// "ByteArrayOutputStream", and Input Stream and not synchronized and has +// matching pack/unpack strategies. +public class BAOS { + + private byte[] _buf; + private int _len; + + public BAOS() { _buf = new byte[1]; } + public BAOS(byte[] buf) { _buf = buf; } + public BAOS(byte[] buf, int x) { _buf = buf; _len = x; } + + public byte[] toByteArray() { + return Arrays.copyOf(_buf,_len); + } + + public byte[] buf() { return _buf; } + public int size() { return _len; } + public void set( byte[] buf, int len ) { _buf=buf; _len=len; } + public void clear() { _len=0; } + + // ----------------------------------------------------------------------- + // Basic read, write (expand) + + private void grow(int len) { + while( _len+len >= _buf.length ) + _buf = Arrays.copyOf(_buf,_buf.length*2); + } + + // Write a byte, grow as needed; silent chop + public BAOS write(int b) { + grow(1); + _buf[_len++] = (byte)b; // Silent chop + return this; + } + + // Read an UNSIGNED bytes at INDEX, AIOOBE if off end + public int read1(int idx) { return _buf[idx] & 0xFF; } + + // Read an UNSIGNED byte, AIOOBE if off end + public int read() { return read1(_len++); } + + // write 2 bytes + public BAOS write2( int x ) { + grow(2); + _buf[_len++] = (byte)(x>> 0); + _buf[_len++] = (byte)(x>> 8); + return this; + } + + // Read a u16 in 2 bytes + public int read2(int idx) { return read1(idx) | (read1(idx+1)<<8); } + public int read2() { int rez = read2(_len); _len+=2; return rez; } + + // write 4 bytes + public BAOS write4( int x ) { + grow(4); + _buf[_len++] = (byte)(x>> 0); + _buf[_len++] = (byte)(x>> 8); + _buf[_len++] = (byte)(x>>16); + _buf[_len++] = (byte)(x>>24); + return this; + } + + // Read a i32 in 4 bytes + public int read4(int idx) { return read2(idx) | (read2(idx+2)<<16); } + public int read4() { int rez = read4(_len); _len+=4; return rez; } + + // write 8 bytes + public BAOS write8( long x ) { + write4((int) x ); + write4((int)(x>>32)); + return this; + } + + // Read a i64 in 8 bytes + public long read8(int idx) { return ((long)read4(idx) & 0xFFFFFFFFL) | ((long)read4(idx+4)<<32); } + public long read8() { long rez = read8(_len); _len+=8; return rez; } + + // Write byte array + public void write( byte[] bs ) { + grow(bs.length); + System.arraycopy(bs,0,_buf,_len,bs.length); + _len += bs.length; + } + + // Read/fill byte array + public byte[] read( byte[] bs ) { + System.arraycopy(_buf,_len,bs,0,bs.length); + _len += bs.length; + return bs; + } + + // ----------------------------------------------------------------------- + // Write a packed integer to the BAOS. Track a histogram to make more + // intelligent choices in the future. + private static final HashMap<Long,Integer> PACKED_HISTOGRAM = new HashMap<>(); + private BAOS histo(long x) { + Integer cnt = PACKED_HISTOGRAM.get(x); + PACKED_HISTOGRAM.put(x,cnt==null ? 1 : cnt+1); + return this; + } + + // Write a u8 (checked) + public void packed1(int x) { + assert 0 <= x && x <= 255; + histo(x).write(x); + } + + public int packed1() { return read(); } + + // Write a u16 (checked) + public void packed2(int x) { + assert 0 <= x && x <= 65535; + (( x <= 254 ) ? write(x) : write(0xFF).write2(x)).histo(x); + } + + // Read a packed u16 + public int packed2() { + int x = read(); + return x==0xFF ? read2() : x; + } + + // Write a packed i32 + public void packed4(int x) { + ((-127 <= x && x <= 127 ) ? write(x) : write(-128).write4(x)).histo(x); + } + + // Read a packed i32 + public int packed4() { + int x = _buf[_len++]; // Signed byte read + return x== -128 ? read4() : x; + } + + // Write a packed i64 + public void packed8(long x) { + ((-127 <= x && x <= 127 ) ? write((int)x) : write(-128).write8(x)).histo(x); + } + + // Read a packed i64 + public long packed8() { + int x = _buf[_len++]; // Signed byte read + return x== -128 ? read8() : x; + } + + public void packedS(String s) { + packed4(s.length()); + for( int i=0; i<s.length(); i++ ) + packed4(s.charAt(i)); + } + + public String packedS() { + int len = packed4(); + String s = new String(_buf,_len,len).intern(); + _len += len; + return s; + } +} diff --git a/src/main/java/com/seaofnodes/simple/util/IntHashMap.java b/src/main/java/com/seaofnodes/simple/util/IntHashMap.java new file mode 100644 index 00000000..b356ec31 --- /dev/null +++ b/src/main/java/com/seaofnodes/simple/util/IntHashMap.java @@ -0,0 +1,87 @@ +package com.seaofnodes.simple.util; + +import java.util.Arrays; + +// Int-Int Hash Map +public class IntHashMap { + private int[] _es; + private int _size; + private final int _sentinel; + + // Pick a sentinel value; not allowed for keys or values. Sentinel is + // returned on a miss. Writing sentinel counts as a remove. + public IntHashMap(int sentinel) { + _sentinel = sentinel; + _es = new int[4]; + Arrays.fill(_es,sentinel); + } + + public int size() { return _size; } + public boolean isEmpty() { return _size==0; } + public void clear() { + _size = 0; + Arrays.fill(_es,_sentinel); + } + + private int cap() { return _es.length>>1; } + + public int put( int key, int val ) { + int slot = probe(key); + int old = _es[(slot<<1)+1]; + if( old == _sentinel && val != _sentinel ) _size++; + if( old != _sentinel && val == _sentinel ) _size--; + // Check cap, resize at 3/4 full + if( _size > ((cap()>>1) + (cap()>>2)) ) { + grow(); + slot = probe(key); + } + + _es[ slot<<1 ] = key; + _es[(slot<<1)+1] = val; + return old; + } + + public int get( int key ) { + int slot = probe(key); + return _es[(slot<<1)+1]; + } + + private int probe( int key ) { + int mask = cap()-1; + int slot = key & mask; + while( _es[slot<<1] != _sentinel && _es[slot<<1] != key ) + slot = (slot + (key|1)) & mask; + return slot; + } + + // Double underlying array size + private void grow() { + int[] old = _es; + _es = new int[_es.length<<1]; + Arrays.fill(_es,_sentinel); + for( int i=0; i<old.length; i+=2 ) + if( old[i] != _sentinel ) { + int slot = probe(old[i]); + _es[ slot<<1 ] = old[i ]; + _es[(slot<<1)+1] = old[i+1]; + } + } + + // Just all the keys + public int[] keySet() { + int[] keys = new int[_size]; + int j=0; + for( int i=0; i<_es.length; i+=2 ) + if( _es[i] != _sentinel ) + keys[j++] = _es[i]; + return keys; + } + + @Override public String toString() { + SB sb = new SB().p("[ "); + for( int i=0; i<_es.length; i+=2 ) + if( _es[i] != _sentinel ) + sb.p('(').p(_es[i]).p(',').p(_es[i+1]).p(')').p(','); + return sb.unchar().p(']').toString(); + } +} diff --git a/src/main/java/com/seaofnodes/simple/util/SB.java b/src/main/java/com/seaofnodes/simple/util/SB.java index d7afd6eb..cc59f2a6 100644 --- a/src/main/java/com/seaofnodes/simple/util/SB.java +++ b/src/main/java/com/seaofnodes/simple/util/SB.java @@ -42,7 +42,7 @@ public final class SB { // 4 bytes, 8 hex digits, 32 bits, Big Endian public SB hex4(int s) { return hex2(s>>16).hex2(s); } // 8 bytes, 16 hex digits, 64 bits, Big Endian - public SB hex8(long s) { return hex4((int)(s>>32)).hex4((int)s); } + public SB hex8(long s) { return hex4((int)(s>>32)).hex4((int)s); } // Fixed width field public SB fix( int sz, String s ) { diff --git a/src/main/smp/sys.smp b/src/main/smp/sys.smp index 6a6d3ca3..7705a187 100644 --- a/src/main/smp/sys.smp +++ b/src/main/smp/sys.smp @@ -1,199 +1,11 @@ -// -*- mode: java; -*- -// top-level default import -struct sys { - // libc bindings - /** https://www.man7.org/linux/man-pages/man2/<libc call>.2.html */ - struct libc { - i32 errno = "C"; - // fd buf len -> len - { i32 i64 u32 -> u32 } write = "C"; - // fd buf len -> len - { i32 i64 u32 -> u32 } read = "C"; - // status - { i32 -> i32 } _exit = "C"; - // addr len prot flags fd off -> void* - { i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C"; - // mmap flags - i32 PROT_EXEC = 4; - i32 PROT_READ = 1; - i32 PROT_WRITE= 2; - i32 PROT_NONE = 0; - i32 MAP_PRIVATE = 2; - i32 MAP_ANON = 32; - }; - struct ary { - val copy = { u8[] dst, int doff, u8[~] src, int soff, int len -> - for( int i=0; i<len; i++ ) - dst[i+doff] = src[i+soff]; - return dst; - }; - }; +// top-level default standard library import - // Ary/Vector of u8s - a resizable array of bytes. - struct aryu8 { - u32 !len = 0; - u8[] !buf; - // Grow as needed - val _grow = { int len2 -> - if( len+len2 < buf# ) return self; - int newlen = buf# * 2; - while( len+len2 >= newlen ) newlen *= 2; - u8[] buf2 = new u8[newlen]; - sys.ary.copy(buf2,0,buf,0,buf#); - buf = buf2; - self; - }; - - // Add a character to aryu8 - val add = { u8 c -> - _grow(1).buf[len++] = c; - self; - }; - - // Add a plain array to the aryu8 - val addAry = { u8[~] ary -> - _grow(ary#); - sys.ary.copy(buf,len,ary,0,ary#); - len += ary#; - self; - }; - - // Remove x chars from the tail. - val undo = { int 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; - } - self; - }; - - // Write to fd, returns the number of bytes written (len). - val write = { i32 fd -> i64 ptr = buf; sys.libc.write(fd,ptr,len); }; - }; - - - // Scanner; a simple string parser. - struct Scan { - int !x; - u8[~] buf; - - // Skip whitespace - val skip = { -> - while( buf[x] <= ' ' ) - x++; - return self; - }; - - // True if find required character, and skip it; else False. - val match = { u8 c -> - skip(); - if( buf[x] != c ) return false; - x++; - return true; - }; - - // Parse a simple number - val number = { -> - skip(); - bool neg = match('-'); - i64 n = 0; - while( '0' <= buf[x] & buf[x] <= '9' ) - n = n*10 + (buf[x++] - '0'); - return neg ? -n : n; - }; - }; - - // Ary/Vector of i64s - struct aryi64 { - u32 !len = 0; - i64[] !buf; - // Grow as needed - val _grow = { int len2 -> - if( len+len2 < buf# ) return self; - int newlen = buf# * 2; - while( len+len2 >= newlen ) newlen *= 2; - i64[] buf2 = new i64[newlen]; - for( int i=0; i<buf#; i++ ) - buf2[i] = buf[i]; - buf = buf2; - self; - }; - - // Add a i64 to aryu8 - val add = { i64 c -> - _grow(1).buf[len++] = c; - self; - }; - - // Write to aryu8 - val str = { -> - // TODO: clunky default constructor syntax - var str = new sys.aryu8 { buf = new u8[1]; }.add('['); - for( int i=0; i<len; i++ ) - str.num(buf[i]).add(',').add(' '); - str.undo(2).add(']'); - }; - }; - - // Parse a comma-separated list of numbers, wrapped with square brackets, - // with optional whitespace e.g. "[ -17, 0,99,12456789 ] ". - // parseAryI64 & aryi64.str are bijections modulo whitespace. - // Returns null if the parse failed. - val parseAryI64 = { u8[~] str -> - !sys.Scan !s = new sys.Scan{ buf = str; }; - if( !s.match('[') ) return null; - // Array of parsed results - var nums = new sys.aryi64{ buf=new i64[1]; }; - // Parse a list of numbers - if( !s.match(']') ) { // Handle the empty list - // Parse "123, " until a something else - while( s.x < s.buf# ) { - nums.add(s.number()); - if( !s.match(',') ) - break; - }; - if( !s.match(']') ) return null; // Close the list - } - return nums; - }; - - struct io { - // Print string to stdout - val p = { u8[~] str -> - i64 ptr = str; // cast array base to i64 - return sys.libc.write(1,ptr,str#); - }; - - // Return stdin as a u8[] - val stdin = { -> - var buf = new u8[8*1024]; - i64 ptr = buf; - int len = 0; - while( true ) { - int rez = sys.libc.read(0,ptr,buf#); - if( rez < 0 ) sys.libc._exit(sys.libc.errno); - len += rez; - if( rez < buf# ) - break; - sys.libc._exit(-2); // Need to implement - } - // Compress - var dst = new u8[len]; - return sys.ary.copy(dst,0,buf,0,len); - }; - }; -}; +val adt = adt; // Abstract Data Types +val ary = ary; // Array utilities +val aryi64 = aryi64; // Variable sized array of i64 +val aryu8 = aryu8; // Variable sized array of u8 +val char = char; // Character utilities +val io = io; // IO library +val libc = libc; // libc FFI bindings +val scan = scan; // A simple string parser diff --git a/src/main/smp/sys/adt.smp b/src/main/smp/sys/adt.smp new file mode 100644 index 00000000..a4fe40bf --- /dev/null +++ b/src/main/smp/sys/adt.smp @@ -0,0 +1,3 @@ + +// Abstract data types. +val bitset = bitset; diff --git a/src/main/smp/sys/adt/bitset.smp b/src/main/smp/sys/adt/bitset.smp new file mode 100644 index 00000000..d02cd2fe --- /dev/null +++ b/src/main/smp/sys/adt/bitset.smp @@ -0,0 +1,57 @@ + +// A set of non-negative integers, represented one bit per value. +struct bitset { + i64[] !_bits; + + new bitset = { -> + _bits = new i64[1]; + }; + + // Return 0 for missing, non-zero for true + val get = { int bit -> + if( (bit >> 6) >= _bits# ) return false; + i64 mask = 1 << (bit & 63); + return (_bits[bit >> 6] & mask); + }; + + val _grow = { int bit -> + int x = bit >> 6; + int len = _bits#; + if( x < len ) return self; + while( x >= len ) len *= 2; + var bits = new i64[len]; + for( int i=0; i<_bits#; i++ ) + bits[i] = _bits[i]; + _bits = bits; + return self; + }; + + // Set + val set = { int bit -> + i64 mask = 1 << (bit & 63); + _grow(bit)._bits[bit >> 6] |= mask; + return self; + }; + + // Test and set + val tset = { int bit -> + int idx = bit >> 6; + i64 mask = 1 << (bit & 63); + if( _grow(bit)._bits[idx] & mask ) + return true; // Already set + _bits[idx] |= mask; + return false; // Was false (and now set) + }; + + val clr = { int bit -> + if( (bit >> 6) >= _bits# ) return self; + i64 mask = 1 << (bit & 63); + _bits[bit >> 6] &= (mask ^ -1); + return self; + }; + + val clear = { -> + for( int i=0; i<_bits#; i++ ) _bits[i] = 0; + return self; + }; +}; diff --git a/src/main/smp/sys/ary.smp b/src/main/smp/sys/ary.smp new file mode 100644 index 00000000..8312e94c --- /dev/null +++ b/src/main/smp/sys/ary.smp @@ -0,0 +1,24 @@ + +// Array utilities + +// Copy byte array contents. Copies the shorter of the buffers and len +val copy = { u8[] dst, int doff, u8[~] src, int soff, int len -> + if( len > dst# - doff ) len = dst# - doff; + if( len > src# - soff ) len = src# - soff; + for( int i=0; i<len; i++ ) + dst[i+doff] = src[i+soff]; + return dst; +}; + +// Compare lexigraphically array contents. Compares up to shorter array or +// 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 max = len1 > len2 ? len2 : len1; + for( int i=0; i<max; i++ ) + if( src1[off1+i] != src2[off2+i] ) + return src1[off1+i] - src2[off2+i]; // Bytes determine compare + return len1 - len2; // Length determines compare +}; diff --git a/src/main/smp/sys/aryi64.smp b/src/main/smp/sys/aryi64.smp new file mode 100644 index 00000000..98b72605 --- /dev/null +++ b/src/main/smp/sys/aryi64.smp @@ -0,0 +1,82 @@ + +// Ary/Vector of i64s +struct aryi64 { + u32 !len = 0; + i64[] !buf; + + new aryi64 = { i64[] b -> + buf = b; + }; + + // Grow as needed + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + i64[] !buf2 = new i64[newlen]; + for( int i=0; i<buf#; i++ ) + buf2[i] = buf[i]; + buf = buf2; + self; + }; + + // Add a i64 to aryu8 + val add = { i64 c -> + _grow(1).buf[len++] = c; + self; + }; + + val get = { int 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++ ) + str.num(buf[i]).add(',').add(' '); + if( len ) str.undo(2); + return str.add(']'); + }; + + val swap = { int i, int j -> + i64 tmp = buf[i]; + buf[i] = buf[j]; + buf[j] = tmp; + self; + }; +}; + + + +// Parse a comma-separated list of numbers, wrapped with square brackets, +// with optional whitespace e.g. "[ -17, 0,99,12456789 ] ". +// parseAryI64 & aryi64.str are bijections modulo whitespace. +// Returns null if the parse failed, without giving a clue as to why. +val parseAryI64 = { u8[~] str -> + !scan !s = new scan(str); + if( !s.match('[') ) return null; + // Array of parsed results + var nums = new aryi64(new i64[1]); + + // Parse a list of numbers + if( s.match(']') ) // Handle the empty list + return nums; + + // Parse number commas "123, " until something else + while( s.x < s.buf# ) { + nums.add(s.number()); + if( s.bad ) return null; + if( !s.match(',') ) + break; + }; + // Close the list + if( !s.match(']') ) return null; + s.skip(); + 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++ ) + dst[i+doff] = src[i+soff]; + return dst; +}; diff --git a/src/main/smp/sys/aryu8.smp b/src/main/smp/sys/aryu8.smp new file mode 100644 index 00000000..21cf920b --- /dev/null +++ b/src/main/smp/sys/aryu8.smp @@ -0,0 +1,87 @@ + +// Ary/Vector of u8s - a resizable array of bytes. +struct aryu8 { + u32 !len = 0; + u8[] !buf; + + 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; + u8[] !buf2 = new u8[newlen]; + ary.copy(buf2,0,buf,0,buf#); + buf = buf2; + self; + }; + + val clear = { -> len = 0; self; }; + + // Add a character to aryu8 + val add = { u8 c -> + _grow(1).buf[len++] = c; + self; + }; + + // Add all of a plain array to the aryu8 + val addAry = { u8[~] xs -> + _grow(xs#); + ary.copy(buf,len,xs,0,xs#); + len += xs#; + self; + }; + + // Remove x bytes from the tail. + val undo = { int 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; + } + self; + }; + + // Write to fd, returns the number of bytes written (len). + val write = { i32 fd -> + i64 ptr = buf; // Cast buffer pointer to i64 for native C call + libc.write(fd,ptr,len); + }; +}; + + +// 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; + while( x < str# && str[x] <= ' ' ) x++; + if( x == str# ) return 0; + bool neg = false; + if( str[x]=='-' ) { neg=true; x++; } + i64 n = 0; + while( x < str# && '0' <= str[x] <= '9' ) + n = n*10 + str[x++]-'0'; + return neg ? -n : n; +}; + +// Zero-terminate string for libc strings. No error checking, just copies and +// appends a zero. +{ u8[~] -> u8[] } str0 = { u8[~] str -> + u8[] !strz = new u8[str#+1]; + ary.copy(strz,0,str,0,str#); + strz[str#] = '\0'; + strz; +}; diff --git a/src/main/smp/sys/char.smp b/src/main/smp/sys/char.smp new file mode 100644 index 00000000..22ca4213 --- /dev/null +++ b/src/main/smp/sys/char.smp @@ -0,0 +1,8 @@ + +val toupper = { u8 c -> + return c >= 'a' && c <= 'z' ? c - ('a' - 'A') : c; +}; + +val tolower = { u8 c -> + return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; +}; diff --git a/src/main/smp/sys/io.smp b/src/main/smp/sys/io.smp new file mode 100644 index 00000000..d6263ffe --- /dev/null +++ b/src/main/smp/sys/io.smp @@ -0,0 +1,64 @@ + +// Simple I/O Library + +// Write byte array to stdout +val p = { u8[~] str -> write(1,str); }; +val p_noInline = { u8[~] str -> write(1,str); }; + +// Return fd contents as a u8[]. +val read = { i32 fd -> + var buf = new u8[8*1024]; + i64 ptr = buf; + int len = 0; + 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 + } + var out = new u8[len]; + ary.copy(out,0,buf,0,len); +}; + +// Write all of str to fd. +val write = { i32 fd, u8[~] str -> + i64 ptr = str; // Convert array pointer to a i64 for the native call + libc.write(fd,ptr,str#); +}; + +// Write all of str to fd. +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); +}; + +// Return stdin as a u8[]. +val stdin = { -> read(0); }; + +// Create file for writing. Return fd, or -1 and errno has the error code +val create = { u8[~] str -> + i64 ptr = sys.aryu8.str0(str); + i32 perms = 6*64 + 6*8 + 6; // 0666 + return libc.creat(ptr, perms); +}; + +// Open file for reading. Return fd, or -1 and errno has the error code +val open = { u8[~] str -> + i64 ptr = sys.aryu8.str0(str); + return libc.open(ptr, sys.libc.O_RDONLY, 0); +}; + +// Close an open fd. Return 0, or -1 and errno has the error code +val close = { i32 fd -> return libc.close(fd); }; + +// Remove a file. Return 0, or -1 and errno has the error code +val remove = { u8[~] str -> + i64 ptr = sys.aryu8.str0(str); + return libc.unlink(ptr); +}; diff --git a/src/main/smp/sys/libc.smp b/src/main/smp/sys/libc.smp new file mode 100644 index 00000000..17ee65cb --- /dev/null +++ b/src/main/smp/sys/libc.smp @@ -0,0 +1,44 @@ + +// 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"); +// 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"; +// fd buf len -> len +{ i32 i64 u32 -> i32 } read = "C"; +// fd -> err +{ i32 -> i32 } close = "C"; +// path -> err +{ i64 -> i32 } unlink = "C"; + +i32 O_RDONLY = 0; +i32 O_WRONLY = 1; +i32 O_RDWR = 2; +i32 O_CREAT = 512; // 02000 on x86_64-pc-cygwin +i32 O_TRUNC = 1024; // 04000 on x86_64-pc-cygwin + +i32 STDIN = 0; +i32 STDOUT = 1; +i32 STDERR = 2; + +// 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(); }; +// addr len prot flags fd off -> void* +{ i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C"; +// mmap flags +i32 PROT_EXEC = 4; +i32 PROT_READ = 1; +i32 PROT_WRITE= 2; +i32 PROT_NONE = 0; +i32 MAP_PRIVATE = 2; +i32 MAP_ANON = 32; diff --git a/src/main/smp/sys/scan.smp b/src/main/smp/sys/scan.smp new file mode 100644 index 00000000..44bedaaa --- /dev/null +++ b/src/main/smp/sys/scan.smp @@ -0,0 +1,39 @@ + +// Scanner; a simple string parser. +struct scan { + int !x; // Cursor + bool !bad; // True after a malformed token + u8[~] buf; // Buffer to parse + + new scan = { u8[~] b -> buf = b; }; + + // Skip whitespace + val skip = { -> + while( x < buf# && buf[x] <= ' ' ) + x++; + return self; + }; + + // True if find required character, and skip it; else False. + val match = { u8 c -> + skip(); + if( x >= buf# ) return false; + if( buf[x] != c ) return false; + x++; + return true; + }; + + // Parse a simple number + val number = { -> + skip(); + bool neg = match('-'); + if( x >= buf# || buf[x] < '0' || '9' < buf[x] ) { + bad = true; + return 0; + } + i64 n = 0; + while( x < buf# && '0' <= buf[x] & buf[x] <= '9' ) + n = n*10 + (buf[x++] - '0'); + return neg ? -n : n; + }; +}; diff --git a/src/test/java/com/seaofnodes/simple/BoundaryTest.java b/src/test/java/com/seaofnodes/simple/BoundaryTest.java new file mode 100644 index 00000000..68d8175f --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/BoundaryTest.java @@ -0,0 +1,91 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import org.junit.Test; +import static org.junit.Assert.*; + +// The new module boundary must preserve the preceding chapters' examples. +public class BoundaryTest { + @Test public void testLocalConstruction() throws java.io.IOException { + 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;"; + CodeGen code=new CodeGen(src).driver(CodeGen.Phase.LocalSched); + assertEquals("2018",Eval2.eval(code,19)); + assertEquals("0",Eval2.eval(code,0)); + code._stop.walk(n -> { + if( n instanceof StoreNode st ) assertEquals(0,st.cfg0().loopDepth()); + return null; + }); + code=new CodeGen("struct Byte { new Byte={int n -> self.x=n; self.x+=3;}; u8 x; }; return new Byte(255).x;").driver(CodeGen.Phase.TypeCheck); + assertEquals("2",Eval2.eval(code,0)); + // data is still unset when the constructor reads and updates x. + code=new CodeGen("struct Box { int x=7; int[] data; new Box={ -> self.x++; int n=self.x; data=new int[n]; }; }; return new Box().data#;") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("8",Eval2.eval(code,0)); + for( String field : new String[]{"data","self.data"} ) { + try { + new CodeGen("struct Box { int[] data; new Box={ -> int n="+field+"#; data=new int[n]; }; }; return new Box();") + .driver(CodeGen.Phase.TypeCheck); + fail("An uninitialized field must not be read"); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("might not be initialized")); + } + } + java.nio.file.Path cfile=java.nio.file.Path.of("build/objs/constructionBoundary.c"); + java.nio.file.Files.createDirectories(cfile.getParent()); + java.nio.file.Files.writeString(cfile,""" + __attribute__((CALL_CONV)) extern long long count(long long); + int main(void) { return count(19)!=2018 || count(0)!=0; } + """); + TestC.run("val count={int arg ->"+src+"};","constructionBoundary",null,TestC.CALL_CONVENTION, + TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi",cfile.toString(),"",-1); + } + + @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);}; + return sumPowers(n,power); + }; + return run(arg); + """; + for( long seed : new long[]{2,13,123} ) { + CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.Opto); + ParmNode base=code._start.walk(n -> n instanceof ParmNode p && p._label.equals("base") ? p : null); + assertNotNull(base); + assertSame(TypeInteger.constant(2),base._type); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("14",Eval2.eval(code,3)); + } + } + + @Test public void testNativeExposure() { + for( String tail : new String[]{ + "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);"; + 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); + code.driver(CodeGen.Phase.TypeCheck); + } + } + + @Test public void testLibrarySource() { + CodeGen code=new CodeGen("return sys.char.toupper('a');").driver(CodeGen.Phase.TypeCheck); + assertEquals("65",Eval2.eval(code,0)); + } + + @Test public void testLazyTables() { + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen("src/test/smp/lazy","build/objs","App/Main",null,seed,true,TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); + assertEquals("0",Eval2.eval(code,0)); + assertEquals("125",Eval2.eval(code,1)); + } + } +} diff --git a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java index a3b98fe0..c2713e4f 100644 --- a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java +++ b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java @@ -4,10 +4,8 @@ import com.seaofnodes.isa.eval.EvalRisc5; import com.seaofnodes.isa.eval.EvalArm64; -import com.seaofnodes.simple.codegen.CodeGen; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; + import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -27,24 +25,24 @@ val brain_fuck = { -> for( int pc = 0; pc < program#; pc++ ) { var command = program[pc]; - if (command == 62) { + if (command == '>') { d++; - } else if (command == 60) { + } else if (command == '<') { d--; - } else if (command == 43) { + } else if (command == '+') { data[d]++; - } else if (command == 45) { + } else if (command == '-') { data[d]--; - } else if (command == 46) { + } else if (command == '.') { // Output a byte; increase the output array size var old = output; output = new u8[output# + 1]; for( int i = 0; i < old#; i++ ) output[i] = old[i]; output[old#] = data[d]; // Add the extra byte on the end - } else if (command == 44) { + } else if (command == ',') { data[d] = 42; - } else if (command == 91) { + } else if (command == '[') { if (data[d] == 0) { for( int d = 1; d > 0; ) { command = program[++pc]; @@ -52,7 +50,7 @@ val brain_fuck = { -> if (command == 93) d--; } } - } else if (command == 93) { + } else if (command == ']') { if (data[d]) { for( int d = 1; d > 0; ) { command = program[--pc]; @@ -65,9 +63,9 @@ val brain_fuck = { -> return output; }; """; - TestC.run(src,"brain_fuck", null, brain_fuck, 40); + TestC.runC(src,"brain_fuck", brain_fuck, 40); - EvalRisc5 R5 = TestRisc5.build("brain_fuck", src, 0, 28, false); + EvalRisc5 R5 = TestRisc5.build( src, "brain_fuck", 0, 25, false); int trap = R5.step(100000); assertEquals(0,trap); int ptr = (int)R5.regs[com.seaofnodes.simple.node.cpus.riscv.riscv.A0]; diff --git a/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java b/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java new file mode 100644 index 00000000..31c0302b --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/BulkMemPhiTest.java @@ -0,0 +1,40 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMem; +import org.junit.Test; +import static org.junit.Assert.*; + +public class BulkMemPhiTest { + // Reduced from Capitalize's Lower-phase failure. A precise MemMerge slot + // may still be supplied by bulk memory which covers the requested alias. + @Test public void testBulkInPreciseSlot() { + CodeGen code=new CodeGen(""); + code._phase=CodeGen.Phase.Lower; + Node m0=code.con(TypeMem.START), m1=code.con(TypeMem.BOT); + Node ctrl=new CProjNode(code._start,0,"Ctrl").init(); + Node iff=new IfNode(ctrl,code.con(TypeInteger.BOOL)).init(); + RegionNode region=new RegionNode(null,null, + new CProjNode(iff,0,"True").init(), + new CProjNode(iff,1,"False").init()).init(); + BulkMemPhiNode bulk=new BulkMemPhiNode("$mem",region,m0,m1).init(); + // A later init check proves its call arm dead. Its precise Phi can + // collapse before the surrounding aggregate gets another peephole. + RegionNode done=new RegionNode(null,null,region,code.XCTRL).init(); + MemPhiNode precise=new MemPhiNode("$4",4,done,bulk,m1).init(); + MemMergeNode merge=new MemMergeNode(null,m0); + merge.alias(4,precise); + merge.init(); + Node folded=precise.peepholeOpt(); + assertSame(bulk,folded); + precise.subsume(folded); + + assertFalse(bulk.isSplit(4)); + assertSame(bulk,merge.alias(4)); + // Splitting this still-covered alias must tolerate the direct bulk use. + Node mem = bulk.idealize(); + bulk.subsume(mem); + } +} diff --git a/src/test/java/com/seaofnodes/simple/ChainedComparisonTest.java b/src/test/java/com/seaofnodes/simple/ChainedComparisonTest.java index 99f18e62..8db9c024 100644 --- a/src/test/java/com/seaofnodes/simple/ChainedComparisonTest.java +++ b/src/test/java/com/seaofnodes/simple/ChainedComparisonTest.java @@ -21,7 +21,7 @@ public class ChainedComparisonTest { @Test public void testSingleEvaluationAndShortCircuit() { for(int seed:new int[]{2,13,123}) { - CodeGen code = new CodeGen("int x=arg; int y=0; val b=0 < x++ <= (y+=3) < (y+=1); return x*100+y*10+b;",TypeInteger.BOT,seed,true) + CodeGen code = new CodeGen("int x=arg; int y=0; val b=0 < x++ <= (y+=3) < (y+=1); return x*100+y*10+b;",seed,true) .driver(CodeGen.Phase.TypeCheck); assertEquals("100",Eval2.eval(code,0)); assertEquals("441",Eval2.eval(code,3)); diff --git a/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/src/test/java/com/seaofnodes/simple/Chapter01Test.java index ac9adacc..fa7fb68a 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter01Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter01Test.java @@ -11,7 +11,7 @@ public class Chapter01Test { @Test public void testSimpleProgram() { - CodeGen code = new CodeGen("return 1;").parse(); + CodeGen code = new CodeGen("1;").parse(); Node expr = code.expr(); if( expr instanceof ConstantNode con ) { assertEquals(code._start,con.in(0)); @@ -53,7 +53,7 @@ public class Chapter01Test { @Test public void testNotBad3() { // this test used to fail in chapter 1 - assertEquals("return 12;", new CodeGen("return - -12;").parse()._stop.print()); + assertEquals("return 12;", new CodeGen("return - -12;").parse().print()); } @Test @@ -62,14 +62,14 @@ public class Chapter01Test { new CodeGen("return 100").parse(); fail(); } catch( RuntimeException e ) { - assertEquals("Syntax error, expected ;: ",e.getMessage()); + assertEquals("Syntax error, expected `;` but found `EOF`",e.getMessage()); } } @Test public void testNotBad5() { // this test used to fail in chapter 1 - assertEquals("return -100;", new CodeGen("return -100;").parse()._stop.print()); + assertEquals("return -100;", new CodeGen("return -100;").parse().print()); } @Test @@ -88,7 +88,7 @@ public class Chapter01Test { new CodeGen("return 1;}").parse(); fail(); } catch( RuntimeException e ) { - assertEquals("Syntax error, unexpected: }",e.getMessage()); + assertEquals("Syntax error, unexpected but found `}`",e.getMessage()); } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter03Test.java b/src/test/java/com/seaofnodes/simple/Chapter03Test.java index 8923566a..f1e4af12 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter03Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter03Test.java @@ -59,7 +59,7 @@ public class Chapter03Test { new CodeGen("int a=1; int b=2; int !c=0; { int b=3; c=a+b;").parse(); fail(); } catch( RuntimeException e ) { - assertEquals("Syntax error, expected }: ",e.getMessage()); + assertEquals("Syntax error, expected `}` but found `EOF`",e.getMessage()); } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter04Test.java b/src/test/java/com/seaofnodes/simple/Chapter04Test.java index ce808084..1132375e 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter04Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter04Test.java @@ -91,7 +91,7 @@ public class Chapter04Test { // Adding functions means `main` does not constant fold the incoming arg value @Ignore @Test public void testConstantArg() { - CodeGen code = new CodeGen("return arg; ", TypeInteger.constant(2), 123L, true).parse(); + CodeGen code = new CodeGen("return arg; ", TypeInteger.constant(2)).parse(); assertEquals("return 2;", code.expr().in(1).toString()); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter05Test.java b/src/test/java/com/seaofnodes/simple/Chapter05Test.java index b234209f..afdb1071 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter05Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter05Test.java @@ -116,7 +116,10 @@ if( arg==0 ) return arg+a+b; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return ((arg+Phi(Region,1,0))+Phi(Region,2,0));", code.print()); + String ir = code.print(); + assertTrue(ir, + ir.equals("return ((arg+Phi(Region,1,0))+Phi(Region,2,0));") || + ir.equals("return (Phi(Region,((!=0)arg+1),0)+Phi(Region,2,0));")); } @Test @@ -177,7 +180,7 @@ return a; @Test public void testBadNum() { try { new CodeGen("return 1-;").parse(); fail(); } - catch( RuntimeException e ) { assertEquals("Syntax error, expected an identifier or expression: ;",e.getMessage()); } + catch( RuntimeException e ) { assertEquals("Syntax error, expected `an identifier or expression` but found `;`",e.getMessage()); } } @Test diff --git a/src/test/java/com/seaofnodes/simple/Chapter06Test.java b/src/test/java/com/seaofnodes/simple/Chapter06Test.java index 53fcefed..d1af5d14 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter06Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter06Test.java @@ -104,7 +104,7 @@ else if( arg==3 ) else a=5; return a; -""", TypeInteger.constant(2), 123L, true).driver(CodeGen.Phase.FinalizeIR); +""", TypeInteger.constant(2)).driver(CodeGen.Phase.FinalizeIR); assertEquals("return 5;", code.print()); } @@ -124,7 +124,7 @@ else if( arg==3 ) else a=5; return a; -""", TypeInteger.constant(1), 123L, true).driver(CodeGen.Phase.FinalizeIR); +""", TypeInteger.constant(1)).driver(CodeGen.Phase.FinalizeIR); assertEquals("return 3;", code.print()); } @@ -164,7 +164,7 @@ else if( arg==3 ) else a=5; return a; -""", TypeInteger.constant(1), 123L, true); +""", TypeInteger.constant(1)); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 3;", code.print()); } @@ -185,7 +185,7 @@ else if( arg==3 ) else a=5; return a; -""", TypeInteger.constant(3), 123L, true); +""", TypeInteger.constant(3)); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 4;", code.print()); } @@ -220,7 +220,7 @@ if( arg ) { else b = 3; } return a+b; -""", TypeInteger.constant(1), 123L, true); +""", TypeInteger.constant(1)); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 4;", code.print()); } @@ -238,7 +238,7 @@ if( arg ) { else b = 3; } return a+b; -""", TypeInteger.constant(0), 123L, true); +""", TypeInteger.constant(0)); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 1;", code.print()); } @@ -277,7 +277,7 @@ if( arg ) { else b = 3; } return a+b+c; -""", TypeInteger.constant(1), 123L, true); +""", TypeInteger.constant(1)); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 6;", code.print()); } @@ -297,7 +297,7 @@ if( arg ) { else b = 3; } return a+b+c; -""", TypeInteger.constant(2), 123L, true); +""", TypeInteger.constant(2)); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 5;", code.print()); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter07Test.java b/src/test/java/com/seaofnodes/simple/Chapter07Test.java index daca77ab..7094c0d5 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter07Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter07Test.java @@ -17,8 +17,8 @@ while(arg < 10) { } return arg; """); - code.parse(); - assertEquals("return Phi(Loop,arg,(Convert_i64(Phi_arg)+1));", code.print()); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Loop,arg,(Phi_arg+1));", code.print()); assertTrue(code.ctrl() instanceof CProjNode); } @@ -71,7 +71,7 @@ while(a < 10) { return b; """); code.parse(); - assertEquals("return Convert_i64(Phi(Loop,2,Phi(Region,Phi_b,4)));", code.print()); + assertEquals("return Phi(Loop,2,Phi(Region,Phi_b,4));", code.print()); assertTrue(code.ctrl() instanceof CProjNode); } @@ -106,8 +106,8 @@ while(a < 10) { } return a; """); - code.parse(); - assertEquals("return Phi(Loop,1,(Convert_i64(Phi_a)+3));", code.print()); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print()); assertTrue(code.ctrl() instanceof CProjNode); } @@ -164,8 +164,8 @@ while(a < 10) { } return a; """); - code.parse(); - assertEquals("return Phi(Loop,1,(Convert_i64(Phi_a)+3));", code.print()); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print()); assertTrue(code.ctrl() instanceof CProjNode); } @@ -197,8 +197,8 @@ while(a < 10) { } return a; """); - code.parse(); - assertEquals("return Phi(Loop,1,(Convert_i64(Phi_a)+3));", code.print()); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print()); assertTrue(code.ctrl() instanceof CProjNode); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/src/test/java/com/seaofnodes/simple/Chapter09Test.java index f24d96eb..7025b98a 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter09Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter09Test.java @@ -209,9 +209,9 @@ return -0+0+0; @Test public void testFuzz3() { - CodeGen code = new CodeGen("int v0=0; while(0==69) while(v0) return 0; return 0;"); + CodeGen code = new CodeGen("int v0=0; while(0==69) while(v0) return 0; return 1;"); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return 0;", code.print()); + assertEquals("return 1;", code.print()); } @Test @@ -255,7 +255,7 @@ return 0!=0; int v0=0; while(0==1) while(v0) v0=1+v0; -return 0; +return 0; """); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 0;", code.print()); diff --git a/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/src/test/java/com/seaofnodes/simple/Chapter10Test.java index afe0b141..39c7040e 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter10Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -2,8 +2,6 @@ package com.seaofnodes.simple; import com.seaofnodes.simple.codegen.CodeGen; import org.junit.Test; -import com.seaofnodes.simple.node.*; -import java.util.BitSet; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -22,7 +20,7 @@ public class Chapter10Test { var code = new CodeGen(src).driver(CodeGen.Phase.LocalSched); for( int arg : new int[]{0,1,2,3} ) { long expected = (arg==1 || arg==2) ? 9 : 10; - assertEquals("S{x="+expected+"}",Eval2.eval(code,arg)); + assertEquals("Test.S{x="+expected+"}",Eval2.eval(code,arg)); } } @@ -33,35 +31,39 @@ public class Chapter10Test { } ) { String src = "struct S { int x; }; " + body; var code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + // This chapter also has constructor/class-init Returns in the CompUnit. + var entry = (com.seaofnodes.simple.node.FunNode)code._start.uctrl(); org.junit.Assert.assertTrue(body, - code.expr()._type instanceof com.seaofnodes.simple.type.TypeMemPtr); + entry.ret().expr()._type instanceof com.seaofnodes.simple.type.TypeMemPtr); } } - - // Issue #246: null-check guards start in Chapter 10; arrays arrive in Chapter 15. private static final String NULLABLE_POINT_SOURCE = """ - struct Point { int x; }; + struct Point { int x; new Point = { int v -> x = v; }; }; !Point?[] !points = new !Point?[2]; - points[arg] = new Point { x = 42; }; + points[arg] = new Point(42); Point? p = points[1]; """; @Test public void testNullGuards() { + // Issue #246: != null lowers to !!p. Follow every negation, on either arm. for( String body : new String[] { "if (p != null) return p.x; return -1;", "if (null != p) return p.x; return -1;", "if (!!!!p) return p.x; return -1;", "if (!!!p) return -1; return p.x;", - "int b = !!p; if (b) return p.x + b - 1; return -1;", + "val b = !!p; if (b) return p.x + b - 1; return -1;", "if (!(p == null || arg == 0)) return p.x; return -1;" - } ) { - CodeGen code = new CodeGen(NULLABLE_POINT_SOURCE+body).driver(CodeGen.Phase.TypeCheck); - assertEquals(body,"-1",Eval2.eval(code,0)); - assertEquals(body,"42",Eval2.eval(code,1)); - } + } ) + for( long seed : new long[] { 1, 42, 126 } ) { + CodeGen code = new CodeGen(NULLABLE_POINT_SOURCE+body,seed,true) + .driver(CodeGen.Phase.TypeCheck); + assertEquals(body,"-1",Eval2.eval(code,0)); + assertEquals(body,"42",Eval2.eval(code,1)); + code.driver(CodeGen.Phase.LocalSched); + } } @Test @@ -90,11 +92,8 @@ public class Chapter10Test { try { new CodeGen(NULLABLE_POINT_SOURCE+body).driver(CodeGen.Phase.TypeCheck); fail(body); - } catch( RuntimeException e ) { - // Known null is rejected during parsing in these chapters. - String expected = body.startsWith("if (p == null)") - ? "Accessing unknown field 'x' from 'null'" : "Might be null accessing 'x'"; - assertEquals(body,expected,e.getMessage()); + } catch( Parser.ParseException e ) { + assertEquals(body,"Might be null accessing 'x'",e.getMessage()); } } } @@ -127,15 +126,15 @@ return p-r; @Test public void testStruct() { CodeGen code = new CodeGen(""" -struct Bar { +struct _Bar { int a; int b; }; -struct Foo { +struct _Foo { int x; }; -Foo? foo = null; -!Bar !bar = new Bar; +_Foo? foo = null; +!_Bar !bar = new _Bar; bar.a = 1; bar.a = 2; return bar.a; @@ -157,7 +156,7 @@ else return v; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Vector2D;", code.print()); + assertEquals("Stop[ return Test.Vector2D; return MEM[ 2:___ 3:.x=0; 4:.y=0;]; return Test.Vector2D; ]", code.print()); } @Test @@ -169,8 +168,8 @@ struct s0 { s0? v1=null; int v3=v1.zAicm; """); - try { code.parse(); fail(); } - catch( Exception e ) { assertEquals("Accessing unknown field 'zAicm' from 'null'",e.getMessage()); } + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Might be null accessing 'zAicm'",e.getMessage()); } } @Test @@ -195,7 +194,7 @@ while (arg) { return bar.a; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Phi(Loop,0,(Phi_a+2));", code.print()); + assertEquals("Stop[ return Phi(Loop,0,(Phi_a+2)); return MEM[ 2:___ 3:.a=0;]; return Test.Bar; ]", code.print()); } @Test @@ -208,7 +207,7 @@ bar.a = 1; return bar.a; """); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Type null is not of declared type *Bar",e.getMessage()); } + catch( Exception e ) { assertEquals("Type null is not of declared type *Test.Bar",e.getMessage()); } } @Test @@ -234,28 +233,28 @@ bar.a = 1; return bar.a; """); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Type null is not of declared type *Bar", e.getMessage()); } + catch( Exception e ) { assertEquals("Type null is not of declared type *Test.Bar", e.getMessage()); } } @Test public void testIfOrNull() { CodeGen code = new CodeGen(""" -struct Bar { int a; }; -!Bar? !bar = new Bar; +struct _Bar { int a; }; +!_Bar? !bar = new _Bar; if (arg) bar = null; if( bar ) bar.a = 1; -return bar; +return bar.a; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Phi(Region,null,Bar);", code.print()); + assertEquals("return Phi(Region,1,.a);", code.print()); } @Test public void testIfOrNull2() { CodeGen code = new CodeGen( """ -struct Bar { int a; }; -!Bar? !bar = new Bar; +struct _Bar { int a; }; +!_Bar? !bar = new _Bar; if (arg) bar = null; int rez = 3; if( !bar ) rez=4; @@ -317,7 +316,7 @@ while( i.x < i.len ) { return sum; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Phi(Loop,0,(Phi(Loop,0,(Phi_x+1))+Phi_sum));", code.print()); + assertEquals("Stop[ return Phi(Loop,0,(Phi(Loop,0,(Phi_x+1))+Phi_sum)); return MEM[ 2:___ 3:.x=0; 4:.len=0;]; return Test.Iter; ]", code.print()); } @@ -336,44 +335,62 @@ while(arg) { return ret; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Phi(Loop,s0,Phi(Region,s0,Phi_ret));", code.print()); + assertEquals("Stop[ return Phi(Loop,Test.s0,Phi(Region,Test.s0,Phi_ret)); return MEM[ 2:___ 3:.v0=0;]; return Test.s0; ]", code.print()); } @Test public void test2() { CodeGen code = new CodeGen(""" -struct s0 {int v0;}; -!s0 !ret = new s0; -!s0 !v0 = new s0; +struct _s0 {int v0;}; +!_s0 !ret = new _s0; +!_s0 !v0 = new _s0; while(arg) { v0.v0 = arg; arg = arg-1; if (arg==5) ret=v0; - } -return ret; +return ret.v0; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Phi(Loop,s0,Phi(Region,s0,Phi_ret));", code.print()); + assertEquals("return .v0;", code.print()); } @Test public void test3() { CodeGen code = new CodeGen(""" -struct s0 {int v0;}; -!s0 !ret = new s0; +struct _s0 {int v0;}; +!_s0 !ret = new _s0; while(arg < 10) { - !s0 !v0 = new s0; - if (arg == 5) ret=v0; + !_s0 !v1 = new _s0; + if (arg == 5) ret=v1; arg = arg + 1; } -return ret; +return ret.v0; +"""); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return .v0;", code.print()); + } + + + @Test + public void testModGlobalInConstructor() { + CodeGen code = new CodeGen( +""" +int cnt = 1; +// cnt is incremented exactly once, it is not part of s0 constructor +// hence s0.x is always 1, and cnt remains at 2 +struct s0 { int x = cnt++; }; +s0 a = new s0(); +s0 b = new s0(); +// a.x==1, b.x==1, cnt==2 +return a.x * 100 + b.x * 10 + cnt; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Phi(Loop,s0,Phi(Region,s0,Phi_ret));", code.print()); + assertEquals("Stop[ return 112; return MEM[ 2:___ 3:___ 4:.x=1;]; return Test.s0; ]", code.print()); } + @Test public void testBug3() { CodeGen code = new CodeGen(""" @@ -382,7 +399,7 @@ return new s0; int v0=null.f0; """); try { code.parse(); fail(); } - catch( Exception e ) { assertEquals("Syntax error, expected ;: .",e.getMessage()); } + catch( Exception e ) { assertEquals("Syntax error, expected `;` but found `.`",e.getMessage()); } } @Test @@ -412,8 +429,8 @@ if(0) return 0; else return new s0; if(new s0.f0) return 0; """); - code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return s0;", code.print()); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("Stop[ return Test.s0; return MEM[ 2:___ 3:.f0=0;]; return Test.s0; ]", code.print()); } @Test @@ -441,7 +458,7 @@ s0 v1 = v0; return v1; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return (const)s0;", code.print()); + assertEquals("Stop[ return (const)Test.s0; return MEM[ 2:___ 3:.f0=0;]; return Test.s0; ]", code.print()); } @@ -488,122 +505,4 @@ return 0; assertEquals("return 0;", code.print()); } - @Test public void testReadBeforeStores() { - var code = new CodeGen(""" - struct S { int x; int y; }; - !S !a = new S; !S !b = new S; - a.x = 11; b.x = 22; - !S !p = a; if (arg) p = b; - int before = p.x; - a.y = 55; b.y = 66; - a.x = 33; b.x = 44; - return before*100 + p.x; - """).driver(CodeGen.Phase.FinalizeIR); - StopNode stop = code._stop; - assertEquals("1133",Eval2.eval(code,0)); - assertEquals("2244",Eval2.eval(code,1)); - } - - static final String NESTED_MEMORY = """ - struct S { int x; int y; int z; }; - !S !s = new S; - s.x = 5; s.y = 7; s.z = 11; - while (arg > 0) { - s.x = s.x + 1; - int j = 3; - while (j > 0) { - j = j - 1; - if (j == 1) continue; - s.y = s.y + arg; - if (arg == 2) break; - s.z = s.z + 1; - } - arg = arg - 1; - } - return s; - """; - - @Test public void testMemoryAcrossNestedLoops() { - var code = new CodeGen(NESTED_MEMORY).driver(CodeGen.Phase.FinalizeIR); - StopNode stop = code._stop; - for (int arg=0; arg<7; arg++) { - assertEquals("S{x="+(5L+arg)+",y="+(7L+arg*(arg+1)-(arg>=2 ? 2 : 0))+ - ",z="+(11L+2*arg-(arg>=2 ? 2 : 0))+"}",Eval2.eval(code,arg)); - } - } - - @Test public void testMemoryAtEarlyReturns() { - var code = new CodeGen(""" - struct S { int x; int y; int z; }; - !S !s = new S; - s.x = 3; s.y = 5; s.z = 7; - if (arg) { s.x = 11; s.z = 13; return s; } - s.y = 17; - return s; - """).driver(CodeGen.Phase.FinalizeIR); - StopNode stop = code._stop; - for (int arg=0; arg<2; arg++) { - assertEquals(arg==0 ? "S{x=3,y=17,z=7}" : "S{x=11,y=5,z=13}",Eval2.eval(code,arg)); - } - } - - @Test public void testKeepLoadsAtMerge() { - var code = new CodeGen(""" - struct S { int x; }; - !S !a = new S; !S !b = new S; - a.x = arg; b.x = arg+1; - !S !p = a; !S !q = b; - if (arg<0) { p=b; q=a; } - int v; - if (arg>1) v=p.x; else v=q.x; - return v; - """).driver(CodeGen.Phase.FinalizeIR); - StopNode stop = code._stop; - // The one-step safety check stops at the memory aggregate. - assertEquals(2,countMemoryNodes(stop,LoadNode.class,new BitSet())); - assertEquals("-1",Eval2.eval(code,-1)); - assertEquals("1",Eval2.eval(code,0)); - assertEquals("3",Eval2.eval(code,3)); - } - - @Test public void testKeepBranchStores() { - var code = new CodeGen(""" - struct S { int x; }; - !S !s = new S; - if (arg) s.x=arg+1; else s.x=arg+2; - return s; - """).driver(CodeGen.Phase.FinalizeIR); - StopNode stop = code._stop; - // Keep each Store on its own branch. - assertEquals(2,countMemoryNodes(stop,StoreNode.class,new BitSet())); - assertEquals("S{x=2}",Eval2.eval(code,0)); - assertEquals("S{x=4}",Eval2.eval(code,3)); - } - - @Test public void testKeepReadsBeforeWrites() { - var code = new CodeGen(""" - struct S { int x; int y; }; - !S !a = new S; !S !b = new S; - a.x=arg; b.x=arg+1; - !S !p=a; !S !q=b; - if (arg<0) { p=b; q=a; } - int v; - if (arg>1) { v=p.x; p.x=41; p.y=5; } - else { v=q.x; q.x=42; } - return v; - """).driver(CodeGen.Phase.FinalizeIR); - StopNode stop = code._stop; - assertEquals(2,countMemoryNodes(stop,LoadNode.class,new BitSet())); - assertEquals("-1",Eval2.eval(code,-1)); - assertEquals("1",Eval2.eval(code,0)); - assertEquals("3",Eval2.eval(code,3)); - } - - private static int countMemoryNodes(Node n, Class<?> kind, BitSet seen) { - if (n==null || seen.get(n._nid)) return 0; - seen.set(n._nid); - int cnt=kind.isInstance(n) ? 1 : 0; - for (Node def : n._inputs) cnt+=countMemoryNodes(def,kind,seen); - return cnt; - } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter11Test.java b/src/test/java/com/seaofnodes/simple/Chapter11Test.java deleted file mode 100644 index 7c5e05f8..00000000 --- a/src/test/java/com/seaofnodes/simple/Chapter11Test.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.*; -import com.seaofnodes.simple.node.*; -import org.junit.Test; -import static org.junit.Assert.*; - -public class Chapter11Test { - @Test public void testIndependentField() { - var code = new CodeGen(""" - struct S { int x; int y; }; - !S !s = new S; - s.x = 42; - while (arg > 0) { s.y = s.y + arg; arg = arg - 1; } - return s.x; - """).driver(CodeGen.Phase.FinalizeIR); - var stop = code._stop; - assertEquals("return 42;",stop.toString()); - assertEquals("42",Eval2.eval(code,5)); - assertNull(stop.walk(n -> n instanceof LoadNode ? n : null)); - } - - @Test public void testParallelMemoryPhis() { - var code = new CodeGen(""" - struct S { int x; int y; int z; }; - !S !a = new S; !S !b = new S; - !S !p = a; if (arg) p = b; - while (arg > 0) { - a.x = p.x + arg; - a.y = p.y + 2; - a.z = p.z + 3; - arg = arg - 1; - } - return a; - """).driver(CodeGen.Phase.FinalizeIR); - var stop = code._stop; - int[] phis = {0}; - stop.walk(n -> { - // Store sinking can replace field memory Phis with value Phis. - if (n instanceof PhiNode) phis[0]++; - if (n instanceof MemMergeNode m && m.in(1) instanceof BulkMemPhiNode b) - for(int a=b._aliases.nextSetBit(0); a>=0; a=b._aliases.nextSetBit(a+1)) - assertTrue(a<m.nIns() && m.in(a)!=null); - assertFalse(n instanceof PhiNode && n.isMem() && - !(n instanceof MemPhiNode) && !(n instanceof BulkMemPhiNode) && !(n instanceof ParmNode)); - return null; - }); - assertTrue(phis[0]>=4); // Three fields and the loop counter. - assertEquals("S{x=1,y=2,z=3}",Eval2.eval(code,4)); - } - - @Test public void testLateSliceWorklist() { - // Seed 97 exposed a store selecting a bulk predecessor without queuing - // it. Keep both the worklist assertion and the final heap check. - for (int seed : new int[]{0,97,123,456}) { - var code = new CodeGen(Chapter10Test.NESTED_MEMORY,com.seaofnodes.simple.type.TypeInteger.BOT,seed,false).parse(); - code.iter().opto().iterPostOpto().finalizeIR(); - assertEquals("S{x=9,y=25,z=17}",Eval2.eval(code,4)); - } - } -} diff --git a/src/test/java/com/seaofnodes/simple/Chapter12Test.java b/src/test/java/com/seaofnodes/simple/Chapter12Test.java index 2a86b11a..fae7611d 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter12Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter12Test.java @@ -12,11 +12,11 @@ public class Chapter12Test { public void testJig() { CodeGen code = new CodeGen( """ -return 3.14; +return 3; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return 3.14;", code.print()); - assertEquals("3.14", Eval2.eval(code, 0)); + assertEquals("return 3;", code.print()); + assertEquals("3", Eval2.eval(code, 0)); } @Test @@ -42,22 +42,22 @@ return head.next.i; public void testLinkedList1() { CodeGen code = new CodeGen( """ -struct LLI { !LLI? !next; int q; }; -!LLI? !head = null; +struct _LLI { !_LLI? !next; int q; }; +!_LLI? !head = null; while( arg ) { - !LLI !x = new LLI; + !_LLI !x = new _LLI; x.next = head; x.q = arg; head = x; arg = arg-1; } if( !head ) return 0; -LLI? next = head.next; +_LLI? next = head.next; if( next==null ) return 1; return next.q; """); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("return Phi(Region,0,1,.q);", code.print()); + assertEquals("return Phi(Region,1,.q,0);", code.print()); assertEquals("2", Eval2.eval(code, 3)); } @@ -65,17 +65,17 @@ return next.q; public void testCoRecur() { CodeGen code = new CodeGen( """ -struct int0 { int i; !flt0? !f; }; -struct flt0 { flt f; !int0? !i; }; -!int0 !i0 = new int0; +struct _int0 { int i; !Test._flt0? !f; }; +struct _flt0 { flt f; !_int0? !i; }; +!_int0 !i0 = new _int0; i0.i = 17; -!flt0 !f0 = new flt0; +!_flt0 !f0 = new _flt0; f0.f = 3.14; i0.f = f0; f0.i = i0; return f0.i.f.i.i; """); - code.driver(CodeGen.Phase.FinalizeIR); + code.driver(CodeGen.Phase.TypeCheck); assertEquals("return 17;", code.print()); } @@ -83,8 +83,8 @@ return f0.i.f.i.i; public void testNullRef0() { CodeGen code = new CodeGen( """ -struct N { !N? !next; int i; }; -N n = new N; +struct _N { !_N? !next; int i; }; +_N n = new _N; return n.next; """); code.driver(CodeGen.Phase.FinalizeIR); @@ -96,12 +96,12 @@ return n.next; CodeGen code = new CodeGen( """ struct M { int m; }; -struct N { !M !next; int i; }; -N n = new N { next = new M; }; +struct N { !M !next; int i; new N = { !M m -> next = m; }; }; +N n = new N(new M); return n.next; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return (const)M;", code.print()); + assertEquals("Stop[ return (const)Test.M; return MEM[ 2:___ 3:.m=0;]; return Test.M; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:___ 7:.next=Parm_m(Test.N.N,*Test.M {i64 !m; }); 8:.i=0;]; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:___ 7:___ 8:.i=0;]; return Test.N; ]", code.print()); } @Test @@ -109,12 +109,12 @@ return n.next; CodeGen code = new CodeGen( """ struct M { int m; }; -struct N { !M !next; int i; }; -N n = new N { next = null; }; +struct N { !M !next; int i; new N = { !M m -> next = m; }; }; +N n = new N(null); return n.next; """); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Type null is not of declared type *M",e.getMessage()); } + catch( Exception e ) { assertEquals("Argument #0 isa null, but must be a *Test.M {i64 !m; }",e.getMessage()); } } @Test @@ -134,20 +134,20 @@ return n.i; public void testNullRef4() { CodeGen code = new CodeGen("return -null-5/null-5;"); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Cannot '//' null",e.getMessage()); } + catch( Exception e ) { assertEquals("Cannot null + 5",e.getMessage()); } } @Test public void testNullRef5() { CodeGen code = new CodeGen("return null+42;"); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Cannot '+' null",e.getMessage()); } + catch( Exception e ) { assertEquals("Cannot null + 42",e.getMessage()); } } @Test public void testEmpty() { CodeGen code = new CodeGen( """ -struct S{}; +struct _S{}; return 0; """); code.driver(CodeGen.Phase.FinalizeIR); @@ -159,10 +159,10 @@ return 0; public void testForwardRef0() { CodeGen code = new CodeGen( """ -struct S1 { !S2? !s; }; +struct S1 { S2? s; }; return new S2; """); - try { code.driver(CodeGen.Phase.FinalizeIR); fail(); } + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } catch( Exception e ) { assertEquals("Unknown struct type 'S2'",e.getMessage()); } } @@ -170,12 +170,12 @@ return new S2; public void testForwardRef1() { CodeGen code = new CodeGen( """ -struct S1 { !S2? !s; }; -struct S2 { int x; }; -return new S1.s=new S2; +struct _S1 { !_S2? !s; }; +struct _S2 { int x; }; +return (new _S1).s=new _S2; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return S2;", code.print()); + assertEquals("return Test._S2;", code.print()); } @Test @@ -199,9 +199,9 @@ return p1.pi.i + 1; public void testCoRecur2() { CodeGen code = new CodeGen( """ -struct A { !B? !f0; !C? !f1; }; !A !a = new A; -struct B { !C? !f0; !A? !f1; }; !B !b = new B; -struct C { !A? !f0; !B? !f1; }; !C !c = new C; +struct _A { !Test._B? !f0; !Test._C? !f1; }; !_A !a = new _A; +struct _B { !Test._C? !f0; !Test._A? !f1; }; !_B !b = new _B; +struct _C { !Test._A? !f0; !Test._B? !f1; }; !_C !c = new _C; a.f0=b; a.f1=c; b.f0=c; b.f1=a; @@ -210,41 +210,41 @@ c.f0=a; c.f1=b; return a.f0.f1.f0.f1.f0; """); - code.driver(CodeGen.Phase.LocalSched); - assertEquals("return B;", code._stop.toString()); - assertEquals("B{f0=C{f0=A{f0=$cyclic,f1=$cyclic},f1=$cyclic},f1=$cyclic}", Eval2.eval(code, 0)); + code.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule).localSched(); + assertEquals("return Test._B;", code.print()); + assertEquals("Test._B{f0=Test._C{f0=Test._A{f0=$cyclic,f1=$cyclic},f1=$cyclic},f1=$cyclic}", Eval2.eval(code, 0)); } @Test public void testCoRecur3() { CodeGen code = new CodeGen( """ -struct A { !L? !a; !T? !b; !F? !c; }; !A !a = new A; -struct B { !M? !a; !U? !b; !G? !c; }; !B !b = new B; -struct C { !N? !a; !V? !b; !H? !c; }; !C !c = new C; -struct D { !O? !a; !W? !b; !I? !c; }; !D !d = new D; -struct E { !P? !a; !X? !b; !J? !c; }; !E !e = new E; -struct F { !Q? !a; !Y? !b; !K? !c; }; !F !f = new F; -struct G { !R? !a; !Z? !b; !L? !c; }; !G !g = new G; -struct H { !S? !a; !A? !b; !M? !c; }; !H !h = new H; -struct I { !T? !a; !B? !b; !N? !c; }; !I !i = new I; -struct J { !U? !a; !C? !b; !O? !c; }; !J !j = new J; -struct K { !V? !a; !D? !b; !P? !c; }; !K !k = new K; -struct L { !W? !a; !E? !b; !Q? !c; }; !L !l = new L; -struct M { !X? !a; !F? !b; !R? !c; }; !M !m = new M; -struct N { !Y? !a; !G? !b; !S? !c; }; !N !n = new N; -struct O { !Z? !a; !H? !b; !T? !c; }; !O !o = new O; -struct P { !A? !a; !I? !b; !U? !c; }; !P !p = new P; -struct Q { !B? !a; !J? !b; !V? !c; }; !Q !q = new Q; -struct R { !C? !a; !K? !b; !W? !c; }; !R !r = new R; -struct S { !D? !a; !L? !b; !X? !c; }; !S !s = new S; -struct T { !E? !a; !M? !b; !Y? !c; }; !T !t = new T; -struct U { !F? !a; !N? !b; !Z? !c; }; !U !u = new U; -struct V { !G? !a; !O? !b; !A? !c; }; !V !v = new V; -struct W { !H? !a; !P? !b; !B? !c; }; !W !w = new W; -struct X { !I? !a; !Q? !b; !C? !c; }; !X !x = new X; -struct Y { !J? !a; !R? !b; !D? !c; }; !Y !y = new Y; -struct Z { !K? !a; !S? !b; !E? !c; }; !Z !z = new Z; +struct _A { !Test._L? !a; !Test._T? !b; !Test._F? !c; }; !_A !a = new _A; +struct _B { !Test._M? !a; !Test._U? !b; !Test._G? !c; }; !_B !b = new _B; +struct _C { !Test._N? !a; !Test._V? !b; !Test._H? !c; }; !_C !c = new _C; +struct _D { !Test._O? !a; !Test._W? !b; !Test._I? !c; }; !_D !d = new _D; +struct _E { !Test._P? !a; !Test._X? !b; !Test._J? !c; }; !_E !e = new _E; +struct _F { !Test._Q? !a; !Test._Y? !b; !Test._K? !c; }; !_F !f = new _F; +struct _G { !Test._R? !a; !Test._Z? !b; !Test._L? !c; }; !_G !g = new _G; +struct _H { !Test._S? !a; !Test._A? !b; !Test._M? !c; }; !_H !h = new _H; +struct _I { !Test._T? !a; !Test._B? !b; !Test._N? !c; }; !_I !i = new _I; +struct _J { !Test._U? !a; !Test._C? !b; !Test._O? !c; }; !_J !j = new _J; +struct _K { !Test._V? !a; !Test._D? !b; !Test._P? !c; }; !_K !k = new _K; +struct _L { !Test._W? !a; !Test._E? !b; !Test._Q? !c; }; !_L !l = new _L; +struct _M { !Test._X? !a; !Test._F? !b; !Test._R? !c; }; !_M !m = new _M; +struct _N { !Test._Y? !a; !Test._G? !b; !Test._S? !c; }; !_N !n = new _N; +struct _O { !Test._Z? !a; !Test._H? !b; !Test._T? !c; }; !_O !o = new _O; +struct _P { !Test._A? !a; !Test._I? !b; !Test._U? !c; }; !_P !p = new _P; +struct _Q { !Test._B? !a; !Test._J? !b; !Test._V? !c; }; !_Q !q = new _Q; +struct _R { !Test._C? !a; !Test._K? !b; !Test._W? !c; }; !_R !r = new _R; +struct _S { !Test._D? !a; !Test._L? !b; !Test._X? !c; }; !_S !s = new _S; +struct _T { !Test._E? !a; !Test._M? !b; !Test._Y? !c; }; !_T !t = new _T; +struct _U { !Test._F? !a; !Test._N? !b; !Test._Z? !c; }; !_U !u = new _U; +struct _V { !Test._G? !a; !Test._O? !b; !Test._A? !c; }; !_V !v = new _V; +struct _W { !Test._H? !a; !Test._P? !b; !Test._B? !c; }; !_W !w = new _W; +struct _X { !Test._I? !a; !Test._Q? !b; !Test._C? !c; }; !_X !x = new _X; +struct _Y { !Test._J? !a; !Test._R? !b; !Test._D? !c; }; !_Y !y = new _Y; +struct _Z { !Test._K? !a; !Test._S? !b; !Test._E? !c; }; !_Z !z = new _Z; a.a=l; a.b=t; a.c=f; b.a=m; b.b=u; b.c=g; @@ -276,14 +276,8 @@ z.a=k; z.b=s; z.c=e; return a.b.c.a.b.c.a.b.c.a.b.c.a.b.c.a.b.c; """); - code.driver(CodeGen.Phase.LocalSched); - assertEquals("return R;", code._stop.toString()); - // All forward shapes are complete now. Following field a visits - // all 26 objects before closing the cycle; b and c revisit them. - String expected = "$cyclic"; - String chain = "RCNYJUFQBMXITEPALWHSDOZKVG"; - for( int i=chain.length()-1; i>=0; i-- ) - expected=chain.charAt(i)+"{a="+expected+",b=$cyclic,c=$cyclic}"; - assertEquals(expected,Eval2.eval(code,0)); + code.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule).localSched(); + assertEquals("return Test._R;", code.print()); + assertEquals("Test._R{a=Test._C{a=Test._N{a=Test._Y{a=Test._J{a=Test._U{a=Test._F{a=Test._Q{a=Test._B{a=Test._M{a=Test._X{a=Test._I{a=Test._T{a=Test._E{a=Test._P{a=Test._A{a=Test._L{a=Test._W{a=Test._H{a=Test._S{a=Test._D{a=Test._O{a=Test._Z{a=Test._K{a=Test._V{a=Test._G{a=$cyclic,b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic},b=$cyclic,c=$cyclic}", Eval2.eval(code, 0)); } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter13Test.java b/src/test/java/com/seaofnodes/simple/Chapter13Test.java index 3301fa5e..214bc69e 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter13Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter13Test.java @@ -1,6 +1,5 @@ package com.seaofnodes.simple; import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.print.IRPrinter; import org.junit.Test; @@ -46,13 +45,30 @@ return primeCount; } @Test + public void testClassField() { + CodeGen code = new CodeGen( +""" +if( arg < 2 ) return 0; +int primeCount = 1; +while( arg-- ) + primeCount++; +return primeCount; +"""); + code.driver(CodeGen.Phase.LocalSched); + assertEquals("0", Eval2.eval(code, 1)); // No primes 1 or below + assertEquals("3", Eval2.eval(code, 2)); // +} + + + @Test public void testAntiDeps1() { CodeGen code = new CodeGen( """ -struct S { int f; }; -!S !v=new S; +struct _S { int f; }; +!_S !v=new _S; v.f = 2; -int i=new S.f; +_S q=new _S; +int i=q.f; i=v.f; if (arg) v.f=1; return i; @@ -67,10 +83,10 @@ return i; public void testAntiDeps2() { CodeGen code = new CodeGen( """ -struct S { int f; }; -!S !v = new S; +struct _S { int f; }; +!_S !v = new _S; v.f = arg; -!S !t = new S; +!_S !t = new _S; int i = 0; if (arg) { if (arg+1) v = t; @@ -88,19 +104,19 @@ return i; public void testAntiDeps3() { CodeGen code = new CodeGen( """ -struct S { int f; }; -!S !v0 = new S; -!S? !v1; -if (arg) v1 = new S; +struct _S { int f; }; +!_S !v0 = new _S; +!_S? !v1; +if (arg) v1 = new _S; if (v1) { v0.f = v1.f; } else { v0.f = 2; } -return v0; +return v0.f; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return S;", code.print()); + assertEquals("return Phi(Region,.f,2);", code.print()); } @@ -108,10 +124,10 @@ return v0; public void testAntiDeps4() { CodeGen code = new CodeGen( """ -struct S { int f; }; -!S !v = new S; +struct _S { int f; }; +!_S !v = new _S; v.f = arg; -S t = new S; +_S t = new _S; int i = v.f; if (arg+1) arg= 0; while (arg) v.f = 2; @@ -125,27 +141,29 @@ return i; public void testAntiDeps5() { CodeGen code = new CodeGen( """ -struct S { int f; }; -!S !v = new S; +struct _S { int f; }; +!_S !v = new _S; while(1) { while(arg+1) { arg=arg-1; } if (arg) break; v.f = 2; } -return v; +return v.f; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return S;", code.print()); + assertEquals("return Phi(Loop,0,2);", code.print()); + for( int arg : new int[]{-1,0,1,3,100} ) + assertEquals("0",Eval2.eval(code,arg)); } @Test public void testAntiDeps6() { CodeGen code = new CodeGen( """ -struct s { int v; }; -!s !ptr=new s; +struct _S { int v; }; +!_S !ptr=new _S; while( -arg ) - ptr = new s; + ptr = new _S; while(1) arg = arg+ptr.v; """); @@ -157,9 +175,9 @@ while(1) public void testAntiDeps7() { CodeGen code = new CodeGen( """ -struct S { int f; }; -!S !v = new S; -S t = new S; +struct _S { int f; }; +!_S !v = new _S; +_S t = new _S; int i = v.f; while (arg) { v.f = arg; @@ -175,9 +193,9 @@ return i; public void testAntiDeps8() { CodeGen code = new CodeGen( """ -struct S { int f; }; -!S !v = new S; -S t = new S; +struct _S { int f; }; +!_S !v = new _S; +_S t = new _S; while(arg) { arg=arg-1; int f = v.f; @@ -195,26 +213,26 @@ return arg; public void testAntiDeps9() { CodeGen code = new CodeGen( """ -struct S { int f; }; -!S !v = new S; -S t = new S; +struct _S { int f; }; +!_S !v = new _S; +_S t = new _S; if (arg) { v.f=2; int i=t.f; v.f=i; } -return v; +return v.f; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return S;", code.print()); + assertEquals("return 0;", code.print()); } @Test public void testExample2() { CodeGen code = new CodeGen( """ -struct S { int f; }; -!S !v = new S; +struct _S { int f; }; +!_S !v = new _S; int i = arg; while (arg > 0) { int j = i/3; @@ -225,7 +243,7 @@ while (arg > 0) { return v; """); code.driver(CodeGen.Phase.FinalizeIR); - //assertEquals("return new S;", code.print()); + //assertEquals("return new _S;", code.print()); } @Test diff --git a/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java b/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java index e9333837..e058a822 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java +++ b/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java @@ -1,11 +1,9 @@ package com.seaofnodes.simple; import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.node.StopNode; import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; public class Chapter14FloatTest { @Test public void testNumericAssignments() { @@ -53,22 +51,23 @@ public class Chapter14FloatTest { public void testJig() { CodeGen code = new CodeGen( """ -return 3.14; +return 0; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return 3.14;", code.print()); - assertEquals("3.14", Eval2.eval(code, 0)); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); } @Test public void testFloat() { CodeGen code = new CodeGen( """ -return 3.14; +flt x = 3.14; +return 0; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return 3.14;", code.print()); - assertEquals("3.14", Eval2.eval(code, 0)); + assertEquals("return 0;", code.print()); + assertEquals("0", Eval2.eval(code, 0)); } @Test @@ -83,6 +82,8 @@ while( 1 ) { } return guess; """); + // TODO: One of the problems with <clinit> returning an OS error code, + // is it chops all results to int. code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return Phi(Loop,(flt)arg,((Phi_guess+(ToFloat/Phi_guess))*0.5f));", code.print()); assertEquals("3.0", Eval2.eval(code, 9)); diff --git a/src/test/java/com/seaofnodes/simple/Chapter14Test.java b/src/test/java/com/seaofnodes/simple/Chapter14Test.java index 06d86f5f..bc2ad8d3 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter14Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter14Test.java @@ -94,12 +94,12 @@ return b; public void testRefLoad() { CodeGen code = new CodeGen( """ -struct Foo { u1 b; }; -!Foo !f = new Foo; +struct _Foo { u1 b; }; +!_Foo !f = new _Foo; f.b = 123; return f.b; """); - code.driver(CodeGen.Phase.FinalizeIR); + code.driver(CodeGen.Phase.TypeCheck); assertEquals("return 1;", code.print()); assertEquals("1", Eval2.eval(code, 0)); } @@ -176,7 +176,7 @@ while(v1) break; return v1; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return (Phi(Loop,(((1<<arg)&1)&Shl),0)&4294967295);", code.print()); + assertEquals("return Phi(Loop,(((1<<arg)&1)&Shl),0);", code.print()); assertEquals("1", Eval2.eval(code, 0)); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter15Test.java b/src/test/java/com/seaofnodes/simple/Chapter15Test.java index 9aaafc28..c2071cb6 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter15Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter15Test.java @@ -21,9 +21,7 @@ public class Chapter15Test { """; var code = new CodeGen(src).parse(); var stop = code._stop; - var main = code.link(code._main); - var load = stop.walk(n -> n instanceof com.seaofnodes.simple.node.LoadNode ld && ld._name.equals("[]") && - ld.in(0)!=null && ld.cfg0().fun()==main ? ld : null); + var load = stop.walk(n -> n instanceof com.seaofnodes.simple.node.LoadNode ld && ld._name.equals("[]") ? ld : null); org.junit.Assert.assertNotNull(load); // The parser uses current control as a placeholder for a range check. // Model an explicit check already performed before the inner diamond: @@ -34,8 +32,7 @@ public class Chapter15Test { load.setDef(0,checked); code.driver(CodeGen.Phase.TypeCheck); org.junit.Assert.assertTrue("Common dominating control permits Phi splitting",load.isDead()); - var split = stop.walk(n -> n instanceof com.seaofnodes.simple.node.LoadNode ld && ld._name.equals("[]") && - ld.in(0)!=null && ld.cfg0().fun()==main ? ld : null); + var split = stop.walk(n -> n instanceof com.seaofnodes.simple.node.LoadNode ld && ld._name.equals("[]") ? ld : null); org.junit.Assert.assertNotNull(split); org.junit.Assert.assertSame("The surviving read retains the earlier check",checked,split.in(0)); code.driver(CodeGen.Phase.LocalSched); @@ -57,9 +54,7 @@ public class Chapter15Test { """; var code = new CodeGen(src).parse(); var stop = code._stop; - var main = code.link(code._main); - var load = stop.walk(n -> n instanceof com.seaofnodes.simple.node.LoadNode ld && ld._name.equals("[]") && - ld.in(0)!=null && ld.cfg0().fun()==main ? ld : null); + var load = stop.walk(n -> n instanceof com.seaofnodes.simple.node.LoadNode ld && ld._name.equals("[]") ? ld : null); org.junit.Assert.assertNotNull(load); org.junit.Assert.assertNotNull("Element loads carry their range-check control",load.in(0)); code.driver(CodeGen.Phase.LocalSched); @@ -84,14 +79,14 @@ return 3.14; public void testCyclic() { CodeGen code = new CodeGen( """ -struct C { !C? !l; }; -!C !c = new C; +struct _C { !_C? !l; }; +!_C !c = new _C; c.l = c; return c; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return C;", code.print()); - assertEquals("C{l=$cyclic}", Eval2.eval(code, 0)); + assertEquals("return Test._C;", code.print()); + assertEquals("Test._C{l=$cyclic}", Eval2.eval(code, 0)); } @Test @@ -156,34 +151,34 @@ return a[0]; public void testBasic4() { CodeGen code = new CodeGen( """ -struct A { int i; }; -!A?[] !a = new !A?[2]; +struct _A { int i; }; +!_A?[] !a = new !_A?[2]; return a; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return []*A?;", code.print()); - assertEquals("*A {i64 !i; }?[ null,null]", Eval2.eval(code, 0)); + assertEquals("return []*Test._A?;", code.print()); + assertEquals("*Test._A {i64 !i; }?[ null,null]", Eval2.eval(code, 0)); } @Test public void testBasic5() { CodeGen code = new CodeGen( """ -struct S { int x; flt y; }; -// A new S -!S !s = new S; s.x=99; s.y = 3.14; +struct _S { int x; flt y; }; +// A new _S +!_S !s = new _S; s.x=99; s.y = 3.14; -// Double-d array of Ss. Fill in one row. -!S?[]?[] !iss = new !S?[]?[2]; -iss[0] = new !S?[7]; +// Double-d array of _Ss. Fill in one row. +!_S?[]?[] !iss = new !_S?[]?[2]; +iss[0] = new !_S?[7]; iss[0][2] = s; // Now pull out the filled-in value, with null checks flt rez; -!S?[]? is = iss[arg]; +!_S?[]? is = iss[arg]; if( !is ) rez = 1.2; else { - S? i = is[2]; + _S? i = is[2]; if( !i ) rez = 2.3; else rez = i.y; } @@ -199,13 +194,13 @@ return rez; public void testBasic6() { CodeGen code = new CodeGen( """ -struct S { int x; flt y; }; -// A new S -!S !s = new S; s.x=99; s.y = 3.14; +struct _S { int x; flt y; }; +// A new _S +!_S !s = new _S; s.x=99; s.y = 3.14; -// Double-d array of Ss. Fill in one row. -!S?[]?[] !iss = new !S?[]?[2]; -iss[0] = new !S?[7]; +// Double-d array of _Ss. Fill in one row. +!_S?[]?[] !iss = new !_S?[]?[2]; +iss[0] = new !_S?[7]; iss[0][2] = s; // Now pull out the filled-in value, with null checks @@ -226,22 +221,22 @@ return rez;"""); CodeGen code = new CodeGen( """ // Can we define a forward-reference array? -struct Tree { !Tree?[]? !_kids; }; -!Tree !root = new Tree; -root._kids = new !Tree?[2]; // NO BANG SO ARRAY IS OF IMMUTABLE TREES???? -root._kids[0] = new Tree; +struct _Tree { !_Tree?[]? !_kids; }; +!_Tree !root = new _Tree; +root._kids = new !_Tree?[2]; // NO BANG SO ARRAY IS OF IMMUTABLE TREES???? +root._kids[0] = new _Tree; return root; """); - code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Tree;", code.print()); - assertEquals("Tree{_kids=*Tree {*[]*Tree?? !_kids; }?[ Tree{_kids=null},null]}", Eval2.eval(code, 0)); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("return Test._Tree;", code.print()); + assertEquals("Test._Tree{_kids=*Test._Tree {*[]*Test._Tree?[mutable]? !_kids; }?[ Test._Tree{_kids=null},null]}", Eval2.eval(code, 0)); } @Test public void testNestedStructAddMemProj() { CodeGen code = new CodeGen( """ -struct S { int a; int[] !b; }; +struct _S { int a; int[] !b; new _S = { int[] ary -> b=ary; }; }; return 0; """); code.driver(CodeGen.Phase.FinalizeIR); @@ -267,7 +262,7 @@ while( i < ary# - 1 ) { } return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6 """); - code.driver(CodeGen.Phase.FinalizeIR); + code.driver(CodeGen.Phase.TypeCheck); assertEquals("return (.[]+(.[]*1000));", code.print()); assertEquals("1006", Eval2.eval(code, 4)); } @@ -285,12 +280,12 @@ return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6 public void testNewNodeInit() { CodeGen code = new CodeGen( """ -struct S {int i; flt f;}; -!S !s1 = new S; -!S !s2 = new S; +struct _S {int i; flt f;}; +!_S !s1 = new _S; +!_S !s2 = new _S; s2.i = 3; s2.f = 2.0; -if (arg) s1 = new S; +if (arg) s1 = new _S; return s1.i + s1.f; """); code.driver(CodeGen.Phase.FinalizeIR); @@ -327,8 +322,8 @@ return is; int[] is = new int[3.14]; return is[1]; """); - try { code.driver(CodeGen.Phase.FinalizeIR); fail(); } - catch( Exception e ) { assertEquals("Cannot allocate an array with length 3.14",e.getMessage()); } + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Exception e ) { assertEquals("Cannot '<<' 3.14",e.getMessage()); } } @Test diff --git a/src/test/java/com/seaofnodes/simple/Chapter16Test.java b/src/test/java/com/seaofnodes/simple/Chapter16Test.java index d066357f..2942cfee 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter16Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter16Test.java @@ -48,8 +48,8 @@ return x+y; @Test public void testConstruct0() { CodeGen code = new CodeGen(""" -struct X { int x=3; }; -X z = new X; +struct _X { int x=3; }; +_X z = new _X; return z.x; """); code.driver(CodeGen.Phase.FinalizeIR); @@ -60,8 +60,8 @@ return z.x; @Test public void testConstruct2() { CodeGen code = new CodeGen(""" -struct X { int x=3; }; -X z = new X { x = 4; }; +struct _X { int x=3; new _X = { int xx -> x=xx; }; }; +_X z = new _X(4); return z.x; """); code.driver(CodeGen.Phase.FinalizeIR); @@ -74,19 +74,19 @@ return z.x; public void testLinkedList1() { CodeGen code = new CodeGen( """ -struct LLI { !LLI? !next; int i; }; -!LLI? !head = null; +struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int ii -> next=n; i=ii; }; }; +!_LLI? !head = null; while( arg ) { - head = new LLI { next=head; i=arg; }; + head = new _LLI(head,arg); arg = arg-1; } if( !head ) return 0; -LLI? next = head.next; +_LLI? next = head.next; if( !next ) return 1; return next.i; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Phi(Region,0,1,.i);", code.print()); + assertEquals("return Phi(Region,1,.i,0);", code.print()); assertEquals("0", Eval2.eval(code, 0)); assertEquals("1", Eval2.eval(code, 1)); assertEquals("2", Eval2.eval(code, 3)); @@ -96,28 +96,27 @@ return next.i; public void testLinkedList2() { CodeGen code = new CodeGen( """ -struct LLI { !LLI? !next; int i; }; -!LLI? !head = null; +struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int a -> + next=n; + int !tmp=a; + while( a > 10 ) { + tmp = tmp + a; + a = a - 1; + } + i=tmp; +}; }; +!_LLI? !head = null; while( arg ) { - head = new LLI { - next=head; - // Any old code in the constructor - int !tmp=arg; - while( arg > 10 ) { - tmp = tmp + arg; - arg = arg - 1; - } - i=tmp; - }; + head = new _LLI(head,arg); arg = arg-1; } if( !head ) return 0; -LLI? next = head.next; +_LLI? next = head.next; if( !next ) return 1; return next.i; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Phi(Region,0,1,.i);", code.print()); + assertEquals("return Phi(Region,1,.i,0);", code.print()); assertEquals("0", Eval2.eval(code, 0)); assertEquals("1", Eval2.eval(code, 1)); assertEquals("2", Eval2.eval(code, 11)); @@ -127,7 +126,7 @@ return next.i; public void testSquare() { CodeGen code = new CodeGen( """ -struct Square { +struct _Square { flt !side = arg; // Newtons approximation to the square root, computed in a constructor. // The actual allocation will copy in this result as the initial @@ -139,34 +138,11 @@ struct Square { diag = next; } }; -return new Square; +return new _Square; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Square;", code.print()); - assertEquals("Square{side=3.0,diag=1.7320508075688772}", Eval2.eval(code, 3)); - assertEquals("Square{side=4.0,diag=2.0}", Eval2.eval(code, 4)); - } - static final String CONSTRUCTOR_MEMORY = """ - struct S { int x; int y; }; - struct T { int z=arg+40; }; - !T !t = new T; - !S !a = new S { x=11; y=7; }; - !S !b = new S { x=22; y=9; }; - !S !p=a; - if (arg) p=b; - int before=p.x; - !S !c = new S { - x=p.x+1; - { int i=0; while (i<2) { p.y=p.y+1; i=i+1; } } - y=p.y; - }; - p.x=33; - return before*10000+c.x*100+c.y+t.z; - """; - - @Test public void testConstructorMemory() { - var code = new CodeGen(CONSTRUCTOR_MEMORY).driver(CodeGen.Phase.FinalizeIR); - assertEquals("111249",Eval2.eval(code,0)); - assertEquals("222352",Eval2.eval(code,1)); + assertEquals("return Test._Square;", code.print()); + assertEquals("Test._Square{side=3.0,diag=1.7320508075688772}", Eval2.eval(code, 3)); + assertEquals("Test._Square{side=4.0,diag=2.0}", Eval2.eval(code, 4)); } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter17aTest.java b/src/test/java/com/seaofnodes/simple/Chapter17aTest.java index 64ecb235..ea7afe56 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter17aTest.java +++ b/src/test/java/com/seaofnodes/simple/Chapter17aTest.java @@ -24,8 +24,8 @@ return y; @Test public void testConstruct1() { CodeGen code = new CodeGen(""" -struct X { int ~x; }; -X z = new X { x=3; }; +struct _X { int ~x; new _X = { int xx -> x=xx; }; }; +_X z = new _X(3); return z.x; """); code.driver(CodeGen.Phase.FinalizeIR); @@ -36,20 +36,20 @@ return z.x; @Test public void testStructFinal0() { CodeGen code = new CodeGen(""" -struct Point { int ~x, ~y; }; -Point p = new Point { x=3; y=4; }; +struct _Point { int ~x, ~y; new _Point = { int xx, int yy -> x=xx; y=yy; }; }; +_Point p = new _Point(3,4); return p; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return (const)Point;", code.print()); - assertEquals("Point{x=3,y=4}", Eval2.eval(code, 0)); + assertEquals("return (const)Test._Point;", code.print()); + assertEquals("Test._Point{x=3,y=4}", Eval2.eval(code, 0)); } @Test public void testStructFinal1() { CodeGen code = new CodeGen(""" -struct Point { int x=3, y=4; }; -~Point p = new Point { x=5; y=6; }; +struct _Point { int x=3, y=4; new _Point = { int xx, int yy -> x=xx; y=yy; }; }; +~_Point p = new _Point(5,6); p.x++; return p; """); @@ -60,8 +60,8 @@ return p; @Test public void testStructFinal2() { CodeGen code = new CodeGen(""" -struct Point { int x=3, y=4; }; -~Point p = new Point; +struct _Point { int x=3, y=4; }; +~_Point p = new _Point; p.x++; return p; """); @@ -71,18 +71,21 @@ return p; @Test public void testStructFinal3() { - for( String fields : new String[] { "var x; var y;", "int[] !x;", "int[] !x;" } ) { - String src = "struct Point { "+fields+" }; Point p = new Point; p.x++; return p;"; - try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(fields); } - catch( Exception e ) { assertEquals("'Point' is not fully initialized, field 'x' needs to be set in a constructor",e.getMessage()); } - } + CodeGen code = new CodeGen(""" +struct _Point { var x; var y; }; +_Point p = new _Point; +p.x++; +return p; +"""); + try { code.driver(CodeGen.Phase.FinalizeIR); fail(); } + catch( Exception e ) { assertEquals("'Test._Point' is not fully initialized, field 'x' needs to be set in a constructor",e.getMessage()); } } @Test public void testStructFinal4() { CodeGen code = new CodeGen(""" -struct Point { val x=3; val y=4; }; -Point p = new Point; +struct _Point { val x=2; val y=4; }; +_Point p = new _Point; p.x++; return p; """); @@ -93,14 +96,14 @@ return p; @Test public void testStructFinal5() { CodeGen code = new CodeGen(""" -struct Point { var x=3; var y=4; }; -!Point !p = new Point; +struct _Point { var x=3; var y=4; }; +!_Point !p = new _Point; p.x++; return p; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Point;", code.print()); - assertEquals("Point{x=4,y=4}", Eval2.eval(code, 0)); + assertEquals("return Test._Point;", code.print()); + assertEquals("Test._Point{x=4,y=4}", Eval2.eval(code, 0)); } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java index 3fbf0daa..62dbbde4 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java +++ b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java @@ -19,7 +19,9 @@ public class Chapter17bTest { """).driver(CodeGen.Phase.LocalSched); int[] stores = {0}; code._stop.walk(n -> { - if( n instanceof com.seaofnodes.simple.node.StoreNode st && st._name.equals("x") ) { + // Constructor zero Stores remain in separate private memory. + if( n instanceof com.seaofnodes.simple.node.StoreNode st && st._name.equals("x") && + st.val() instanceof com.seaofnodes.simple.node.PhiNode ) { assertEquals(0,st.cfg0().loopDepth()); // One final Store outside the loop. stores[0]++; } @@ -28,7 +30,7 @@ public class Chapter17bTest { assertEquals(1,stores[0]); for( int arg : new int[]{-1,0,1,4} ) { long expected = arg<=0 ? 10 : arg; - assertEquals("S{x="+expected+"}",Eval2.eval(code,arg)); + assertEquals("Test.S{x="+expected+"}",Eval2.eval(code,arg)); } } @@ -106,8 +108,8 @@ return xs[0]; @Test public void testInc5() { CodeGen code = new CodeGen(""" -struct S { u16 x; }; -!S !s = new S; +struct _S { u16 x; }; +!_S !s = new _S; s.x--; return s.x; """); @@ -159,13 +161,13 @@ return s.x; @Test public void testVar0() { CodeGen code = new CodeGen("var d; return d;"); try { code.driver(CodeGen.Phase.FinalizeIR); fail(); } - catch( Exception e ) { assertEquals("Syntax error, expected =expression: ;",e.getMessage()); } + catch( Exception e ) { assertEquals("Syntax error, expected `=expression` but found `;`",e.getMessage()); } } @Test public void testVar1() { CodeGen code = new CodeGen("val d; return d;"); try { code.driver(CodeGen.Phase.FinalizeIR); fail(); } - catch( Exception e ) { assertEquals("Syntax error, expected =expression: ;",e.getMessage()); } + catch( Exception e ) { assertEquals("Syntax error, expected `=expression` but found `;`",e.getMessage()); } } @Test public void testVar2() { @@ -183,14 +185,14 @@ return s.x; } @Test public void testVar4() { - CodeGen code = new CodeGen("struct S{int x;}; !S? !s; s=new S; s.x++; return s.x; // Ok, no initializer so x is mutable "); + CodeGen code = new CodeGen("struct _S{int x;}; !_S? !s; s=new _S; s.x++; return s.x; // Ok, no initializer so x is mutable "); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 1;", code.print()); assertEquals("1", Eval2.eval(code, 0)); } @Test public void testVar5() { - CodeGen code = new CodeGen("struct S{int x;}; !S? !s; s=new S{x=3;}; s.x++; return s.x; // Ok, no initializer so x is mutable "); + CodeGen code = new CodeGen("struct _S{int x; new _S={ int xx -> x=xx; };}; !_S? !s; s=new _S(3); s.x++; return s.x; // Ok, no initializer so x is mutable "); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 4;", code.print()); assertEquals("4", Eval2.eval(code, 0)); @@ -203,20 +205,20 @@ return s.x; } @Test public void testVar7() { - CodeGen code = new CodeGen("struct S{int x;}; ~S s = new S{x=3;}; s.x++; return s.x; // Error initializer so x is immutable "); + CodeGen code = new CodeGen("struct S{int x; new S={ int xx -> x=xx; };}; ~S s = new S(3); s.x++; return s.x; // Error initializer so x is immutable "); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } catch( Exception e ) { assertEquals("Cannot modify final field 'x'",e.getMessage()); } } @Test public void testVar8() { - CodeGen code = new CodeGen("struct S{int x;}; !S !s=new S; s.x++; return s.x; // Ok, has '!' so s.x is mutable "); + CodeGen code = new CodeGen("struct _S{int x;}; !_S !s=new _S; s.x++; return s.x; // Ok, has '!' so s.x is mutable "); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 1;", code.print()); assertEquals("1", Eval2.eval(code, 0)); } @Test public void testVar9() { - CodeGen code = new CodeGen("struct S{int x;}; var s=new S; s.x++; return s.x; // Ok, has var so s.x is mutable "); + CodeGen code = new CodeGen("struct _S{int x;}; var s=new _S; s.x++; return s.x; // Ok, has var so s.x is mutable "); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return 1;", code.print()); assertEquals("1", Eval2.eval(code, 0)); @@ -234,8 +236,8 @@ struct Bar { int x; }; !Bar !bar = new Bar; bar.x = 3; // Ok, bar is mutable -struct Foo { !Bar? !bar; int y; }; -!Foo !foo = new Foo { bar = bar; }; +struct Foo { !Bar? !bar; int y; new Foo = { !Bar? b -> bar = b; }; }; +!Foo !foo = new Foo(bar); foo.bar = bar; // Ok foo is mutable foo.bar.x++; // Ok foo and foo.bar and foo.bar.x are all mutable @@ -249,16 +251,16 @@ return xfoo.bar.x; @Test public void testVar12() { CodeGen code = new CodeGen(""" -struct Bar { int x; }; -!Bar !bar = new Bar; +struct _Bar { int x; }; +!_Bar !bar = new _Bar; bar.x = 3; // Ok, bar is mutable -struct Foo { !Bar? !bar; int y; }; -!Foo !foo = new Foo; +struct _Foo { !_Bar? !bar; int y; }; +!_Foo !foo = new _Foo; foo.bar = bar; // Ok bar is mutable foo.bar.x++; // Ok foo and foo.bar and foo.bar.x are all mutable -~Foo xfoo = foo; // Throw away mutability +~_Foo xfoo = foo; // Throw away mutability int x4 = xfoo.bar.x; // Ok to read through xfoo, gets 4 foo.bar.x++; // Bumps to 5 int x5 = xfoo.bar.x; // Ok to read through xfoo, gets 5 @@ -283,41 +285,64 @@ int i,i++; public void testVar14() { CodeGen code = new CodeGen(""" struct B {}; -struct A { !B !b; }; -A x = new A { +struct A { !B !b; new A = { -> return b; // read before init b = new B; -}; +}; }; +A x = new A; """); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Cannot read uninitialized field 'b'",e.getMessage()); } + catch( Exception e ) { assertEquals("Field 'b' might not be initialized",e.getMessage()); } } @Test public void testVar15() { - CodeGen code = new CodeGen(""" -struct B {}; -struct A { !B !b; }; -return new A { - if (arg) b = new B; // Constructor ends with partial init of b -}.b; -"""); - try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("'A' is not fully initialized, field 'b' needs to be set in a constructor",e.getMessage()); } + for( String fields : new String[] { + "!B !b; new A = { -> if (arg) b = new B; };", + "!B !b; new A = { -> if (arg) return 0; b = new B; };", + "!B !b; new A = { !B !b -> b = new B; };", // Mutable argument shadows field + "new A = { -> }; !B !b;", // Field declared later + } ) { + CodeGen code = new CodeGen("struct B {}; struct A { "+fields+" }; return 0;"); + try { code.parse(); fail(fields); } + catch( Parser.ParseException e ) { assertEquals("'Test.A' is not fully initialized, field 'b' is only partially set in the constructor",e.getMessage()); } + } + } + + @Test + public void testDefaultConstructorFields() { + for( String field : new String[] { "!B !b;", "!B !b;" } ) { + CodeGen code = new CodeGen("struct B {}; struct A { "+field+" }; return new A;"); + try { code.parse(); fail(field); } + catch( Parser.ParseException e ) { assertEquals("'Test.A' is not fully initialized, field 'b' needs to be set in a constructor",e.getMessage()); } + } + for( String fields : new String[] { + "B b = new B;", + "!B !b; b = new B;", + "new A = { -> }; B b = new B;", + "!B !b; new A = { -> if (arg) { b = new B; return 0; } b = new B; };", + } ) { + CodeGen code = new CodeGen("struct B {}; struct A { "+fields+" }; return new A.b != null;"); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + assertEquals("1",Eval2.eval(code,1)); + } + // No returning path means no object can escape uninitialized. + new CodeGen("struct B {}; struct A { !B !b; new A = { -> while (true) {} }; }; return 0;").parse(); } @Test public void testVar16() { CodeGen code = new CodeGen( """ -struct S{}; +struct S{ new S = { -> x = 2; }; }; val x = 1; -~S s = new S{x = 2;}; +~S s = new S; return x; """ ); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Cannot reassign final 'x'",e.getMessage()); } + catch( Exception e ) { assertEquals("Undefined name 'x'",e.getMessage()); } } @Test @@ -336,7 +361,7 @@ return me; """ ); try { code.parse(); fail(); } - catch( Exception e ) { assertEquals("Syntax error, expected =expression: ;",e.getMessage()); } + catch( Exception e ) { assertEquals("Syntax error, expected `=expression` but found `;`",e.getMessage()); } } // --------------------------------------------------------------- @@ -362,23 +387,36 @@ return arg ? 0 : arg; } @Test + public void testShortCircuitGuard() { + CodeGen code = new CodeGen(""" +struct S { int !x; }; +val get = { !S s -> s.x; }; +!S? !s = arg ? new S; +if( !s || !arg ) return 0; +return get(s); +"""); + code.driver(CodeGen.Phase.TypeCheck); + } + + @Test public void testTrinary2() { CodeGen code = new CodeGen(""" -struct Bar { int x; }; -var b = arg ? new Bar : null; +struct _Bar { int x; }; +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("-1", Eval2.eval(code, 0)); - assertEquals("1", Eval2.eval(code, 1)); + + assertEquals("1", Eval2.eval(code, 1)); } @Test public void testTrinary3() { CodeGen code = new CodeGen(""" -struct Bar { int x; }; -var b = arg ? new Bar; +struct _Bar { int x; }; +var b = arg ? new _Bar; return b ? b.x++ + b.x++ : -1; """); code.driver(CodeGen.Phase.FinalizeIR); @@ -391,8 +429,8 @@ return b ? b.x++ + b.x++ : -1; public void testTrinary4() { // This test case will benefit from an unzipping transformation CodeGen code = new CodeGen(""" -struct Bar { !Bar? !next; int x; }; -var b = arg ? new Bar { next = (arg==2) ? new Bar{x=2;}; x=1; }; +struct _Bar { !_Bar? !next; int x; new _Bar = { !_Bar? !n, int xx -> next=n; x=xx; }; }; +var b = arg ? new _Bar((arg==2) ? new _Bar(null,2), 1); return b ? b.next ? b.next.x : b.x; // parses "b ? (b.next ? b.next.x : b.x) : 0" """); code.driver(CodeGen.Phase.FinalizeIR); @@ -421,7 +459,7 @@ struct S{}; return arg ? 7 : new S; """); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("No common type amongst int and reference",e.getMessage()); } + catch( Exception e ) { assertEquals("Cannot return generic scalar",e.getMessage()); } } // --------------------------------------------------------------- @@ -496,48 +534,48 @@ return i; for(;;arg++;) {} """); try { code.driver(CodeGen.Phase.FinalizeIR); fail(); } - catch( Exception e ) { assertEquals("Syntax error, expected Unexpected code after expression: ;",e.getMessage()); } + catch( Exception e ) { assertEquals("Syntax error, expected `Unexpected code after expression` but found `;`",e.getMessage()); } } - // --------------------------------------------------------------- @Test public void testForward0() { CodeGen code = new CodeGen(""" -struct A{ +struct _A { !B? !f1; !B? !f2; }; -return new A; +return new _A; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return A;", code.print()); - assertEquals("A{f1=null,f2=null}", Eval2.eval(code,0)); + assertEquals("return Test._A;", code.print()); + assertEquals("Test._A{f1=null,f2=null}", Eval2.eval(code,0)); } @Test public void testForward1() { CodeGen code = new CodeGen(""" -struct A { +struct _A { !B?[]? !nil_array_of_b; !B?[] !array_of_b; + new _A = { !B?[] bs -> array_of_b = bs; }; }; -return new A{array_of_b = new !B?[0]; }.array_of_b; +return new _A(new !B?[0]).array_of_b; """); code.driver(CodeGen.Phase.FinalizeIR); assertEquals("return []*B?;", code.print()); - assertEquals("*B {... }?[]", Eval2.eval(code,0)); + assertEquals("*B {}?[]", Eval2.eval(code,0)); } // --------------------------------------------------------------- @Test public void testLinkedList2() { CodeGen code = new CodeGen(""" -struct LLI { !LLI? !next; int i; }; -!LLI? !head = null; +struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int ii -> next=n; i=ii; }; }; +!_LLI? !head = null; while( arg-- ) - head = new LLI { next=head; i=arg; }; + head = new _LLI(head,arg); int sum=0; var ptr = head; // A read-only ptr, to be assigned from read-only next fields for( ; ptr; ptr = ptr.next ) @@ -575,7 +613,7 @@ for( int j=0; j<nprimes; j++ ) rez[j] = primes[j]; return rez; """); - code.driver(CodeGen.Phase.Schedule); + 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)); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter18Test.java b/src/test/java/com/seaofnodes/simple/Chapter18Test.java index 732d4fa1..632b9539 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter18Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter18Test.java @@ -94,7 +94,7 @@ public class Chapter18Test { new CodeGen(test[0]).driver(CodeGen.Phase.TypeCheck); fail("Expected incompatible return types: " + test[0]); } catch( Parser.ParseException e ) { - assertEquals(test[1],e.getMessage()); + assertEquals("Cannot return generic scalar",e.getMessage()); } } } @@ -102,10 +102,10 @@ public class Chapter18Test { @Test public void testPrintingForwardReferenceScope() throws Exception { new CodeGen("return 0;").parse(); - var declared = TypeMemPtr.make(TypeStruct.open("PrintForward")); + var declared = TypeMemPtr.make(TypeStruct.make("PrintForward",true,true)); var scope = new ScopeNode(); scope.define("ptr",declared,false,new ConstantNode(TypeInteger.ZERO),null); - Parser.TYPES.put("PrintForward",TypeMemPtr.make(TypeStruct.make("PrintForward",false))); + Parser.TYPES.put("PrintForward",TypeMemPtr.make(TypeStruct.make("PrintForward",false,false))); var field = Var.class.getDeclaredField("_type"); field.setAccessible(true); var v = scope.var(0); @@ -123,7 +123,7 @@ public class Chapter18Test { return 0; """); code.driver(Phase.LocalSched); - assertEquals("return 0;", code._stop.toString()); + assertEquals("return 0;", code.print()); assertEquals("0", Eval2.eval(code, 2)); } @@ -141,7 +141,7 @@ while(arg--) { return a; """); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("return Phi(Loop,1,Phi(Loop,2,Phi_a));", code._stop.toString()); + assertEquals("return Phi(Loop,1,Phi(Loop,2,Phi_a));", code.print()); assertEquals("1", Eval2.eval(code, 0)); assertEquals("2", Eval2.eval(code, 1)); assertEquals("1", Eval2.eval(code, 2)); @@ -158,7 +158,7 @@ return a; return x2; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return null;", code._stop.toString()); + assertEquals("return null;", code.print()); assertEquals("null", Eval2.eval(code, 0 ) ); } @@ -171,31 +171,50 @@ return x2; }; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return (Parm_x(sq,i64)*x);", code._stop.toString()); - //assertEquals("{ int -> int #1}", Eval2.eval(code, 3)); + assertEquals("Stop[ return { sq}; return (Parm_x(sq,i64)*x); ]", code.print()); + assertEquals("{ ptr i64 -> i64 #[ 3]}", Eval2.eval(code, 3)); } @Test public void testFcn1() { CodeGen code = new CodeGen( """ -var sq = { int x -> +var _sq = { int x -> x*x; }; -return sq(arg)+sq(3); +return _sq(arg)+_sq(3); """); code.driver(Phase.LocalSched); - assertEquals("Stop[ return (#2+#2); return (Parm_x(sq,i64)*x); ]", code._stop.toString()); + assertEquals("return ((arg*arg)+9);", code.print()); assertEquals("13", Eval2.eval(code, 2)); } // Function scope test @Test - public void testFcn2() { + public void testFcn2A() { CodeGen code = new CodeGen( """ -int cnt=1; -return { -> cnt; }; +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)); + } + + // Function scope test + @Test + public void testFcn2B() { + CodeGen code = new CodeGen( +""" +val ctr = { -> + int cnt; // Function-local var + return { -> cnt=cnt+1; cnt; }; // Escape cnt out of scope +}; +val A = ctr(); // Make a counter +val B = ctr(); // Make a counter +A(); // Increment counter A +return A()*10 + B(); // Incr A and B; result: 2*10+1 == 21 """); try { code.driver(CodeGen.Phase.FinalizeIR); fail(); } catch( Exception e ) { assertEquals("Variable 'cnt' is out of function scope and must be a final constant",e.getMessage()); } @@ -210,7 +229,7 @@ val cnt=2; return { -> cnt; }(); """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return 2;", code._stop.toString()); + assertEquals("return 2;", code.print()); assertEquals("2", Eval2.eval(code, 0)); } @@ -223,8 +242,7 @@ var fcn = arg ? { int x -> x*x; } : { int x -> x+x; }; return fcn(3); """); code.driver(CodeGen.Phase.FinalizeIR); - // Both addresses stay local, so both callees see only x=3. - assertEquals("Stop[ return #2; return 9; return 6; ]", code._stop.toString()); + assertEquals("Stop[ return #2; return (Parm_x($fun3,i64,3)*x); return (Parm_x($fun4,i64,3)<<1); ]", code.print()); assertEquals("6", Eval2.eval(code, 0)); assertEquals("9", Eval2.eval(code, 1)); } @@ -232,9 +250,12 @@ 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 -> x <= 1 ? 1 : x*_fact(x-1); }; return _fact(arg);"); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("Stop[ return #2; return Phi(Region,1,(Parm_x(fact,i64)*#2)); ]", code._stop.toString()); + String ir = code.print(); + assertTrue(ir.equals("Stop[ return #2; return Phi(Region,1,(Parm_x(_fact,arg,(x-1))*#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)); assertEquals( "1", Eval2.eval(code, 1)); assertEquals( "2", Eval2.eval(code, 2)); @@ -242,31 +263,55 @@ return fcn(3); assertEquals("24", Eval2.eval(code, 4)); } + // The multiply initially sees one integer input and one unresolved + // The multiply initially sees one integer input and one unresolvedfcn8 + // 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);"); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("1.0", Eval2.eval(code, 0)); + assertEquals("1.0", Eval2.eval(code, 1)); + assertEquals("2.0", Eval2.eval(code, 2)); + assertEquals("6.0", Eval2.eval(code, 3)); + assertEquals("24.0", Eval2.eval(code, 4)); + } + + @Test + public void testFcn5Minus() { + CodeGen code = new CodeGen("val _neg = { int x -> x ? -_neg(x-1) : 1; }; return _neg(arg);"); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals( "1", Eval2.eval(code, 0)); + assertEquals("-1", Eval2.eval(code, 1)); + assertEquals( "1", Eval2.eval(code, 2)); + assertEquals("-1", Eval2.eval(code, 3)); + } + @Test public void testFcn6() { CodeGen code = new CodeGen( """ -struct S { int i; }; -val newS = { int x -> return new S { i=x; }; }; -return newS(1).i; +struct _S { int i; new _S = { int x -> i=x; }; }; +val _newS = { int x -> return new _S(x); }; +return _newS(1).i; """); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("return 1;", code._stop.toString()); + assertEquals("return 1;", code.print()); assertEquals("1", Eval2.eval(code, 0)); } - // Double forward reference + // Only forward refs to direct function calls allowed @Test public void testFcn7() { CodeGen code = new CodeGen( """ -if( arg ? f : g ) return 1; -val f = {->1;}; -val g = {->2;}; +val _f = {->1;}; +val _g = {->2;}; +if( arg ? _f : _g ) return 1; return 2; """); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("return 1;", code._stop.toString()); + assertEquals("return 1;", code.print()); assertEquals("1", Eval2.eval(code, 0)); } @@ -274,17 +319,17 @@ return 2; public void testFcn8() { CodeGen code = new CodeGen( """ -{int -> int}? !i2i = null; -var id = {{int->int} f-> return f;}; +{int -> int}? !_i2i = null; +var _id = {{int->int} f-> return f;}; for(;;) { - if (i2i) return i2i(arg); - var x = {int i-> return i;}; - arg = x(3); - i2i = id(x); + if (_i2i) return _i2i(arg); + var _x = {int i-> return i;}; + arg = _x(3); + _i2i = _id(_x); } """); code.driver(Phase.LocalSched); - assertEquals("return Phi(Loop,arg,3);", code._stop.toString()); + assertEquals("return Phi(Loop,arg,3);", code.print()); assertEquals("3", Eval2.eval(code, 0)); } @@ -300,8 +345,8 @@ for(;;) { } """); code.driver(Phase.LocalSched); - assertEquals("return Top;", code._stop.toString()); - assertNull(Eval2.eval(code,0)); // Infinite loop: i2i remains null. + assertEquals("Stop[ return Top; return Parm_i(x,i64); ]", code.print()); + assertNull( Eval2.eval( code, 0 ) ); // Infinite loop, <clinit> never exits } @@ -309,23 +354,23 @@ for(;;) { public void testFcn10() { CodeGen code = new CodeGen( """ -struct Person { +struct _Person { int age; }; -val fcn = { !Person?[] !ps, int x -> +val _fcn = { !_Person?[] !ps, int x -> val tmp = ps[x]; if( ps[x] ) ps[x].age++; }; -var ps = new !Person?[2]; -ps[0] = new Person; -ps[1] = new Person; -return fcn(ps,1); +var ps = new !_Person?[2]; +ps[0] = new _Person; +ps[1] = new _Person; +return _fcn(ps,1); """); code.driver(Phase.LocalSched); - assertEquals("return 0;", code._stop.toString()); + assertEquals("return .age;", code.print()); assertEquals("0", Eval2.eval(code, 0)); } @@ -360,7 +405,7 @@ for(;;) { return 0; """); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Might be null calling { i64 -> i64 #35}?",e.getMessage()); } + catch( Exception e ) { assertEquals("Might be null calling { ptr i64 -> i64 #[ 3]}?",e.getMessage()); } } @@ -372,7 +417,7 @@ val f = { int i, int j -> return i+j; }; return f(); """); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Expecting 2 arguments, but found 0",e.getMessage()); } + catch( Exception e ) { assertEquals("Expecting 3 arguments, but found 1",e.getMessage()); } } @@ -384,13 +429,11 @@ struct S { {int} f = { -> x(); return 0; }; // Do not let fref x be a field }; val x = { -> return 1; }; -!S? !s = null; -for(;;) { - if (s) return s.x; -} +S? s = new S; +return s.x; """); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Accessing unknown field 'x' from '*S'",e.getMessage()); } + catch( Exception e ) { assertEquals("Accessing unknown field 'x' from '*Test.S'",e.getMessage()); } } @Test @@ -416,7 +459,7 @@ 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); """); - code.driver(CodeGen.Phase.FinalizeIR); + code.driver(CodeGen.Phase.TypeCheck); assertEquals("1", Eval2.eval(code, 0)); assertEquals("0", Eval2.eval(code, 1)); assertEquals("1", Eval2.eval(code, 2)); @@ -437,8 +480,8 @@ for(;;) { if (s) return s.f; } """); - code.driver(CodeGen.Phase.Schedule); - assertEquals("return 0;", code._stop.toString()); + code.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule); + assertEquals("return 0;", code.print()); assertEquals("0", Eval2.eval(code, 0)); } @@ -447,15 +490,15 @@ for(;;) { public void testInline() { CodeGen code = new CodeGen( """ -{int->int}?! i2i = {int i->return i;}; -{{int->int}->{int->int}}! f2f = {{int->int} f->return f;}; -val o = i2i; -if (arg) i2i = null; -if (i2i) return i2i(arg); -return f2f(o)(1); +{int->int}?! _i2i_noInline = {int i->return i;}; +{{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); +return _f2f(_o)(1); """); code.driver(Phase.LocalSched); - assertEquals("return Phi(Region,arg,1);", code._stop.toString()); + assertEquals("Stop[ return Phi(Region,#2,#2); return Parm_i(_i2i_noInline,i64); ]", code.print()); assertEquals("1", Eval2.eval(code, 2)); } @@ -463,82 +506,16 @@ return f2f(o)(1); public void testOperField() { CodeGen code = new CodeGen( """ -struct Person { +struct _Person { int coffee_count; }; -!Person !p = new Person; +!_Person !p = new _Person; p.coffee_count += 1; return p.coffee_count; """); code.driver(Phase.LocalSched); - assertEquals("return 1;", code._stop.toString()); + assertEquals("return 1;", code.print()); assertEquals("1", Eval2.eval(code, 2)); } - static final String CALL_MEMORY = """ - struct S { int x; int y; }; - val bump = { !S !s, int d -> int old=s.x; s.x=old+d; return old; }; - !S !a = new S { x=10; y=7; }; - !S !b = new S { x=20; y=9; }; - !S !p=a; if (arg) p=b; - int before=p.x; - int old=bump(p,3); - int after=p.x; - bump(a,5); - return before*1000000+old*10000+after*100+a.x+a.y; - """; - - @Test public void testCallMemory() { - var code = new CodeGen(CALL_MEMORY).driver(CodeGen.Phase.FinalizeIR); - int[] calls={0}; - code._stop.walk(n -> { if(n instanceof CallNode) calls[0]++; return null; }); - assertEquals(2,calls[0]); // Keep real calls so this checks scheduling across them. - assertEquals("10101325",Eval2.eval(code,0)); - assertEquals("20202322",Eval2.eval(code,1)); - code._stop.walk(n -> { - if(n instanceof CallNode call) { - assertEquals(5,call.nIns()); // No scheduling edges may become extra arguments. - assertTrue(call.fptr()._type instanceof TypeFunPtr); - } - return null; - }); - } - - static final String RECURSIVE_MEMORY = """ - struct S { int x; }; - val rec = { !S !s, int n -> - if (n==0) return s.x; - int before=s.x; - s.x=before+1; - return before+rec(s,n-1); - }; - !S !s=new S { x=10; }; - int value=rec(s,arg); - return value*100+s.x; - """; - - @Test public void testRecursiveMemory() { - var code = new CodeGen(RECURSIVE_MEMORY).driver(CodeGen.Phase.FinalizeIR); - for(int n=0; n<7; n++) - assertEquals(Long.toString((10L*(n+1)+n*(n+1)/2)*100+10+n),Eval2.eval(code,n)); - } - - static final String INLINE_MEMORY = """ - struct S { int x; int y; }; - val make = { !S s -> - S t = new S { x=s.x+1; y=3; }; - s.y=9; - return t; - }; - !S !a = new S { x=arg+40; y=7; }; - S t=make(a); - return a.x*10000+a.y*100+t.x+t.y; - """; - - @Test public void testInlineAllocationMemory() { - var code = new CodeGen(INLINE_MEMORY).driver(CodeGen.Phase.FinalizeIR); - assertNull(code._stop.walk(n -> n instanceof CallNode ? n : null)); - assertEquals("400944",Eval2.eval(code,0)); - assertEquals("420946",Eval2.eval(code,2)); - } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter19Test.java b/src/test/java/com/seaofnodes/simple/Chapter19Test.java index 337dbc62..6f290279 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter19Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter19Test.java @@ -6,8 +6,6 @@ import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.codegen.CodeGen.Phase; import com.seaofnodes.simple.codegen.CodeGen; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.*; @@ -15,14 +13,14 @@ import static org.junit.Assert.*; public class Chapter19Test { @Test public void testPrintingFunctionLookup() { var code = new CodeGen("return 0;").parse(); - var tfp = TypeFunPtr.make((byte)2,false,new Type[]{TypeInteger.constant(987654)},TypeInteger.constant(123456),XInt.make(1000)); - var con = new ConstantNode(tfp); + var tfp = TypeFunPtr.make1((byte)2,false,new Type[]{TypeInteger.constant(987654)},TypeInteger.constant(123456),30); + var con = ConstantNode.raw(tfp); var call = new CallNode(null,code._start,con,con); // Diagnostic lookup may construct/intern the return-erased linker key. con.toString(); org.junit.Assert.assertNull(call.name()); - var fun = new FunNode(null,tfp,"printerTarget"); + var fun = new FunNode(null,tfp,"printerTarget",null); fun._name = "printerTarget"; fun.addDef(code._start); code.link(fun); @@ -40,18 +38,19 @@ public class Chapter19Test { return 0; """); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("return 0;", code._stop.toString()); + assertEquals("return 0;", code.print()); assertEquals("0", Eval2.eval(code, 2)); } @Test - public void testString() throws IOException { + public void testString() { String src = """ struct String { - u8[] !cs; + u8[~] !cs; int _hashCode; + new String = { u8[~] s -> cs=s; }; }; // Compare two Strings @@ -64,12 +63,6 @@ val equals = { !String !self, !String s -> return true; }; -// Return the String hashCode (cached, and never 0) -val hashCode = { !String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); -}; val _hashCodeString = { !String self -> int hash=0; @@ -78,107 +71,141 @@ val _hashCodeString = { !String self -> if( !hash ) hash = 123456789; return hash; }; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +hashCode(new String("Hello, World!")); """; - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.LocalSched); - assertEquals("Stop[ return Phi(Region,1,0,0,1); return Phi(Region,(!=0)._hashCode,Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash))))); ]", code._stop.toString()); - //assertEquals("-4898613127354160978", Eval2.eval(code, 2)); + CodeGen code = new CodeGen(src).driver(Phase.LocalSched); + assertEquals("Stop[ return Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash)))); return MEM[ 2:___ 3:___ 4:___ 5:.cs=Parm_s(Test.String.String,*[]u8[final]); 6:._hashCode=0;]; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:._hashCode=0;]; 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)); + } + + @Test + public void testTernaryStoreCall() { + String src = +""" +struct String { + u8[~] !cs; + int _hashCode; + new String = { u8[~] s -> cs=s; }; +}; + +val f = { !String self -> 7; }; + +val hashCode = { !String self -> + self._hashCode + ? 3 + : (self._hashCode = f(self)); +}; + +hashCode(new String("Hello")); +"""; + CodeGen code = new CodeGen(src).driver(Phase.LocalSched); + assertFalse(code.print().contains("return Top")); + assertEquals("7", Eval2.eval(code, 2)); } @Test public void testBasic0() { CodeGen code = new CodeGen("return 0;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return 0;", code._stop.toString()); + assertEquals("return 0;", code.print()); } @Test public void testBasic1() { CodeGen code = new CodeGen("return arg+1;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (inc,arg);", code._stop.toString()); + assertEquals("return (inc,arg);", code.print()); } @Test public void testBasic2() { CodeGen code = new CodeGen("return -17;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return -17;", code._stop.toString()); + assertEquals("return -17;", code.print()); } @Test public void testBasic3() { CodeGen code = new CodeGen("return arg==1;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (set==,(cmp,arg));", code._stop.toString()); + assertEquals("return (set==,(cmp,arg));", code.print()); } @Test public void testBasic4() { CodeGen code = new CodeGen("return arg<<1;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (shli,arg);", code._stop.toString()); + assertEquals("return (shli,arg);", code.print()); } @Test public void testBasic5() { CodeGen code = new CodeGen("return arg >> 1;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (sari,arg);", code._stop.toString()); + assertEquals("return (sari,arg);", code.print()); } @Test public void testBasic6() { CodeGen code = new CodeGen("return arg >>> 1;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (shri,arg);", code._stop.toString()); + assertEquals("return (shri,arg);", code.print()); } @Test public void testBasic7() { CodeGen code = new CodeGen("return arg / 2;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (div,arg,2);", code._stop.toString()); + assertEquals("return (div,arg,2);", code.print()); } @Test public void testBasic8() { CodeGen code = new CodeGen("return arg * 6;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (muli,arg);", code._stop.toString()); + assertEquals("return (muli,arg);", code.print()); } @Test public void testBasic9() { CodeGen code = new CodeGen("return arg & 2;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (andi,arg);", code._stop.toString()); + assertEquals("return (andi,arg);", code.print()); } @Test public void testBasic10() { CodeGen code = new CodeGen("return arg | 2;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (ori,arg);", code._stop.toString()); + assertEquals("return (ori,arg);", code.print()); } @Test public void testBasic11() { CodeGen code = new CodeGen("return arg ^ 2;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (xori,arg);", code._stop.toString()); + assertEquals("return (xori,arg);", code.print()); } @Test public void testBasic12() { CodeGen code = new CodeGen("return arg + 2.0;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (addf,(cvtf,arg),2.0f);", code._stop.toString()); + assertEquals("return (addf,(cvtf,arg),2.0f);", code.print()); } @Test public void testBasic13() { CodeGen code = new CodeGen("return arg - 2.0;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (subf,(cvtf,arg),2.0f);", code._stop.toString()); + assertEquals("return (subf,(cvtf,arg),2.0f);", code.print()); } @Test public void testBasic14() { CodeGen code = new CodeGen("return arg * 2.0;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (mulf,(cvtf,arg),2.0f);", code._stop.toString()); + assertEquals("return (mulf,(cvtf,arg),2.0f);", code.print()); } @Test public void testBasic15() { CodeGen code = new CodeGen("return arg / 2.0;").driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (mulf,(cvtf,arg),0.5f);", code._stop.toString()); + assertEquals("return (mulf,(cvtf,arg),0.5f);", code.print()); } @Test @@ -188,7 +215,7 @@ val _hashCodeString = { !String self -> int arg1 = arg + 1; return arg1 / arg;"""); code.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (div,(inc,arg),arg);", code._stop.toString()); + assertEquals("return (div,(inc,arg),arg);", code.print()); } @Test @@ -199,7 +226,7 @@ int arg1 = arg + 1; return arg1 * arg; """); code.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (mul,(inc,arg),arg);", code._stop.toString()); + assertEquals("return (mul,(inc,arg),arg);", code.print()); } @Test @@ -209,7 +236,7 @@ int a = arg; return a + 2.0; """ ).driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return (addf,(cvtf,arg),2.0f);", code._stop.toString()); + assertEquals("return (addf,(cvtf,arg),2.0f);", code.print()); } @Test @@ -258,10 +285,10 @@ return sum;"""); public void testAlloc1() { CodeGen code = new CodeGen( """ -struct S { int a; !S? !c; }; -return new S;"""); +struct _S { int a; !_S? !c; }; +return new _S;"""); code.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return S;", code.print()); + assertEquals("return Test._S;", code.print()); } @Test @@ -349,7 +376,7 @@ return A[1]; public void testNewton() throws IOException { String src = """ -val test_sqrt = { flt x -> +val _test_sqrt = { flt x -> flt epsilon = 1e-15; flt guess = x; while( 1 ) { @@ -359,7 +386,8 @@ val test_sqrt = { flt x -> guess = next; } }; -flt farg = arg; return test_sqrt(farg); +flt farg = arg; +return _test_sqrt(farg); """; CodeGen code = new CodeGen(src).driver(Phase.LocalSched,"x86_64_v2", "SystemV"); assertEquals("return Phi(Loop,(cvtf,arg),(mulf,(addf,Phi_guess,(divf,cvtf,Phi_guess)),0.5f));", code.print()); @@ -367,7 +395,7 @@ flt farg = arg; return test_sqrt(farg); @Test - public void sieveOfEratosthenes() throws IOException { + public void sieveOfEratosthenes() { String src = """ val sieve = { int N -> @@ -402,8 +430,8 @@ val sieve = { int N -> }; """; CodeGen code = new CodeGen(src).driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("return []u32;", code.print()); - //assertEquals("u32[ 2,3,5,7,11,13,17,19]",Eval2.eval(code, 20)); + assertEquals("Stop[ return { sieve}; return []u32; ]", code.print()); + //assertEquals("{ ptr i64 -> *[]u32 #[ 3]}",Eval2.eval(code, 20)); } @@ -411,96 +439,21 @@ val sieve = { int N -> public void testFcn1() { CodeGen code = new CodeGen( """ -val fcn = arg ? { int x -> x*x; } : { int x -> x+x; }; -return fcn(2)*10 + fcn(3); +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($fun35,[2-3]),x); return (shli,Parm_x($fun36,[2-3])); ]", code.print()); + assertEquals("Stop[ return (add,#2,(muli,#2)); return (mul,Parm_x($fun3,[2-3]),x); return (shli,Parm_x($fun4,[2-3])); ]", code.print()); } @Test public void testFcn2() { CodeGen code = new CodeGen( """ -val sq = { int x -> x*x; }; -return sq(arg) + sq(3); +val _sq = { int x -> x*x; }; +return _sq(arg) + _sq(3); """); code.driver(Phase.LocalSched,"x86_64_v2", "SystemV"); - assertEquals("Stop[ return (add,#2,#2); return (mul,Parm_x(sq,i64),x); ]", code.print()); - } - - @Test public void testSelectedMemory() { - for( String cpu : new String[]{"x86_64_v2","riscv","arm"} ) - for( String src : new String[]{Chapter10Test.NESTED_MEMORY,Chapter16Test.CONSTRUCTOR_MEMORY, - Chapter18Test.CALL_MEMORY,Chapter18Test.RECURSIVE_MEMORY} ) { - var code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - var aliases = memoryPhis(code._stop); - code.loopTree().driver(CodeGen.Phase.Select,cpu,"SystemV"); - assertEquals(aliases,memoryPhis(code._stop)); - code.driver(CodeGen.Phase.Schedule).localSched(); - code._stop.walk(n -> { - if( !(n instanceof CFGNode) && !(n instanceof ProjNode) ) - assertTrue(n.in(0) instanceof CFGNode); - if( n instanceof NewNode nn ) { - assertEquals(2,((TypeTuple)nn._type)._types.length); - assertNull(nn.mem()); // The allocation starts with private memory. - nn.cacheRegs(code); - MachNode mach = (MachNode)nn; - assertNull(mach.regmap(1)); - assertNotNull(mach.regmap(2)); - assertNotNull(mach.outregmap(0)); - assertNull(mach.outregmap(1)); - } - if( n instanceof CallNode call ) { - assertEquals(4,call.nIns()); // ctrl, memory, two args; direct target is embedded. - } - return null; - }); - } - } - - private static java.util.ArrayList<String> memoryPhis(Node stop) { - var phis = new java.util.ArrayList<String>(); - // Count only definitions reachable from Stop, as instruction selection does. - // A dead Phi cycle can still appear through the reverse use edges. - var seen = new java.util.BitSet(); - var work = new java.util.ArrayList<Node>(); - work.add(stop); - for( int i=0; i<work.size(); i++ ) { - Node n = work.get(i); - if( n==null || seen.get(n._nid) ) continue; - seen.set(n._nid); - for( Node def : n._inputs ) work.add(def); - if( n instanceof MemPhiNode phi ) phis.add("alias:"+phi._alias); - if( n instanceof BulkMemPhiNode phi ) phis.add("bulk:"+phi._aliases); - assertFalse(n instanceof PhiNode && n.isMem() && - !(n instanceof MemPhiNode) && !(n instanceof BulkMemPhiNode) && !(n instanceof ParmNode)); - } - java.util.Collections.sort(phis); - return phis; - } - - @Test public void testFoldedReadBeforeWrites() { - var code = new CodeGen(""" - struct S { int x; }; - !S !a = new S { x=11; }; !S !b = new S { x=22; }; - !S !p = a; if( arg ) p = b; - int before = p.x + arg; - a.x=33; b.x=44; - return before; - """).driver(CodeGen.Phase.LocalSched,"x86_64_v2","SystemV"); - var read = (MemOpNode)code._stop.walk(n -> - n instanceof com.seaofnodes.simple.node.cpus.x86_64_v2.AddMemX86 ? n : null); - assertNotNull(read); - int writes = 0; - for( Node use : read.antiDeps() ) - if( use instanceof MemOpNode st && st.isMem() ) { - assertSame(read.cfg0(),st.cfg0()); - assertTrue(st._inputs.find(read)>=0); - assertTrue(read.cfg0()._outputs.find(read)<st.cfg0()._outputs.find(st)); - assertNull(((MachNode)st).regmap(st._inputs.find(read))); - writes++; - } - assertEquals(1,writes); // The next Store follows this first clobber through memory. + assertEquals("return (addi,(mul,arg,arg));", code.print()); } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/src/test/java/com/seaofnodes/simple/Chapter20Test.java index c5e4c583..cdfd16a7 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter20Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter20Test.java @@ -4,17 +4,11 @@ import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.print.ASMPrinter; -import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.*; public class Chapter20Test { - @Test public void testFlagSpills() throws Exception { - com.seaofnodes.simple.codegen.FlagSpillTestSupport.run(); - } - @Test public void testAllocatorTopPhi() { com.seaofnodes.simple.codegen.RegAllocTestSupport.topPhi(); } @Test public void testNeverReturnAllocation() { @@ -56,11 +50,11 @@ public class Chapter20Test { int stores=0; for( var bb : code._cfg ) for( var node : bb.outs() ) - if( node instanceof com.seaofnodes.simple.node.cpus.x86_64_v2.StoreX86 st ) { + if( node instanceof com.seaofnodes.simple.node.cpus.x86_64_v2.StoreX86 st && st._name.equals("x") ) { stores++; assertEquals(-1,st.regmap(4).nextReg((short)15)); } - assertEquals(1,stores); + assertTrue(stores>0); } } @@ -75,14 +69,12 @@ return f(s); // The entry of g can disappear before its Return is folded into f. // Exercise both worklist orders; deleting the entry does not kill 123. for( int seed=0; seed<64; seed++ ) { - CodeGen code = new CodeGen(src,com.seaofnodes.simple.type.TypeInteger.BOT,seed,true).driver(CodeGen.Phase.TypeCheck); + CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck); assertEquals("seed "+seed, "123", Eval2.eval(code,0)); } } - @org.junit.Rule public final org.junit.rules.ErrorCollector _errors = new org.junit.rules.ErrorCollector(); - @Test public void testAllocatorMasks() { com.seaofnodes.simple.codegen.RegAllocTestSupport.masks(); } @Test public void testAllocatorUnion() { com.seaofnodes.simple.codegen.RegAllocTestSupport.union(); } @Test public void testAllocatorCopyClobber() throws Exception { com.seaofnodes.simple.codegen.RegAllocTestSupport.copyClobber(); } @@ -101,40 +93,38 @@ return f(s); public void testJig() { CodeGen code = new CodeGen("return 0;"); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("return 0;", code._stop.toString()); + assertEquals("return 0;", code.print()); assertEquals("0", Eval2.eval(code, 2)); } - // Collect differences so every target runs; JUnit still fails the test. - private void testTarget(String src, String cpu, String os, int spills, String stop) { - _errors.checkSucceeds(() -> { testCPU(src,cpu,os,spills,stop); return null; }); - } - - static void testCPU(String src, String cpu, String os, int spills, String stop) { + static void testCPU( String src, String cpu, String os, int spills, String stop ) { CodeGen code = new CheckedCodeGen(src); code.driver(CodeGen.Phase.RegAlloc,cpu,os); - SpillStats.record(code,"Chapter20",cpu,os); - SpillStats.checkSpills(spills,code._regAlloc._spillScaled); - if( stop!=null ) assertEquals(stop,code._stop.toString()); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); + if( stop != null ) + assertEquals(stop, code.print()); } - private void testAllCPUs( String src, int spills, String stop ) { - testTarget(src,"x86_64_v2", "SystemV",spills,stop); - testTarget(src,"riscv" , "SystemV",spills,stop); - testTarget(src,"arm" , "SystemV",spills,stop); + private static void testAllCPUs( String src, int spills, String stop ) { + testCPU(src,"x86_64_v2", "SystemV",spills,stop); + testCPU(src,"riscv" , "SystemV",spills,stop); + testCPU(src,"arm" , "SystemV",spills,stop); } @Test public void testAlloc0() { - testTarget("return new u8[arg];","x86_64_v2","SystemV",4,"return []u8;"); - testTarget("return new u8[arg];","riscv","SystemV",5,"return []u8;"); - testTarget("return new u8[arg];","arm","SystemV",5,"return []u8;"); + testCPU("return new u8[arg];","x86_64_v2", "SystemV",3,"return []u8;"); + testCPU("return new u8[arg];","riscv" , "SystemV",5,"return []u8;"); + testCPU("return new u8[arg];","arm" , "SystemV",5,"return []u8;"); } @Test public void testBasic1() { String src = "return arg | 2;"; - testTarget(src,"x86_64_v2", "SystemV",1,"return (ori,mov(arg));"); - testTarget(src,"riscv" , "SystemV",0,"return ( arg | #2 );"); - testTarget(src,"arm" , "SystemV",0,"return (ori,arg);"); + testCPU(src,"x86_64_v2", "SystemV",1,"return mov((ori,arg));"); + testCPU(src,"riscv" , "SystemV",0,"return ( arg | #2 );"); + testCPU(src,"arm" , "SystemV",0,"return (ori,arg);"); } @Test @@ -142,50 +132,76 @@ return f(s); String src = """ // Newtons approximation to the square root -val sqrt = { int x -> +val _sqrt = { int x -> int guess = x; while( 1 ) { - int next = (x/guess + guess)/2; + int next = (x/guess + guess)>>1; if( next == guess ) return guess; guess = next; } }; -return sqrt(arg) + sqrt(arg+2); +return _sqrt(arg) + _sqrt(arg+2); """; - testTarget(src,"x86_64_v2", "SystemV",40,null); - testTarget(src,"riscv" , "SystemV",17,null); - testTarget(src,"arm" , "SystemV",18,null); + testCPU(src,"x86_64_v2", "win64" ,38,null); + testCPU(src,"riscv" , "SystemV",19,null); + testCPU(src,"arm" , "SystemV",19,null); } @Test public void testNewtonFloat() { String src = """ -// Newtons approximation to the square root -val sqrt = { flt x -> +val _test_sqrt_noInline = { flt x -> + flt epsilon = 1e-15; flt guess = x; while( 1 ) { flt next = (x/guess + guess)/2; - if( next == guess ) return guess; + if( guess-epsilon <= next && next <= guess+epsilon ) + return guess; guess = next; } }; -flt farg = arg; -return sqrt(farg) + sqrt(farg+2.0); +flt farg = arg; return _test_sqrt_noInline(farg) + _test_sqrt_noInline(farg+2.0); """; - testTarget(src,"x86_64_v2", "SystemV",23,null); - testTarget(src,"riscv" , "SystemV",18,null); - testTarget(src,"arm" , "SystemV",18,null); + testCPU(src,"x86_64_v2", "SystemV",40,null); + testCPU(src,"riscv" , "SystemV",17,null); + testCPU(src,"arm" , "SystemV",18,null); + } + + @Test + public void testIterPrettyPrintIsLocalAndReadOnly() { + CodeGen code = new CodeGen(""" + val f_noInline = { flt x -> while(1) { if(x) return x; } }; + flt y=arg; + return f_noInline(y)+f_noInline(y+2.0); + """).driver(CodeGen.Phase.Iter); + assertNull(code._start._ltree); + int pre = code._start._pre; + + String p0 = code.toString(); + String p1 = code.toString(); + assertEquals(p0,p1); + assertNull(code._start._ltree); + assertEquals(pre,code._start._pre); + + int main0 = p0.indexOf("--- class:Test.<clinit>"); + int main1 = p0.indexOf("--- class:Test.<clinit> ----------------------",main0); + int fun0 = p0.indexOf("--- f_noInline ",main1); + int fun1 = p0.indexOf("--- f_noInline ----------------------",fun0); + assertTrue(main0 < main1 && main1 < fun0 && fun0 < fun1); + assertTrue(p0.indexOf("ToFloat",main0) < p0.indexOf("Call",main0)); + assertTrue(p0.indexOf("CallEnd",fun0) == -1 || p0.indexOf("CallEnd",fun0) > fun1); } @Test public void testAlloc2() { String src = "int[] !xs = new int[3]; xs[arg]=1; return xs[arg&1];"; - testTarget(src,"x86_64_v2","SystemV",3,"return .[];"); - testTarget(src,"riscv","SystemV",6,"return .[];"); - testTarget(src,"arm","SystemV",6,"return .[];"); + testCPU(src,"x86_64_v2", "SystemV",-1,"return .[];"); + testCPU(src,"riscv" , "SystemV", 6,"return .[];"); + testCPU(src,"arm" , "SystemV", 6,"return .[];"); } + @Test public void testArray1() { String src = @@ -199,20 +215,23 @@ for( int i=0; i<ary#-1; i++ ) ary[i+1] += ary[i]; return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6 """; - // Array publication changes memory scheduling and the live ranges. - testTarget(src,"x86_64_v2", "SystemV",7,"return .[];"); - testTarget(src,"riscv" , "SystemV",7,"return (add,.[],(mul,.[],1000));"); - testTarget(src,"arm" , "SystemV",5,"return (add,.[],(mul,.[],1000));"); + 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));"); } + @Test public void testString() { - String src = """ + String src = +""" struct String { u8[] !cs; int _hashCode; + new String = { u8[] data -> cs=data; }; }; +// Compare two Strings val equals = { !String !self, !String s -> if( self == s ) return true; if( self.cs# != s.cs# ) return false; @@ -222,6 +241,7 @@ val equals = { !String !self, !String s -> return true; }; +// Return the String hashCode (cached, and never 0) val hashCode = { !String self -> self._hashCode ? self._hashCode @@ -230,30 +250,23 @@ val hashCode = { !String self -> val _hashCodeString = { !String self -> int hash=0; - if( self.cs ) { - for( int i=0; i< self.cs#; i++ ) - hash = hash*31 + self.cs[i]; - } + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; if( !hash ) hash = 123456789; return hash; }; - -!String !s = new String { cs = new u8[17]; }; -s.cs[0] = 67; // C -s.cs[1] = 108; // l -hashCode(s); """; - testTarget(src,"x86_64_v2", "SystemV",9,null); - testTarget(src,"riscv" , "SystemV",1,null); - testTarget(src,"arm" , "SystemV",1,null); + testCPU(src,"x86_64_v2", "SystemV",12,null); + testCPU(src,"riscv" , "SystemV",6,null); + testCPU(src,"arm" , "SystemV",6,null); } @Test public void testCast() { String src = "struct Bar { int x; }; var b = arg ? new Bar; return b ? b.x++ + b.x++ : -1;"; - testTarget(src,"x86_64_v2","SystemV",1,null); - testTarget(src,"riscv","SystemV",2,null); - testTarget(src,"arm","SystemV",2,null); + testCPU(src,"x86_64_v2", "SystemV",3,null); + testCPU(src,"riscv" , "SystemV",6,null); + testCPU(src,"arm" , "SystemV",6,null); } @Test @@ -265,15 +278,15 @@ hashCode(s); @Test public void testFlags1() { String src = """ -bool b1 = arg == 1; +bool _b1 = arg == 1; bool b2 = arg == 2; -if (b2) if (b1) return 1; -if (b1) return 2; +if (b2) if (_b1) return 1; +if (_b1) return 2; return 0; """; - testTarget(src,"x86_64_v2", "SystemV",0,"return Phi(Region,1,2,0);"); - testTarget(src,"riscv" , "SystemV",0,"return Phi(Region,1,2,0);"); - testTarget(src,"arm" , "SystemV",0,"return Phi(Region,1,2,0);"); + testCPU(src,"x86_64_v2", "SystemV",0,"return Phi(Region,2,0,1);"); + testCPU(src,"riscv" , "SystemV",0,"return Phi(Region,2,0,1);"); + testCPU(src,"arm" , "SystemV",0,"return Phi(Region,2,0,1);"); } @Test @@ -286,146 +299,8 @@ while (arg > 0) { } return arg; """; - testTarget(src,"x86_64_v2", "SystemV",1,null); - testTarget(src,"riscv" , "SystemV",0,null); - testTarget(src,"arm" , "SystemV",0,null); - } - // Original Chapter 20 allocation workload, kept fixed for cohort comparisons. - @Test - public void testBrainfuck() { - var program = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.".getBytes(java.nio.charset.StandardCharsets.UTF_8); - var encoded = new StringBuilder("u8[] !program = new u8[").append(program.length).append("];"); - for (int i = 0; i < program.length; i++) { - int value = program[i] & 0xFF; - encoded.append("program[").append(i).append("] = ").append(value).append(";"); - } - - String src = encoded + """ - -int d = 0; -u8[] !output = new u8[0]; -u8[] !data = new u8[100]; - -for( int pc = 0; pc < program#; pc++ ) { - var command = program[pc]; - if (command == 62) { - d++; - } else if (command == 60) { - d--; - } else if (command == 43) { - data[d]++; - } else if (command == 45) { - data[d]--; - } else if (command == 46) { - // Output a byte; increase the output array size - var old = output; - output = new u8[output# + 1]; - for( int i = 0; i < old#; i++ ) - output[i] = old[i]; - output[old#] = data[d]; // Add the extra byte on the end - } else if (command == 44) { - data[d] = 42; - } else if (command == 91) { - if (data[d] == 0) { - for( int d = 1; d > 0; ) { - command = program[++pc]; - if (command == 91) d++; - if (command == 93) d--; - } - } - } else if (command == 93) { - if (data[d]) { - for( int d = 1; d > 0; ) { - command = program[--pc]; - if (command == 93) d++; - if (command == 91) d--; - } - } - } -} -return output; -"""; - testTarget(src,"x86_64_v2", "SystemV",40,null); - testTarget(src,"riscv" , "SystemV",28,null); - testTarget(src,"arm" , "SystemV",28,null); - //assertEquals("Hello World!\n", Eval2.eval(code, 0, 10000)); - } - // Original Chapter 20 allocation workload, kept fixed for cohort comparisons. - @Test - public void testMergeSort() { - String src = -""" -// based on the top-down version from https://en.wikipedia.org/wiki/Merge_sort - -val merge_sort = { int[] !a, int[] !b, int n -> - copy_array(a, 0, n, b); - split_merge(a, 0, n, b); -}; - -val split_merge = { int[] !b, int begin, int end, int[] a -> - if (end - begin <= 1) - return 0; - int middle = (end + begin) / 2; - split_merge(a, begin, middle, b); - split_merge(a, middle, end, b); - merge(b, begin, middle, end, a); - return 0; -}; - -val merge = { int[] !b, int begin, int middle, int end, int[] a -> - int i = begin, j = middle; - - for (int k = begin; k < end; k++) { - // && and || - bool cond = false; - if (i < middle) { - if (j >= end) cond = true; - else if (a[i] <= a[j]) cond = true; - } - if (cond) b[k] = a[i++]; - else b[k] = a[j++]; - } -}; - -val copy_array = { int[] !a, int begin, int end, int[] b -> - for (int k = begin; k < end; k++) - b[k] = a[k]; -}; - -int[] !a = new int[arg]; -int[] !b = new int[a#]; - -for (int i = 0; i < a#; i++) - a[i] = a# - i; - -merge_sort(a, b, a#); - -return a; -"""; - testTarget(src,"x86_64_v2", "SystemV",52,null); - testTarget(src,"riscv" , "SystemV",44,null); - testTarget(src,"arm" , "SystemV",41,null); -//assertEquals("int[ 1,2,3,4,5,6,7,8,9,10,11]", Eval2.eval(code, 11)); - } - - - @Test public void testMemoryAllocation() { - for( String cpu : new String[]{"x86_64_v2","riscv","arm"} ) - for( String src : new String[]{Chapter10Test.NESTED_MEMORY,Chapter16Test.CONSTRUCTOR_MEMORY, - Chapter18Test.CALL_MEMORY,Chapter18Test.RECURSIVE_MEMORY, - "int[] !a = new int[3]; a[arg]=arg; return a[0]+1;"} ) { - var code = new CodeGen(src).driver(CodeGen.Phase.RegAlloc,cpu,"SystemV"); - com.seaofnodes.simple.codegen.RegAllocTestSupport.checkRegisters(code); - code._stop.walk(n -> { - // Whole memory, precise Phis, and memory projections need no register. - if( n._type instanceof com.seaofnodes.simple.type.TypeMem ) - assertEquals(-1,code._regAlloc.regnum(n)); - if( n instanceof com.seaofnodes.simple.node.NewNode nn ) { - assertTrue(code._regAlloc.regnum(nn.proj(0))>=0); - assertEquals(-1,code._regAlloc.regnum(nn.proj(1))); - } - return null; - }); - } + testCPU(src,"x86_64_v2", "SystemV",3,null); + testCPU(src,"riscv" , "SystemV",2,null); + testCPU(src,"arm" , "SystemV",2,null); } } diff --git a/src/test/java/com/seaofnodes/simple/Chapter21AllocTest.java b/src/test/java/com/seaofnodes/simple/Chapter21AllocTest.java deleted file mode 100644 index bab7a86a..00000000 --- a/src/test/java/com/seaofnodes/simple/Chapter21AllocTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.seaofnodes.simple; - -import java.io.IOException; -import java.nio.file.*; -import org.junit.Test; - -// Revised Chapter 20 examples introduced with native encoding. -public class Chapter21AllocTest { - @Test - public void testNewtonInteger() { - String src = -""" -// Newtons approximation to the square root -val sqrt = { int x -> - int guess = x; - while( 1 ) { - int next = (x/guess + guess)/2; - if( next == guess ) return guess; - guess = next; - } -}; -return sqrt(arg) + sqrt(arg+2); -"""; - Chapter21Test.testCPU(src,"x86_64_v2", "Win64" ,40,null); - Chapter21Test.testCPU(src,"riscv" , "SystemV",19,null); - Chapter21Test.testCPU(src,"arm" , "SystemV",19,null); - } - - @Test - public void testNewtonFloat() throws IOException { - String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/newtonFloat.smp")) - + "flt farg = arg; return test_sqrt(farg) + test_sqrt(farg+2.0);"; - Chapter21Test.testCPU(src,"x86_64_v2", "SystemV",39,null); - Chapter21Test.testCPU(src,"riscv" , "SystemV",17,null); - Chapter21Test.testCPU(src,"arm" , "SystemV",18,null); - } - - @Test - public void testArray1() throws IOException { - String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/array1.smp")); - Chapter21Test.testCPU(src,"x86_64_v2", "SystemV",-1,"return .[];"); - Chapter21Test.testCPU(src,"riscv" , "SystemV", 8,"return (add,.[],(mul,.[],1000));"); - Chapter21Test.testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));"); - } - - - @Test - public void testString() throws IOException { - String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/stringHash21.smp")); - Chapter21Test.testCPU(src,"x86_64_v2", "SystemV", 9,null); - Chapter21Test.testCPU(src,"riscv" , "SystemV", 3,null); - Chapter21Test.testCPU(src,"arm" , "SystemV", 1,null); - } - -} diff --git a/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/src/test/java/com/seaofnodes/simple/Chapter21Test.java index 8442bbc8..2fcf65e9 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -10,14 +10,33 @@ import com.seaofnodes.simple.node.cpus.arm.arm; import com.seaofnodes.simple.node.cpus.riscv.riscv; import com.seaofnodes.simple.util.SB; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import org.junit.Ignore; import org.junit.Test; 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; };"+ + "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, + 0x1p63,Math.nextDown(0x1p63),-0x1p63,Math.nextDown(-0x1p63), + Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY,Double.NaN} ) { + for( String name : new String[]{"float_to_int","float_to_u8"} ) { + long expected = name.equals("float_to_u8") ? ((long)x)&255 : (long)x; + EvalRisc5 r = TestRisc5.build(src,name,0,-1,false); + r.fregs[riscv.FA0-riscv.F_OFFSET]=x; + assertEquals(0,r.step(1000)); + assertEquals(name+"("+x+")",expected,r.regs[riscv.A0]); + EvalArm64 a = TestArm64.build(name,src,0,-1,false); + a.fregs[arm.D0-arm.D_OFFSET]=x; + assertEquals(0,a.step(1000)); + assertEquals(name+"("+x+")",expected,a.regs[arm.X0]); + } + } + } + @Test public void testReadBeforeConditionalStore() { var code = new CodeGen(""" struct S { int x; }; !S !a=new S; !S !b=new S; @@ -52,35 +71,10 @@ public class Chapter21Test { } - @Test public void testReturnedFunctionEncoding() throws IOException { - String src = "if(arg) return {->42;}; return {->43;};"; - CodeGen code = new CodeGen(src).driver("riscv","SystemV",null); - for( int arg : new int[]{0,1} ) { - byte[] image = new byte[1<<16]; - byte[] bits = code._encoding.bits(); - System.arraycopy(bits,0,image,0,bits.length); - EvalRisc5 cpu = new EvalRisc5(image,1<<15); - cpu._pc = code._encoding._opStart[code.link(code._main)._nid]; - cpu.regs[riscv.A0] = arg; - assertEquals(0,cpu.step(100)); - assertEquals(0,cpu._pc); - cpu._pc = (int)cpu.regs[riscv.A0]; - assertEquals(0,cpu.step(100)); - assertEquals(0,cpu._pc); - assertEquals(arg==0 ? 43 : 42,cpu.regs[riscv.A0]); - } - // Anonymous bodies need code, but no exported ELF name. - new CodeGen(src).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,"build/objs/anonymous.o"); - } - @Test public void testArmFloatMemory() throws IOException { for( String type : new String[]{"f32","f64"} ) { String src = type+"[] !a=new "+type+"[3]; a[arg]=1.25; return a[1];"; - CodeGen code = new CodeGen(src).driver("arm","SystemV",null); - byte[] image = new byte[1<<20]; - byte[] bits = code._encoding.bits(); - System.arraycopy(bits,0,image,0,bits.length); - EvalArm64 cpu = new EvalArm64(image,1<<16); + EvalArm64 cpu = TestArm64.build("probe","val probe={ int arg -> "+src+" };",1,-1,false); cpu.regs[arm.X0]=1; assertEquals(0,cpu.step(1000)); assertEquals(1.25,cpu.fregs[0],0); @@ -156,6 +150,7 @@ public class Chapter21Test { struct _Vec { u32 !len; int[] !buf; + new _Vec = { int[] initial -> buf=initial; }; val grow = { int sz -> var buf2 = new int[sz]; for( int i=0; i<len; i++ ) buf2[i]=buf[i]; @@ -168,7 +163,7 @@ public class Chapter21Test { }; }; val run = { int arg -> - val v = new _Vec{buf=new int[4];}.add(2).add(3).add(5).add(7).add(11); + val v = new _Vec(new int[4]).add(2).add(3).add(5).add(7).add(11); return v.len*100+v.buf[arg]; }; """; @@ -176,7 +171,7 @@ public class Chapter21Test { } private static int checkArmCalls(String src, String entryName, int arg, int result) throws IOException { - CodeGen code = new CodeGen(src).driver("arm","SystemV",null); + CodeGen code = new CodeGen(src).driver("arm","SystemV",true,false); byte[] image = new byte[1<<20]; System.arraycopy(code._encoding.bits(),0,image,0,code._encoding.bits().length); EvalArm64 cpu = new EvalArm64(image,1<<16); @@ -184,7 +179,7 @@ public class Chapter21Test { int frames=0, calls=0; for( var bb : code._cfg ) { if( bb instanceof com.seaofnodes.simple.node.FunNode fun ) { - int off = code._encoding._opStart[fun._nid]; + int off = code._encoding.opStart(fun); if( entryName.equals(fun._name) ) { cpu._pc=off; entry=true; } int frame=fun._frameAdjust; assertEquals(0,frame&15); @@ -194,7 +189,7 @@ public class Chapter21Test { } } if( bb instanceof com.seaofnodes.simple.node.ReturnNode ret ) { - int off=code._encoding._opStart[ret._nid]; + int off=code._encoding.opStart(ret); int frame=ret.fun()._frameAdjust; if( frame>0 ) { assertEquals(0x910003FF | (frame<<10),cpu.ld4s(off)); // ADD SP,SP,#bytes @@ -220,52 +215,6 @@ public class Chapter21Test { @Test public void testCoalescing() { com.seaofnodes.simple.codegen.RegAllocTestSupport.coalescing(); } @Test public void testCopyForward() throws Exception { com.seaofnodes.simple.codegen.RegAllocTestSupport.copyForward(); } - @Test public void testNarrowStores() throws IOException { - for( String type : new String[]{"i8","u8","i16","u16"} ) { - String src = "struct S { "+type+" x; }; !S !s = new S; s.x = arg; return 0;"; - CodeGen code = new CodeGen(src).driver("riscv","SystemV",null); - int stores=0; - for( var bb : code._cfg ) - for( var node : bb.outs() ) - if( node instanceof com.seaofnodes.simple.node.cpus.riscv.StoreRISC st ) { - stores++; - assertEquals("Byte/short stores have no FP encoding",-1,st.regmap(4).nextReg((short)31)); - } - assertEquals(1,stores); - byte[] image = new byte[1<<20]; - byte[] bits = code._encoding.bits(); - System.arraycopy(bits,0,image,0,bits.length); - EvalRisc5 cpu = new EvalRisc5(image,1<<16); - cpu.regs[riscv.A0] = 0x8765; - assertEquals(0,cpu.step(100)); - assertEquals(type.endsWith("8") ? 0x65 : 0x8765, - type.endsWith("8") ? cpu.ld1z(1<<16) : cpu.ld2z(1<<16)); - code = new CodeGen(src).driver(CodeGen.Phase.Encoding,"x86_64_v2",TestC.CALL_CONVENTION); - stores=0; - for( var bb : code._cfg ) - for( var node : bb.outs() ) - if( node instanceof com.seaofnodes.simple.node.cpus.x86_64_v2.StoreX86 st ) { - stores++; - assertEquals("Byte/short stores have no XMM encoding",-1,st.regmap(4).nextReg((short)15)); - } - assertEquals(1,stores); - } - } - - @Test public void testNativeExitStatus() throws IOException { - Path source = Path.of("build/objs/nativeExit.c"); - Files.createDirectories(source.getParent()); - Files.writeString(source,"int main() { return 7; }\n"); - try { - TestC.gcc(source.toString(),null,null,false, - "build/objs/nativeExit"+(TestC.OS.startsWith("Windows") ? ".exe" : "")); - } catch( AssertionError error ) { - assertTrue(error.getMessage(),error.getMessage().contains("Program exit status")); - return; - } - fail("A failing native program must fail the test, even with empty stdout"); - } - @Test public void testArmSubtractRegisters() { EvalArm64 cpu = new EvalArm64(new byte[16],16); cpu.st4(0,0xCB020020); // SUB X0,X1,X2, no shift or flag update. @@ -309,40 +258,60 @@ public class Chapter21Test { } - // Frozen Jig runs in Chapter23AllocTest; the revised input is in Chapter24AllocTest. - @Test @Ignore + @Ignore + @Test public void testJig() throws IOException { - String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/jig.smp")); - testCPU(src,"x86_64_v2", "Win64" ,-1,null); + String src = +""" +struct _s0 { + bool v1; + i16 v2; + int v3; + i8 v4; + byte v5; +}; +while(new _s0.v3) + while(new _s0.v5<<new _s0.v4) {} +if(0) { + if(0) { + flt !P5ZUD4=new _s0.v2; + } + while(0) {} +} +return new _s0.v1; +"""; + testCPU(src,"x86_64_v2", "win64" ,-1,null); testCPU(src,"riscv" , "SystemV",-1,null); testCPU(src,"arm" , "SystemV",-1,null); } static void testCPU( String src, String cpu, String os, int spills, String stop ) { CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); - SpillStats.record(code,"Chapter21",cpu,os); - SpillStats.checkSpills(spills,code._regAlloc._spillScaled); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); if( stop != null ) - assertEquals(stop, code._stop.toString()); + assertEquals(stop, code.print()); } @Test public void testBasic1() { String src = "return arg | 2;"; - testCPU(src,"x86_64_v2", "SystemV",1,"return (ori,mov(arg));"); + testCPU(src,"x86_64_v2", "SystemV",1,"return mov((ori,arg));"); testCPU(src,"riscv" , "SystemV",0,"return ( arg | #2 );"); testCPU(src,"arm" , "SystemV",0,"return (ori,arg);"); } @Test public void testInfinite() { - String src = "struct S { int i; }; !S !s = new S; while(1) s.i++;"; + String src = "struct _S { int i; }; !_S !s = new _S; while(1) s.i++; return s.i;"; testCPU(src,"x86_64_v2", "SystemV",0,"return Top;"); testCPU(src,"riscv" , "SystemV",0,"return Top;"); testCPU(src,"arm" , "SystemV",0,"return Top;"); } @Test - public void testArray1() throws IOException { + public void testArray1() { String src = """ int[] !ary = new int[arg]; @@ -354,19 +323,19 @@ 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 .[];"); + 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));"); } @Test - public void testAntiDeps1() throws IOException { + public void testAntiDeps1() { String src = """ -struct S { int f; }; -var v0 = new S; -!S? !v1; -if (arg) v1 = new S; +struct _S { int f; }; +var v0 = new _S; +!_S? !v1; +if (arg) v1 = new _S; if (v1) { v0.f = v1.f; } else { @@ -374,18 +343,19 @@ if (v1) { } return v0; """; - testCPU(src,"x86_64_v2", "SystemV", 7,"return mov(mov(S));"); - testCPU(src,"riscv" , "SystemV",10,"return mov(mov(S));"); - testCPU(src,"arm" , "SystemV",10,"return mov(mov(S));"); + testCPU(src,"x86_64_v2", "SystemV", 9,"return mov(mov(Test._S));"); + testCPU(src,"riscv" , "SystemV",10,"return mov(mov(Test._S));"); + testCPU(src,"arm" , "SystemV",11,"return mov(mov(Test._S));"); } @Test - public void testString() throws IOException { + public void testString() { String src = """ struct String { u8[] !cs; int _hashCode; + new String = { u8[] data -> cs=data; }; }; // Compare two Strings @@ -413,9 +383,9 @@ val _hashCodeString = { !String self -> return hash; }; """; - testCPU(src,"x86_64_v2", "SystemV", 9,null); - testCPU(src,"riscv" , "SystemV", 3,null); - testCPU(src,"arm" , "SystemV", 1,null); + testCPU(src,"x86_64_v2", "SystemV",12,null); + testCPU(src,"riscv" , "SystemV",6,null); + testCPU(src,"arm" , "SystemV",6,null); } @Test public void testStringExport() throws IOException { @@ -424,6 +394,7 @@ val _hashCodeString = { !String self -> struct String { u8[] !cs; int _hashCode; + new String = { u8[] data -> cs=data; }; }; // Compare two Strings @@ -451,10 +422,10 @@ val _hashCodeString = { !String self -> return hash; }; """; - TestC.run(src, "stringHash",null, "", 9); + TestC.runC(src, "stringHash", "",12); } - @Test public void testLoop2() throws IOException { + @Test public void testLoop2() { String src = """ int i = 0; @@ -467,17 +438,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,"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)));"); - // Guards now survive into selection. They must preserve the loop's - // value through both continues and the final break without code. - EvalRisc5 r5 = TestRisc5.build("guardLoop",src,0,0,false); - assertEquals(0,r5.step(1000)); - assertEquals(2,r5.regs[riscv.A0]); - EvalArm64 a5 = TestArm64.build("guardLoop",src,0,0,false); - assertEquals(0,a5.step(1000)); - assertEquals(2,a5.regs[0]); } @Test public void testNewtonExport() throws IOException { @@ -506,9 +469,9 @@ val test_sqrt = { flt x -> } }; """; - TestC.run(src, "newtonFloat", null, result, TestC.CALL_CONVENTION.equals("Win64") ? 42 : 34); + TestC.runC(src, "newtonFloat", result, TestC.CALL_CONVENTION.equals("win64") ? 42 : 34); - EvalRisc5 R5 = TestRisc5.build("newtonFloat",src, 0, 10, false); + EvalRisc5 R5 = TestRisc5.build( src, "test_sqrt", 0, 10, false); R5.fregs[riscv.FA0 - riscv.F_OFFSET] = 3.0; int trap_r5 = R5.step(1000); assertEquals(0,trap_r5); @@ -516,7 +479,7 @@ val test_sqrt = { flt x -> assertEquals(1.732051,R5.fregs[riscv.FA0 - riscv.F_OFFSET], 0.00001); // arm - EvalArm64 A5 = TestArm64.build("newtonFloat", src,0, 10, false); + EvalArm64 A5 = TestArm64.build("test_sqrt", src,0, 10, false); A5.fregs[arm.D0 - arm.D_OFFSET] = 3.0; int trap_arm = A5.step(1000); assertEquals(0,trap_arm); @@ -566,11 +529,11 @@ 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.run(src, "sieve", null, sprimes, 186); + TestC.runC(src, "sieve", sprimes, 186); // Evaluate on RISC5 emulator; expect return of an array of primes in // the simulated heap. - EvalRisc5 R5 = TestRisc5.build("sieve", src, 100, 92, false); + EvalRisc5 R5 = TestRisc5.build( src, "sieve", 100, 90, false); int trap = R5.step(10000); assertEquals(0,trap); // Return register A0 holds sieve(100) @@ -582,7 +545,7 @@ val sieve = { int N -> // Evaluate on ARM5 emulator; expect return of an array of primes in // the simulated heap. - EvalArm64 A5 = TestArm64.build("sieve", src, 100, 94, false); + EvalArm64 A5 = TestArm64.build("sieve", src, 100, 88, false); int trap_arm = A5.step(10000); assertEquals(0, trap_arm); int ary_arm = (int)A5.regs[arm.X0]; @@ -595,7 +558,7 @@ val sieve = { int N -> @Test public void testFibExport() throws IOException { String src = """ -val fib = {int n -> +val fib = { int n -> int f1=1; int f2=1; while( n-- > 1 ){ @@ -607,9 +570,9 @@ val fib = {int n -> }; """; String fib = "[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]"; - TestC.run(src, "fib", null, fib, 24); + TestC.runC(src, "fib", fib, 24); - EvalRisc5 R5 = TestRisc5.build("fib", src, 9, 17, false); + EvalRisc5 R5 = TestRisc5.build( src, "fib", 9, 17, false); int trap = R5.step(100); assertEquals(0,trap); // Return register A0 holds fib(8)==55 @@ -624,8 +587,19 @@ val fib = {int n -> } @Test public void testPerson() throws IOException { + String src = + """ + struct Person { + i32 age; + }; + + val fcn = { !Person?[] !ps, int x -> + if( ps[x] ) + ps[x].age++; + }; + """; String person = "6\n"; - TestC.run("person21", person, 0); + TestC.run(src, "person1", null, TestC.CALL_CONVENTION, "", TestC.C_DRIVERS_DIR+"person.c", person, 2); // Memory layout starting at PS: int ps = 1<<16; // Person array pointer starts at heap start @@ -636,7 +610,7 @@ val fib = {int n -> int p1 = ps+4*8+1*8; // P2 = { age } // sizeof=8 int p2 = ps+4*8+2*8; - EvalRisc5 R5 = TestRisc5.build("person21", ps, 0, false); + EvalRisc5 R5 = TestRisc5.build( src, "fcn", ps, 2, false); R5.regs[riscv.A1] = 1; // Index 1 R5.st8(ps,3); // Length R5.st8(ps+1*8,p0); @@ -652,7 +626,7 @@ val fib = {int n -> assertEquals(17+1,R5.ld8(p1)); assertEquals(60+0,R5.ld8(p2)); - EvalArm64 A5 = TestArm64.build("person21", ps, 0, false); + EvalArm64 A5 = TestArm64.build("fcn", src, ps, 2, false); A5.regs[arm.X1] = 1; // Index 1 A5.st8(ps, 3); A5.st8(ps+1*8,p0); @@ -670,14 +644,21 @@ val fib = {int n -> } @Test public void testArgCount() throws IOException { - // Test passes more args than registers in Sys5, which is far far more - // than what Win64 allows - so Win64 gets a lot more spills here. + // Test passes more args than registers in Sys5, which is far, far more + // than what win64 allows - so win64 gets a lot more spills here. + String src = +""" +val addAll = { int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, flt f7, int x8, flt f9, int i10, flt f11, int i12, flt f13, int i14, flt f15, int x16, flt f17 int x18, flt f19 -> + return + i0 + f1+ i2+ f3+ i4+ f5+ i6+ f7+ x8 +f9 + + i10+f11+i12+f13+i14+f15+x16+f17+x18+f19 ; +}; +"""; String arg_count = "191.000000\n"; - TestC.run("arg_count", arg_count, - TestC.CALL_CONVENTION.equals("Win64") ? 32 : 9); + TestC.runC(src, "arg_count", arg_count, TestC.CALL_CONVENTION.equals("win64") ? 35 : 9); - EvalRisc5 R5 = TestRisc5.build("no_stack_arg_count", 0, 0, false); + EvalRisc5 R5 = TestRisc5.build( src, "addAll", 0, 4, false); // Todo: handle stack(imaginary stack in emulator) // pass in float arguments @@ -690,14 +671,13 @@ val fib = {int n -> R5.fregs[riscv.FA6 - riscv.F_OFFSET] = 1.1; R5.fregs[riscv.FA7 - riscv.F_OFFSET] = 1.1; - // a0 is passed in arg + R5.regs[riscv.A0] = 2; R5.regs[riscv.A1] = 2; R5.regs[riscv.A2] = 2; R5.regs[riscv.A3] = 2; R5.regs[riscv.A4] = 2; R5.regs[riscv.A5] = 2; R5.regs[riscv.A6] = 2; - R5.regs[riscv.A7] = 2; int trap = R5.step(100); assertEquals(0,trap); @@ -707,7 +687,7 @@ val fib = {int n -> assertEquals(22.8, result, 0.00001); // arm - EvalArm64 A5 = TestArm64.build("no_stack_arg_count", 0, 0, false); + EvalArm64 A5 = TestArm64.build("addAll", src, 0, 4, false); A5.fregs[arm.D0 - arm.D_OFFSET] = 1.1; A5.fregs[arm.D1 - arm.D_OFFSET] = 1.1; diff --git a/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/src/test/java/com/seaofnodes/simple/Chapter22Test.java index 3ff8dc09..00d77f65 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter22Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter22Test.java @@ -5,13 +5,11 @@ import com.seaofnodes.isa.eval.EvalRisc5; import com.seaofnodes.isa.eval.EvalArm64; import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.util.Utils; import com.seaofnodes.simple.codegen.CodeGen; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - import com.seaofnodes.simple.node.cpus.arm.arm; import com.seaofnodes.simple.node.cpus.riscv.riscv; +import java.io.IOException; import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.*; @@ -30,8 +28,8 @@ public class Chapter22Test { var enc = code._encoding; // Link the external call to a tiny independent native stub. var call = code._stop.walk(n -> n instanceof com.seaofnodes.simple.node.CallNode ? n : null); - int start = enc._opStart[call._nid]; - ((com.seaofnodes.simple.codegen.RIPRelSize)call).patch(enc,start,enc._opLen[call._nid],0x1000-start); + int start = enc.opStart(call); + ((com.seaofnodes.simple.codegen.RIPRelSize)call).patch(enc,start,enc.opLen(call),0x1000-start); for( long value : values ) { long expected = switch(i) { case 0 -> (byte)value; case 1 -> value&255; @@ -45,7 +43,6 @@ public class Chapter22Test { if( target.equals("arm") ) { EvalArm64 cpu = new EvalArm64(image,1<<15); // MOVZ X0,0x1100; LDR X0,[X0]; RET X30. - cpu._pc = enc._opStart[code.link(code._main)._nid]; cpu.st4(0x1000,0xd2822000); cpu.st4(0x1004,0xf9400000); cpu.st4(0x1008,0xd65f03c0); cpu.st8(0x1100,value); @@ -55,7 +52,6 @@ public class Chapter22Test { } else { EvalRisc5 cpu = new EvalRisc5(image,1<<15); // AUIPC a0,0; LD a0,12(a0); RET. - cpu._pc = enc._opStart[code.link(code._main)._nid]; cpu.st4(0x1000,0x00000517); cpu.st4(0x1004,0x00c53503); cpu.st4(0x1008,0x00008067); // RV64 widens even u32 returns by sign-extending bit 31. @@ -83,148 +79,80 @@ public class Chapter22Test { val test_i64={->return c_i64();}; val test_cmp={->return c_i32()==-1;}; """; - for( String conv : new String[]{"SystemV","Win64"} ) - TestC.run(src,conv,com.seaofnodes.simple.type.TypeInteger.BOT,conv.equals("SystemV") ? "sysv_abi" : "ms_abi", - "src/test/java/com/seaofnodes/simple/progs/c_returns.c", - "build/objs/c_returns"+conv,"S","",-1); - } - - - - @Test public void testReturnedFunctions() throws IOException { - for( String src : new String[]{"return {->42;};", "val f={->42;}; return f;", "return sys.io.p;"} ) - for( String target : new String[]{"riscv","arm"} ) { - CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,target,"SystemV").exportELF(null); - byte[] image = new byte[1<<16]; - byte[] bits = code._encoding.bits(); - System.arraycopy(bits,0,image,0,bits.length); - boolean library = src.contains("sys.io.p"); - // u8[]: four-byte length followed by its bytes. - image[0x2000]=2; image[0x2004]='o'; image[0x2005]='k'; - int entry = code._encoding._opStart[code.link(code._main)._nid]; - if( target.equals("riscv") ) { - EvalRisc5 cpu = new EvalRisc5(image,1<<15); - cpu._pc = entry; - assertEquals(0,cpu.step(1000)); - assertEquals(0,cpu._pc); - cpu._pc = (int)cpu.regs[riscv.A0]; // Call the returned address. - cpu.regs[library ? riscv.A1 : riscv.A0] = 0x2000; // Methods also receive self. - assertEquals(0,cpu.step(1000)); - assertEquals(0,cpu._pc); - if( library ) assertEquals("ok",cpu._stdout.toString()); - else assertEquals(42,cpu.regs[riscv.A0]); - } else { - EvalArm64 cpu = new EvalArm64(image,1<<15); - cpu._pc = entry; - assertEquals(0,cpu.step(1000)); - assertEquals(0,cpu._pc); - cpu._pc = (int)cpu.regs[0]; - cpu.regs[library ? 1 : 0] = 0x2000; - assertEquals(0,cpu.step(1000)); - assertEquals(0,cpu._pc); - if( library ) assertEquals("ok",cpu._stdout.toString()); - else assertEquals(42,cpu.regs[0]); - } - } - CodeGen unused = new CodeGen("({->42;}); return 0;").driver(CodeGen.Phase.FinalizeIR); - assertEquals(1,unused._stop.nIns()); - } - - @Test public void testReturnedFunctionsNative() throws IOException { - String src = "val factory={int x -> if(x) return {->42;}; return {->43;};}; val printer={->return sys.io.p;};"; - String obj = "build/objs/funptr.o"; - new CheckedCodeGen(src).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,obj); - assertEquals("ok",TestC.gcc(obj,"","src/test/java/com/seaofnodes/simple/progs/funptr.c",false, - "build/objs/funptr"+(TestC.OS.startsWith("Windows") ? ".exe" : ""))); - } - - - @Test public void testRiscvRightShifts() { - // SRAI, SRLI, SRA, SRL: a0 = -32 shifted by 4. - int[] ops = {0x40455513,0x00455513,0x40b55533,0x00b55533}; - for( int i=0; i<ops.length; i++ ) { - EvalRisc5 r5 = new EvalRisc5(new byte[1<<16],1<<15); - r5.st4(0,ops[i]); - r5.regs[riscv.A0] = -32; - r5.regs[riscv.A0+1] = 4; - assertEquals(0,r5.step(1)); - assertEquals((i&1)==0 ? -2L : 0x0fff_ffff_ffff_fffeL,r5.regs[riscv.A0]); - } + for( String conv : new String[]{"SystemV","win64"} ) + TestC.run(src,"c_returns"+conv,null,conv, + conv.equals("SystemV") ? "sysv_abi" : "ms_abi", + TestC.C_DRIVERS_DIR+"c_returns.c","",-1); } - @Test public void testRiscvPointerRelocation() { - CodeGen code = new CheckedCodeGen("return \"x\";").driver(CodeGen.Phase.Encoding,"riscv","SystemV"); - var enc = code._encoding; - assertEquals(1,enc._bigCons.size()); - var ptr = (com.seaofnodes.simple.node.cpus.riscv.TMPRISC)enc._bigCons.keySet().iterator().next(); - int start = enc._opStart[ptr._nid]; - for( int delta : new int[]{0x7ff,0x800,0xfff,0x1000,0x1800,-1,-0x800,-0x801} ) { - ptr.patch(enc,start,enc._opLen[ptr._nid],delta); - // Place the program above zero so backward targets are valid addresses. - byte[] image = new byte[1<<16]; - System.arraycopy(enc.bits(),0,image,0x4000,enc._bits.size()); - EvalRisc5 r5 = new EvalRisc5(image,1<<15); - r5._pc = 0x4000; - assertEquals(0,r5.step(100)); - assertEquals(0x4000+start+delta,r5.regs[riscv.A0]); - } - } @Test public void testSubZeroTypeError() { try { new CodeGen("return null-0;").driver(CodeGen.Phase.TypeCheck); fail("Subtraction must reject null even when the other operand is zero"); } catch( Parser.ParseException e ) { - assertEquals("Cannot '-' null",e.getMessage()); + assertEquals("Cannot null - 0",e.getMessage()); } } - @Test public void testInfiniteReturn() { - String src = "struct S { int i; }; !S !s = new S; while(1) s.i++; return s.i;"; - testCPU(src,"x86_64_v2","SystemV",0,"return Top;"); - testCPU(src,"riscv","SystemV",0,"return Top;"); - testCPU(src,"arm","SystemV",0,"return Top;"); - } - - // Frozen Jig runs in Chapter23AllocTest; the revised input is in Chapter24AllocTest. - @Test @Ignore + @Ignore + @Test public void testJig() throws IOException { - String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/jig.smp")); - //String src = Files.readString(Path.of("docs/examples/BubbleSort.smp")); - testCPU(src,"x86_64_v2", "Win64" ,-1,null); + String src = """ +struct s0 { + bool v1; + i16 v2; + int v3; + i8 v4; + byte v5; +}; +while(new s0.v3) + while(new s0.v5<<new s0.v4) {} +if(0) { + if(0) { + flt !P5ZUD4=new s0.v2; + } + while(0) {} +} +return new s0.v1; +"""; + testCPU(src,"x86_64_v2", "win64" ,-1,null); testCPU(src,"riscv" , "SystemV",-1,null); testCPU(src,"arm" , "SystemV",-1,null); } static CodeGen testCPU( String src, String cpu, String os, int spills, String stop ) { CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); - SpillStats.record(code,"Chapter22",cpu,os); - SpillStats.checkSpills(spills,code._regAlloc._spillScaled); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); if( stop != null ) - assertEquals(stop, code._stop.toString()); + assertEquals(stop, code.print()); return code; } - static int testCPUSize( String src, String cpu, String os, int spills, String stop ) { - return testCPU(src,cpu,os,spills,stop)._encoding._bits.size(); + static int testCPUSize( String src, String cpu, String os, int spills ) { + // Includes the file initializer's hidden $init=true store. + return testCPU(src,cpu,os,spills, "return 0;" )._encoding._bits.size(); } // Should not fold away @Test public void testSextFail() throws IOException { String src = """ -struct Person { i32 age;}; -!Person !p = new Person; +struct _Person { i32 age;}; +!_Person !p = new _Person; p.age = (arg<<17)>>17; return 0; """; - assertEquals(46, testCPUSize(src, "x86_64_v2","Win64",2,"return 0;")); - assertEquals(60, testCPUSize(src, "riscv","SystemV",4,"return 0;")); - assertEquals(56, testCPUSize(src, "arm","SystemV",4,"return 0;")); + assertEquals(76, testCPUSize(src, "x86_64_v2","win64", 3 )); + assertEquals(92, testCPUSize(src, "riscv", "SystemV", 4 )); + assertEquals(92, testCPUSize(src, "arm" , "SystemV", 4 )); // do assertEquals here - EvalRisc5 R5 = TestRisc5.build("sext_str_not_fold_away", src,0, 4, false); + EvalRisc5 R5 = TestRisc5.build( src, "sext_str_not_fold_away", 0, 4, false); int trap = R5.step(100); assertEquals(0,trap); @@ -237,24 +165,23 @@ return 0; // Should not fold away @Test public void testSextFail2() throws IOException { String src = """ - - struct Person { i32 age;}; - !Person !p = new Person; + struct _Person { i32 age;}; + !_Person !p = new _Person; p.age = (arg<<48)>>48; return 0; """; - EvalRisc5 R5 = TestRisc5.build("sext_str_not_fold_away_2", src, 0, 4, false); + EvalRisc5 R5 = TestRisc5.build( src, "sext_str_not_fold_away_2", 0, 4, false); int trap = R5.step(100); assertEquals(0,trap); - EvalArm64 A5 = TestArm64.build("sext_str_not_fold_away_2", src, 0, 4, false); + EvalArm64 A5 = TestArm64.build("sext_str_not_fold_away_2", src, 0, 3, false); int trap_arm = A5.step(100); assertEquals(0,trap_arm); - assertEquals(46, testCPUSize(src, "x86_64_v2","Win64",2,"return 0;")); - assertEquals(60, testCPUSize(src, "riscv","SystemV",4,"return 0;")); - assertEquals(56, testCPUSize(src, "arm","SystemV",4,"return 0;")); + assertEquals(76, testCPUSize(src, "x86_64_v2","win64",3 )); + assertEquals(92, testCPUSize(src, "riscv", "SystemV",4 )); + assertEquals(92, testCPUSize(src, "arm", "SystemV",4 )); } @@ -262,13 +189,13 @@ return 0; @Test public void testSextSuccess() throws IOException { String src = """ // Should fold away sign extend -struct Person { i8 age;}; -!Person !p = new Person; +struct _Person { i8 age;}; +!_Person !p = new _Person; p.age = (arg<<48)>>48; return 0; - """; +"""; - EvalRisc5 R5 = TestRisc5.build("sext_str_fold_away", src, 0, 5, false); + EvalRisc5 R5 = TestRisc5.build( src, "sext_str_fold_away", 0, 5, false); int trap = R5.step(100); assertEquals(0,trap); @@ -276,9 +203,9 @@ return 0; int trap_arm = A5.step(100); assertEquals(0,trap_arm); - assertEquals(41, testCPUSize(src, "x86_64_v2","Win64",2,"return 0;")); - assertEquals(56, testCPUSize(src, "riscv","SystemV",5,"return 0;")); - assertEquals(52, testCPUSize(src, "arm","SystemV",5,"return 0;")); + assertEquals(66, testCPUSize(src, "x86_64_v2","win64",3 )); + assertEquals(84, testCPUSize(src, "riscv", "SystemV",5 )); + assertEquals(84, testCPUSize(src, "arm", "SystemV",5)); // do assertEquals here } @@ -287,17 +214,17 @@ return 0; @Test public void testPerson() throws IOException { String src = """ -struct Person { +struct Person { // Exports a field typed singleton+final *class:Person{age:i32} i32 age; }; -val fcn = { !Person?[] !ps, int x -> +val fcn = { !Person?[] !ps, int x -> // exports a field with a constant fcn ptr; exports the fcn also if( ps[x] ) ps[x].age++; }; """; String person = "6\n"; - TestC.run(src, "person", null, person, 0); + TestC.run(src, "person2", null, TestC.CALL_CONVENTION, "", TestC.C_DRIVERS_DIR+"person.c", person, 2); // Memory layout starting at PS: int ps = 1<<16; // Person array pointer starts at heap start @@ -308,7 +235,7 @@ val fcn = { !Person?[] !ps, int x -> int p1 = ps+4*8+1*8; // P2 = { age } // sizeof=8 int p2 = ps+4*8+2*8; - EvalRisc5 R5 = TestRisc5.build("person", src, ps, 0, false); + EvalRisc5 R5 = TestRisc5.build( src, "fcn", ps, 2, false); R5.regs[riscv.A1] = 1; // Index 1 R5.st8(ps,3); // Length R5.st8(ps+1*8,p0); @@ -324,7 +251,7 @@ val fcn = { !Person?[] !ps, int x -> assertEquals(17+1,R5.ld8(p1)); assertEquals(60+0,R5.ld8(p2)); - EvalArm64 A5 = TestArm64.build("person", src, ps, 0, false); + EvalArm64 A5 = TestArm64.build("fcn", src, ps, 2, false); A5.regs[arm.X1] = 1; // Index 1 A5.st8(ps, 3); A5.st8(ps+1*8,p0); @@ -344,47 +271,62 @@ val fcn = { !Person?[] !ps, int x -> @Test public void testCoRecur() { String src = """ -struct A { !B? !b; !C? !c; i64 ax; val az = x*2; }; -struct B { !A? !a; !C? !c; f32 bx; val bz = x*3; }; -struct C { !A? !a; !B? !b; f64 cx; val cz = x*x; }; -!A !aa = new A{ ax=17; }; -!B !bb = new B{ bx=3.14; a = aa; }; -!C !cc = new C{ cx=2.73; a = aa; b = bb; }; +val x = 5; // aa.az; // Error to self-define forward ref +struct _A { !Test._B? !b; !Test._C? !c; i64 ax; val az = x*2; new _A = { i64 x -> ax=x; }; }; +struct _B { !Test._A? !a; !Test._C? !c; f32 bx; val bz = x*3; new _B = { f32 x, !Test._A? aa -> bx=x; a=aa; }; }; +struct _C { !Test._A? !a; !Test._B? !b; f64 cx; val cz = x*x; new _C = { f64 x, !Test._A? aa, !Test._B? bb -> cx=x; a=aa; b=bb; }; }; +!_A !aa = new _A(17); +!_B !bb = new _B(3.14f, aa); +!_C !cc = new _C(2.73, aa, bb); aa.b = bb; aa.c = cc; bb.c = cc; -val x = 5; // aa.az; // Error to self-define forward ref return cc.cz; """; CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("return 25;", code._stop.toString()); + assertEquals("return 25;", code.print()); assertEquals("25", Eval2.eval(code, 0)); } @Test public void testHelloWorld() throws IOException { - String src = -""" -sys.io.p("Hello, World!"); + String src = """ +// fd buf len -> len +{ i32 i64 u32 -> u32 } write = "C"; +// Write byte array to stdout +val p = { u8[~] str -> + i64 ptr = str; // cast array base to i64 + return write(1,ptr,str#); +}; +p("Hello, World!"); return 0; """; - TestC.run(src,TestC.CALL_CONVENTION,null, null,null,"build/objs/helloWorld","","Hello, World!",0); + TestC.runSF(src,"helloWorld", "Hello, World!",2); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("helloWorld", src, 0, 2, false); + EvalRisc5 R5 = TestRisc5.build( src, "helloWorld", 0, 5, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(0,R5.regs[riscv.A0]); assertEquals("Hello, World!",R5._stdout.toString()); // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("helloWorld", src,0, 2, false); + EvalArm64 arm = TestArm64.build("helloWorld", src,0, 5, false); trap = arm.step(100); assertEquals(0,trap); assertEquals(0,arm.regs[0]); assertEquals("Hello, World!",arm._stdout.toString()); } + + @Test + public void testStrLoad() { + CodeGen code = new CodeGen("return \"abc\"[1];"); + code.driver(CodeGen.Phase.FinalizeIR); + assertEquals("return .[];", code.print()); + } + + @Test public void testFinalArray() { String src = """ @@ -392,16 +334,16 @@ int N=4; i32[] !is = new i32[N]; for( int i=0; i<N; i++ ) is[i] = i*i; -val sum = { i32[~] is -> // final array +val _sum = { i32[~] is -> // final array int sum=0; for( int i=0; i<is#; i++ ) sum += is[i]; return sum; }; -return sum(is); +return _sum(is); """; CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("return Phi(Loop,0,(Phi_sum+.[]));", code._stop.toString()); + assertEquals("return Phi(Loop,0,(Phi_sum+.[]));", code.print()); assertEquals("14", Eval2.eval(code, 0)); } @@ -413,10 +355,10 @@ return sum(is); // Echo stdin to stdout. return sys.io.p( sys.io.stdin() ); """; - TestC.run(src,TestC.CALL_CONVENTION,null, null,null,"build/objs/echo","","",0); + TestC.runSF(src,"echo", "",0); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("echo", src, 0, 2, false); + EvalRisc5 R5 = TestRisc5.build( src, "echo", 0, 2, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(0,R5.regs[riscv.A0]); diff --git a/src/test/java/com/seaofnodes/simple/Chapter23AllocTest.java b/src/test/java/com/seaofnodes/simple/Chapter23AllocTest.java deleted file mode 100644 index f3cc7512..00000000 --- a/src/test/java/com/seaofnodes/simple/Chapter23AllocTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.seaofnodes.simple; - -import java.io.IOException; -import java.nio.file.*; -import org.junit.Ignore; -import org.junit.Test; - -// Revised or newly enabled allocation examples in Chapter 23. -public class Chapter23AllocTest { - @Test - public void testString() throws IOException { - String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/stringHash.smp")); - Chapter23Test.testCPU(src,"x86_64_v2", "SystemV", 9,null); - Chapter23Test.testCPU(src,"riscv" , "SystemV", 3,null); - Chapter23Test.testCPU(src,"arm" , "SystemV", 1,null); - } - - @Ignore - @Test - public void testJig21() throws IOException { - String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/jig.smp")); - Chapter23Test.testCPU(src,"x86_64_v2", "Win64" ,-1,null); - Chapter23Test.testCPU(src,"riscv" , "SystemV",-1,null); - Chapter23Test.testCPU(src,"arm" , "SystemV",-1,null); - } - - @Ignore - @Test - public void testJig22() throws IOException { - String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/jig.smp")); - //String src = Files.readString(Path.of("docs/examples/BubbleSort.smp")); - Chapter23Test.testCPU(src,"x86_64_v2", "Win64" ,-1,null); - Chapter23Test.testCPU(src,"riscv" , "SystemV",-1,null); - Chapter23Test.testCPU(src,"arm" , "SystemV",-1,null); - } - -} diff --git a/src/test/java/com/seaofnodes/simple/Chapter23Test.java b/src/test/java/com/seaofnodes/simple/Chapter23Test.java index 22be3af1..df02d3b4 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter23Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter23Test.java @@ -11,7 +11,6 @@ import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.node.cpus.riscv.riscv; -import com.seaofnodes.simple.type.TypeInteger; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -25,7 +24,7 @@ public class Chapter23Test { @Test public void testDiagnosticTypeAccessors() throws Exception { new CodeGen("return 0;").parse(); var tfp = TypeFunPtr.TEST; - var con = new ConstantNode(tfp); + var con = ConstantNode.raw(tfp); var call = new CallNode(null,con,con,con); var field = Type.class.getDeclaredField("VISIT"); field.setAccessible(true); @@ -45,335 +44,252 @@ public class Chapter23Test { } - @Test - public void testOrRhsLoopLocal() throws IOException { - checkOrRhsLoop(""" - struct S { int f; }; - int x = 0; - int result = (arg & 1) || (new S { - while (x < 3) x++; - f = ++x; - }).f; - return x * 10 + result; - """); - } - - @Test - public void testOrRhsLoopMemory() throws IOException { - checkOrRhsLoop(""" - struct S { int f; }; - !S !s = new S; - int result = (arg & 1) || (new S { - while (s.f < 3) s.f++; - s.f++; - f = s.f; - }).f; - return s.f * 10 + result; - """); - } - - private static void checkOrRhsLoop(String src) throws IOException { - CodeGen code = new CheckedCodeGen(src).driver("riscv", "SystemV", null); - SpillStats.record(code,"Chapter23","riscv","SystemV"); - for (int arg : new int[]{0, 1, 2, 3}) { - byte[] image = new byte[1 << 20]; - System.arraycopy(code._encoding.bits(), 0, image, 0, code._encoding._bits.size()); - EvalRisc5 r5 = new EvalRisc5(image, 1 << 16); - r5.regs[riscv.A0] = arg; - assertEquals("Execution must finish", 0, r5.step(1000)); - // False LHS runs the loop and final increment, returning 4. - // True LHS skips all RHS effects and returns the LHS value, 1. - assertEquals("OR result and side effects for arg=" + arg, - (arg & 1) == 0 ? 44 : 1, r5.regs[riscv.A0]); - } - } - - @Ignore - @Test + @Test @Ignore public void testJig() throws IOException { - String src = -""" -var apply = { i64 x, { i64 -> u32 } fcn -> - return fcn(x); -} -"""; + //String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/jig.smp")); + String src = Files.readString(Path.of("docs/examples/BubbleSort.smp")); + testCPU(src,"x86_64_v2", "win64" ,-1,null); + testCPU(src,"riscv" , "SystemV",-1,null); + testCPU(src,"arm" , "SystemV",-1,null); } static CodeGen testCPU( String src, String cpu, String os, int spills, String stop ) { CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); - SpillStats.record(code,"Chapter23",cpu,os); - SpillStats.checkSpills(spills,code._regAlloc._spillScaled); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); if( stop != null ) - assertEquals(stop, code._stop.toString()); + assertEquals(stop, code.print()); return code; } @Test public void testOr() throws IOException { - String src = - """ - int a = 1; - int b = 0; - - if(a++ || b++ ) { - if(b == 0 && a == 2) { - sys.io.p("Or"); - } - } else{ - sys.io.p("And"); - } - return 0; - """; - - TestC.runSF("or1", src, null, "Or", 0); + String src = """ +int a = 1; +int b = 0; +int rez = 2; + +if( a++ || b++ ) { + if( b == 0 && a == 2 ) + rez = 0; // Expected answer +} else { + rez = 1; +} +return rez; +"""; + + TestC.runSF( src, "or1", "", 0); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("or1", src, 0, 2, false); + EvalRisc5 R5 = TestRisc5.build( src, "or1", 0, 0, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(0,R5.regs[riscv.A0]); - assertEquals("Or",R5._stdout.toString()); // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("or1", src,0, 2, false); + EvalArm64 arm = TestArm64.build("or1", src,0, 0, false); trap = arm.step(100); assertEquals(0,trap); assertEquals(0,arm.regs[0]); - assertEquals("Or",arm._stdout.toString()); } @Test public void testAnd() throws IOException { - String src = - """ - int a = 1; - int b = 1; - - if(a && b) { - sys.io.p("And"); - } else { - sys.io.p("Or"); - } - return 0; - """; - TestC.runSF("and1", src, null, "And", 0); + String src = """ +int a = 1; +int b = 1; +return a && b ? 0 : 1; // Expected answer 0 +"""; + TestC.runSF( src, "and1", "", 0); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("and1", src, 0, 2, false); + EvalRisc5 R5 = TestRisc5.build( src, "and1", 0, 0, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(0,R5.regs[riscv.A0]); - assertEquals("And",R5._stdout.toString()); // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("and1", src, 0, 2, false); + EvalArm64 arm = TestArm64.build("and1", src, 0, 0, false); trap = arm.step(100); assertEquals(0,trap); assertEquals(0,arm.regs[0]); - assertEquals("And",arm._stdout.toString()); + } + + @Test + public void testLogicalValueSemantics() { + assertEquals("42", Eval2.eval(new CodeGen("return 7 && 42;").driver(CodeGen.Phase.FinalizeIR), 0)); + assertEquals("0", Eval2.eval(new CodeGen("return 0 && 42;").driver(CodeGen.Phase.FinalizeIR), 0)); + assertEquals("7", Eval2.eval(new CodeGen("return 7 || 42;").driver(CodeGen.Phase.FinalizeIR), 0)); + assertEquals("42", Eval2.eval(new CodeGen("return 0 || 42;").driver(CodeGen.Phase.FinalizeIR), 0)); + } + + @Test + public void testMixedScalarNotPhiPredicate() { + CodeGen code = new CodeGen(""" +struct S {}; +S? s = arg ? new S; +return !(!s || !arg) ? 1 : 0; +"""); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("0", Eval2.eval(code, 0)); + assertEquals("1", Eval2.eval(code, 1)); } @Test public void testAndPtr() throws IOException { // Todo: have one src here - String src = - """ - struct S { !S? !fld; }; - val ptr = arg == 1 ? null : new S{fld = arg==1 ? null : new S{fld = null;};}; - return ptr && ptr.fld ? "true" : "false"; - """; - String src2 = - """ - struct S { !S? !fld; }; - - val ptr = arg == 1 ? null : new S{fld = arg==1 ? null : new S{fld = null;};}; - if( ptr && ptr.fld ) { - sys.io.p("true"); - } else { - sys.io.p("false"); - } - - return 0; - """; + String src = """ +struct S { !S? !fld; new S = { !S? f -> fld=f; }; }; +val ptr = arg == 1 ? null : new S(arg==1 ? null : new S(null)); +return ptr && ptr.fld ? "true" : "false"; +"""; CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("[97-117][ 116,114,117,101]", Eval2.eval(code, 0)); + assertEquals("true", Eval2.eval(code, 0)); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("ptrand",src2, 1, 8, false); + EvalRisc5 R5 = TestRisc5.build( src, "class:Test.<clinit>", 1, 12, false); int trap = R5.step(100); assertEquals(0,trap); - assertEquals(0,R5.regs[riscv.A0]); - assertEquals("false",R5._stdout.toString()); + int adr = (int)R5.regs[riscv.A0]; // Returns a Simple *u8[~] string + assertEquals(4,R5.ld4s(adr)); // String length + assertEquals(0x65757274,R5.ld4s(adr+4)); // "true" // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("ptrand", src2, 1, 8, false); + EvalArm64 arm = TestArm64.build("class:Test.<clinit>", src, 1, 12, false); trap = arm.step(100); assertEquals(0,trap); - assertEquals(0,arm.regs[0]); - assertEquals("false",arm._stdout.toString()); + int adr2 = (int)arm.regs[0]; // Returns a Simple *u8[~] string + assertEquals(4,arm.ld4s(adr2)); // String length + assertEquals(0x65757274,arm.ld4s(adr2+4)); // "true" } // conditional side effect @Test public void testCondSideEffAnd() throws IOException { - String src = - """ - int a = 1; - int b = 1; - - int x=1; - int y=1; - int z=0; - - int g = x++ && y++ && z++; - - if(x == 2 && y == 2 && z == 1 && g == 0) { - sys.io.p("Effected"); - } else { - sys.io.p("Not effected"); - } - return 0; - """; - - TestC.runSF("and2", src, null, "Effected", 0); + String src = """ +int x=1; +int y=1; +int z=0; + +int g = x++ && y++ && z++; +return x == 2 && y == 2 && z == 1 && g == 0 + ? 0 // Effected, Expected + : 1; // Error, not effected +"""; + + TestC.runSF( src, "and2", "", 0); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("and2", src, 0, 2, false); + EvalRisc5 R5 = TestRisc5.build( src, "and2", 0, 0, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(0,R5.regs[riscv.A0]); - assertEquals("Effected",R5._stdout.toString()); // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("and2", src, 0, 2, false); + EvalArm64 arm = TestArm64.build("and2", src, 0, 0, false); trap = arm.step(100); assertEquals(0,trap); assertEquals(0,arm.regs[0]); - assertEquals("Effected",arm._stdout.toString()); } @Test public void testCondSideEffOr() throws IOException { - String src = - """ - int a = 1; - int b = 1; - - int x = -1; - int y=1; - int z= -1; - - int g = x++ || y++ || z++; - - int switch = 0; - if(x == 4 || y == 4 || z == 4) { - switch = -1; - } else { - switch = 1; - } - int cd = 1; - if((x == 0 && y == 1 && z==-1) && switch && g) { - sys.io.p("Effected"); - } else { - sys.io.p("Not effected"); - } - return 0; - """; - TestC.runSF("or2", src, null, "Effected", 0); + String src = """ +int x = -1; +int y = 1; +int z = -1; +int g = x++ || y++ || z++; +int switch = (x == 4 || y == 4 || z == 4) ? -1 : 1; +return (x == 0 && y == 1 && z==-1) && switch && g + ? 0 // Effected, expected + : 1; // Error +"""; + TestC.runSF( src, "or2", "", 0); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("or2", src, 0, 2, false); + EvalRisc5 R5 = TestRisc5.build( src, "or2", 0, 0, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(0,R5.regs[riscv.A0]); - assertEquals("Effected",R5._stdout.toString()); // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("or2", src, 0, 2, false); + EvalArm64 arm = TestArm64.build("or2", src, 0, 0, false); trap = arm.step(100); assertEquals(0,trap); assertEquals(0,arm.regs[0]); - assertEquals("Effected",arm._stdout.toString()); } // test it with function calls @Test public void testFuncCall() throws IOException { - String src = - """ - // -*- mode: java; -*- - int a = 1; - int b = 1; - - var sq = { int x -> - x*x; - }; - - if(a && sq(0)) { - sys.io.p("And"); - } else { - sys.io.p("Or"); - } - return 0; - """; - TestC.runSF("and3", src, null, "Or", 0); + String src = """ +int a = 1; +int b = 1; +var sq_noInline = { int x -> + x*x; +}; +return (a && sq_noInline(0)) + ? 1 // Error + : 0; // Expected +"""; + TestC.runSF( src, "and3", "", 2); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("and3", src, 0, 2, false); + EvalRisc5 R5 = TestRisc5.build( src, "and3", 0, 2, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(0,R5.regs[riscv.A0]); - assertEquals("Or",R5._stdout.toString()); // Evaluate on ARM emulator EvalArm64 arm = TestArm64.build("and3", src, 0, 2, false); trap = arm.step(100); assertEquals(0,trap); assertEquals(0,arm.regs[0]); - assertEquals("Or",arm._stdout.toString()); } @Test public void testFRefFields() { - String src = -""" + String src = """ // A hypothetical scanner class -struct Scan { +struct _Scan { int !x; u8[~] !buf; + new _Scan = { u8[~] b -> buf=b; }; // Skip whitespace - val skip = { !Scan s -> - while( s.buf[s.x] <= ' ' ) - s.x++; + val skip = { -> + while( buf[x] <= ' ' ) + x++; }; // Peek a character; if matched consume it, else false. - val peek = { !Scan !s, u8 c -> - skip(s); - if( s.buf[s.x] != c ) return false; - s.x++; + val peek = { u8 c -> + skip(); + if( buf[x] != c ) return false; + x++; return true; }; }; -!Scan !s = new Scan{ buf = " q"; }; -return Scan.peek(s,'q'); - """; +!_Scan !_s = new _Scan(" q"); +return _s.peek('q'); +"""; CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("return Phi(Region,0,1);", code._stop.toString()); + 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",23,null); }; @Test public void testFRefFields2() { - String src = -""" + String src =""" // A hypothetical scanner class -struct Scan { +struct _Scan { int !x; u8[~] !buf; + new _Scan = { u8[~] b -> buf=b; }; // Skip whitespace val skip = { -> while( buf[x] <= ' ' ) @@ -381,23 +297,26 @@ struct Scan { return self; }; }; -~Scan s = new Scan{ buf = " q"; }; +~_Scan s = new _Scan(" q"); return s.skip().x; - """; +"""; try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { org.junit.Assert.assertTrue(e.getMessage().startsWith("Argument #0 isa")); } + catch( Exception e ) { + // Bad error message, but basically requires a mutable 'x' field. + org.junit.Assert.assertTrue(e.getMessage().startsWith("Argument #0 isa")); + } }; @Test public void testMethod() { - String src = -""" + String src = """ // A hypothetical scanner class -struct Scan { +struct _Scan { int !x; u8[~] !buf; + new _Scan = { u8[~] b -> buf=b; }; // Skip whitespace val skip = { -> while( buf[x] <= ' ' ) @@ -408,14 +327,14 @@ struct Scan { buf[x++]==ch; }; }; -!Scan !s = new Scan{ buf = " [1,2]"; }; -return s.require('['); - """; +!_Scan !_s = new _Scan(" [1,2]"); +return _s.require('['); +"""; CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("return (.[]==91);", code._stop.toString()); + assertEquals("return (.[]==91);", code.print()); assertEquals("1", Eval2.eval(code, 0)); - testCPU(src,"x86_64_v2", "Win64" ,4,null); + testCPU(src,"x86_64_v2", "win64",4, null); }; diff --git a/src/test/java/com/seaofnodes/simple/Chapter24AllocTest.java b/src/test/java/com/seaofnodes/simple/Chapter24AllocTest.java deleted file mode 100644 index 7fae2c27..00000000 --- a/src/test/java/com/seaofnodes/simple/Chapter24AllocTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.isa.eval.EvalRisc5; - -import com.seaofnodes.isa.eval.EvalArm64; - -import java.io.IOException; -import java.nio.file.*; -import com.seaofnodes.simple.node.cpus.arm.arm; -import com.seaofnodes.simple.node.cpus.riscv.riscv; -import org.junit.Ignore; -import org.junit.Test; -import static org.junit.Assert.*; - -// Revised inherited programs belong to the chapter which changed their inputs. -public class Chapter24AllocTest { - @Test - public void testNewtonInteger() { - String src = -""" -// Newtons approximation to the square root -val sqrt = { int x -> - int guess = x; - while( 1 ) { - int next = (x/guess + guess)/2; - if( next == guess ) return guess; - guess = next; - } -}; -int cast_int = arg+2; -return sqrt(arg) + sqrt(cast_int); -"""; - Chapter24Test.testCPU(src,"x86_64_v2", "Win64" ,40,null); - Chapter24Test.testCPU(src,"riscv" , "SystemV",19,null); - Chapter24Test.testCPU(src,"arm" , "SystemV",19,null); - } - - @Ignore - @Test - public void testJig21() throws IOException { - String src = -""" -struct s0 { - bool v1; - i16 v2; - int v3; - i8 v4; - byte v5; -}; -while(new s0.v3) - while(new s0.v5<<new s0.v4) {} -if(0) { - if(0) { - flt !P5ZUD4=new s0.v2; - } - while(0) {} -} -return new s0.v1; -"""; - Chapter24Test.testCPU(src,"x86_64_v2", "Win64" ,-1,null); - Chapter24Test.testCPU(src,"riscv" , "SystemV",-1,null); - Chapter24Test.testCPU(src,"arm" , "SystemV",-1,null); - } - - @Test public void testStackArguments() throws IOException { - // Test passes more args than registers in Sys5, which is far far more - // than what Win64 allows - so Win64 gets a lot more spills here. - String src = -""" -val addAll = { int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, flt f7, int x8, flt f9, int i10, flt f11, int i12, flt f13, int i14, flt f15, int x16, flt f17 int x18, flt f19 -> - return - i0 + f1+ i2+ f3+ i4+ f5+ i6+ f7+ x8 +f9 + - i10+f11+i12+f13+i14+f15+x16+f17+x18+f19 ; -}; -"""; - String arg_count = "191.000000\n"; - - TestC.run(src, "arg_count", null, arg_count, TestC.CALL_CONVENTION.equals("Win64") ? 32 : 9); - - EvalRisc5 R5 = TestRisc5.build("no_stack_arg_count", src, 0, 4, false); - - // Todo: handle stack(imaginary stack in emulator) - // pass in float arguments - R5.fregs[riscv.FA0 - riscv.F_OFFSET] = 1.1; - R5.fregs[riscv.FA1 - riscv.F_OFFSET] = 1.1; - R5.fregs[riscv.FA2 - riscv.F_OFFSET] = 1.1; - R5.fregs[riscv.FA3 - riscv.F_OFFSET] = 1.1; - R5.fregs[riscv.FA4 - riscv.F_OFFSET] = 1.1; - R5.fregs[riscv.FA5 - riscv.F_OFFSET] = 1.1; - R5.fregs[riscv.FA6 - riscv.F_OFFSET] = 1.1; - R5.fregs[riscv.FA7 - riscv.F_OFFSET] = 1.1; - - // a0 is passed in arg - R5.regs[riscv.A1] = 2; - R5.regs[riscv.A2] = 2; - R5.regs[riscv.A3] = 2; - R5.regs[riscv.A4] = 2; - R5.regs[riscv.A5] = 2; - R5.regs[riscv.A6] = 2; - R5.regs[riscv.A7] = 2; - - int trap = R5.step(100); - assertEquals(0,trap); - - double result = R5.fregs[riscv.FA0 - riscv.F_OFFSET]; - - assertEquals(22.8, result, 0.00001); - - // arm - EvalArm64 A5 = TestArm64.build("no_stack_arg_count", src, 0, 4, false); - - A5.fregs[arm.D0 - arm.D_OFFSET] = 1.1; - A5.fregs[arm.D1 - arm.D_OFFSET] = 1.1; - A5.fregs[arm.D2 - arm.D_OFFSET] = 1.1; - A5.fregs[arm.D3 - arm.D_OFFSET] = 1.1; - A5.fregs[arm.D4 - arm.D_OFFSET] = 1.1; - A5.fregs[arm.D5 - arm.D_OFFSET] = 1.1; - A5.fregs[arm.D6 - arm.D_OFFSET] = 1.1; - A5.fregs[arm.D7 - arm.D_OFFSET] = 1.1; - - A5.regs[arm.X1] = 2; - A5.regs[arm.X2] = 2; - A5.regs[arm.X3] = 2; - A5.regs[arm.X4] = 2; - A5.regs[arm.X5] = 2; - A5.regs[arm.X6] = 2; - A5.regs[arm.X7] = 2; - - int trap_arm = A5.step(100); - assertEquals(0,trap_arm); - - double result1 = A5.fregs[arm.D0 - arm.D_OFFSET]; - assertEquals(22.8, result1, 0.00001); - } - - @Ignore - @Test - public void testJig22() throws IOException { - String src = -""" -val fib = {int n -> - int temp=0; - int f1=1; - int f2=1; - int i=n; - while( i>1 ){ - temp = f1+f2; - f1=f2; - f2=temp; - i=i-1; - } - return f2; -}; - -fib(10); -"""; - Chapter24Test.testCPU(src,"x86_64_v2", "Win64" ,-1,null); - Chapter24Test.testCPU(src,"riscv" , "SystemV",-1,null); - Chapter24Test.testCPU(src,"arm" , "SystemV",-1,null); - } - - @Test - public void testBubbleSort() throws IOException { - //String src = Files.readString(Path.of("src/test/java/com/seaofnodes/simple/progs/jig.smp")); - String src = Files.readString(Path.of("docs/examples/BubbleSort.smp")); - Chapter24Test.testCPU(src,"x86_64_v2", "Win64" ,-1,null); - Chapter24Test.testCPU(src,"riscv" , "SystemV",-1,null); - Chapter24Test.testCPU(src,"arm" , "SystemV",-1,null); - } -} diff --git a/src/test/java/com/seaofnodes/simple/Chapter24Test.java b/src/test/java/com/seaofnodes/simple/Chapter24Test.java index 6e0fd95f..2bc2ce7f 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter24Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter24Test.java @@ -26,7 +26,7 @@ public class Chapter24Test { }; for( int i=0; i<srcs.length; i++ ) for( long seed : new long[]{0,42,126} ) { - var code = new CodeGen(srcs[i],TypeInteger.BOT,seed,true).driver(CodeGen.Phase.LocalSched); + var code = new CodeGen(srcs[i],seed,true).driver(CodeGen.Phase.LocalSched); assertEquals(i==0 ? "0" : "3",Eval2.eval(code,3)); } } @@ -36,14 +36,14 @@ public class Chapter24Test { @Test public void testPrintingDeadFunction() throws Exception { var code = new CodeGen("return 0;").parse(); var tfp = TypeFunPtr.TEST; - var fun = new FunNode(null,tfp,"deadPrinterTarget"); + var fun = new FunNode(null,tfp,"deadPrinterTarget",null); org.junit.Assert.assertTrue(fun.isDead()); code.link(fun); - var con = new ConstantNode(tfp); + var con = ConstantNode.raw(tfp); var call = new CallNode(null,con,con,con); con.toString(); call.name(); - + code.funcName(tfp.fidx()); org.junit.Assert.assertSame(fun,code._link(tfp)); // Optimizer lookup still performs its usual lazy cleanup. org.junit.Assert.assertNull(code.link(tfp)); @@ -55,435 +55,110 @@ public class Chapter24Test { @Test public void testJig() throws IOException { String src = "int v0=0; v0 = arg && 0== 0 !=0;"; - testCPU(src,"x86_64_v2", "Win64" ,-1,null); + testCPU(src,"x86_64_v2", "win64" ,-1,null); testCPU(src,"riscv" , "SystemV",-1,null); testCPU(src,"arm" , "SystemV",-1,null); } static CodeGen testCPU( String src, String cpu, String os, int spills, String stop ) { CodeGen code = new CheckedCodeGen(src).driver(CodeGen.Phase.Encoding,cpu,os); - SpillStats.record(code,"Chapter24",cpu,os); - SpillStats.checkSpills(spills,code._regAlloc._spillScaled); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); if( stop != null ) - assertEquals(stop, code._stop.toString()); + assertEquals(stop, code.print()); return code; } - @Test - public void testOr() throws IOException { - String src = -""" -int a = 1; -int b = 0; - -if(a++ || b++ ) { - if(b == 0 && a == 2) { - sys.io.p("Or"); - } -} else{ - sys.io.p("And"); -} -return 0; -"""; - - TestC.runSF("or1", src, null, "Or", 0); - - // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("or1", src, 0, 2, false); - int trap = R5.step(100); - assertEquals(0,trap); - assertEquals(0,R5.regs[riscv.A0]); - assertEquals("Or",R5._stdout.toString()); - - // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("or1", src,0, 2, false); - trap = arm.step(100); - assertEquals(0,trap); - assertEquals(0,arm.regs[0]); - assertEquals("Or",arm._stdout.toString()); - } - - @Test - public void testAnd() throws IOException { - String src = -""" -int a = 1; -int b = 1; - -if(a && b) { - sys.io.p("And"); -} else { - sys.io.p("Or"); -} -return 0; -"""; - TestC.runSF("and1", src, null, "And", 0); - - // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("and1", src, 0, 2, false); - int trap = R5.step(100); - assertEquals(0,trap); - assertEquals(0,R5.regs[riscv.A0]); - assertEquals("And",R5._stdout.toString()); - - // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("and1", src, 0, 2, false); - trap = arm.step(100); - assertEquals(0,trap); - assertEquals(0,arm.regs[0]); - assertEquals("And",arm._stdout.toString()); - } - - @Test - public void testAndPtr() throws IOException { - // Todo: have one src here - String src = -""" -struct S { !S? !fld; }; -val ptr = arg == 1 ? null : new S{fld = arg==1 ? null : new S{fld = null;};}; -return ptr && ptr.fld ? "true" : "false"; -"""; - String src2 = -""" -struct S { !S? !fld; }; - -val ptr = arg == 1 ? null : new S{fld = arg==1 ? null : new S{fld = null;};}; -if( ptr && ptr.fld ) { - sys.io.p("true"); -} else { - sys.io.p("false"); -} - -return 0; -"""; - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("[97-117][ 116,114,117,101]", Eval2.eval(code, 0)); - - // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("ptrand",src2, 1, 8, false); - int trap = R5.step(100); - assertEquals(0,trap); - assertEquals(0,R5.regs[riscv.A0]); - assertEquals("false",R5._stdout.toString()); - - // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("ptrand", src2, 1, 8, false); - trap = arm.step(100); - assertEquals(0,trap); - assertEquals(0,arm.regs[0]); - assertEquals("false",arm._stdout.toString()); - } - - // conditional side effect - @Test - public void testCondSideEffAnd() throws IOException { - String src = -""" -int a = 1; -int b = 1; - -int x=1; -int y=1; -int z=0; - -int g = x++ && y++ && z++; - -if(x == 2 && y == 2 && z == 1 && g == 0) { - sys.io.p("Effected"); -} else { - sys.io.p("Not effected"); -} -return 0; -"""; - - TestC.runSF("and2", src, null, "Effected", 0); - - // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("and2", src, 0, 2, false); - int trap = R5.step(100); - assertEquals(0,trap); - assertEquals(0,R5.regs[riscv.A0]); - assertEquals("Effected",R5._stdout.toString()); - - // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("and2", src, 0, 2, false); - trap = arm.step(100); - assertEquals(0,trap); - assertEquals(0,arm.regs[0]); - assertEquals("Effected",arm._stdout.toString()); - } - - - @Test - public void testCondSideEffOr() throws IOException { - String src = -""" -int a = 1; -int b = 1; - -int x = -1; -int y=1; -int z= -1; - -int g = x++ || y++ || z++; - -int switch = 0; -if(x == 4 || y == 4 || z == 4) { - switch = -1; -} else { - switch = 1; -} -int cd = 1; -if((x == 0 && y == 1 && z==-1) && switch && g) { - sys.io.p("Effected"); -} else { - sys.io.p("Not effected"); -} -return 0; -"""; - TestC.runSF("or2", src, null, "Effected", 0); - - // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("or2", src, 0, 2, false); - int trap = R5.step(100); - assertEquals(0,trap); - assertEquals(0,R5.regs[riscv.A0]); - assertEquals("Effected",R5._stdout.toString()); - - // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("or2", src, 0, 2, false); - trap = arm.step(100); - assertEquals(0,trap); - assertEquals(0,arm.regs[0]); - assertEquals("Effected",arm._stdout.toString()); - } - - // test it with function calls - @Test - public void testFuncCall() throws IOException { - String src = -""" -// -*- mode: java; -*- -int a = 1; -int b = 1; - -var sq = { int x -> - x*x; -}; - -if(a && sq(0)) { - sys.io.p("And"); -} else { - sys.io.p("Or"); -} -return 0; -"""; - TestC.runSF("and3", src, null, "Or", 0); - - // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("and3", src, 0, 2, false); - int trap = R5.step(100); - assertEquals(0,trap); - assertEquals(0,R5.regs[riscv.A0]); - assertEquals("Or",R5._stdout.toString()); - - // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("and3", src, 0, 2, false); - trap = arm.step(100); - assertEquals(0,trap); - assertEquals(0,arm.regs[0]); - assertEquals("Or",arm._stdout.toString()); - } - // test it with function calls @Test public void testStack1() throws IOException { - String src = -""" + String src =""" int something_specific = 11; - -if (10 < something_specific < 20) { - sys.io.p("In Range"); -} else { - sys.io.p("Out of Range"); -} -return 0; +return (10 < something_specific < 20) + ? 0 // Expected + : 1; // Error """; - - TestC.runSF("stacked_r_1", src, null, "In Range", 0); + TestC.runSF( src, "stacked_r_1", "", 0); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("stacked_r_1", src, 0, 2, false); + EvalRisc5 R5 = TestRisc5.build( src, "stacked_r_1", 0, 0, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(0,R5.regs[riscv.A0]); - assertEquals("In Range",R5._stdout.toString()); // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("stacked_r_1", src, 0, 2, false); + EvalArm64 arm = TestArm64.build("stacked_r_1", src, 0, 0, false); trap = arm.step(100); assertEquals(0,trap); assertEquals(0,arm.regs[0]); - assertEquals("In Range",arm._stdout.toString()); - } - - @Test - public void testStack2() throws IOException { - String src = -""" -int score = 75; - -if (60 <= score < 90) { - sys.io.p("In Range"); -} else { -sys.io.p("Out of range"); -} -return 0; -"""; - - TestC.runSF("stacked_r_2", src, null, "In Range", 0); - - // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("stacked_r_2", src, 0, 2, false); - int trap = R5.step(100); - assertEquals(0,trap); - assertEquals(0,R5.regs[riscv.A0]); - assertEquals("In Range",R5._stdout.toString()); - - // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("stacked_r_2", src, 0, 2, false); - trap = arm.step(100); - assertEquals(0,trap); - assertEquals(0,arm.regs[0]); - assertEquals("In Range",arm._stdout.toString()); } @Test public void testStack3() throws IOException { - String src = -""" -var sq = { int x -> + String src =""" +var sq_noInline = { int x -> x*x; }; - -int score = 5; - -val str = (sq(2) <= score < sq(3)) ? "In Range" : "Out of range"; -sys.io.p(str); -return 0; +int score = 4; +return (sq_noInline(2) <= score < sq_noInline(3)) + ? 0 // Expected + : 1; // Error """; - - TestC.runSF("stacked_r_3", src, null, "In Range", 3); + TestC.runSF( src, "stacked_r_3", "", 2); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("stacked_r_3", src, 0, 3, false); + EvalRisc5 R5 = TestRisc5.build( src, "stacked_r_3", 0, 2, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(0,R5.regs[riscv.A0]); - assertEquals("In Range",R5._stdout.toString()); // Evaluate on ARM emulator EvalArm64 arm = TestArm64.build("stacked_r_3", src, 0, 3, false); int trap_1 = arm.step(100); assertEquals(0,trap_1); assertEquals(0,arm.regs[0]); - assertEquals("In Range",arm._stdout.toString()); } @Test public void testStack4() throws IOException { - String src = -""" + String src =""" int something_specific = 9; - -if (10 < something_specific < 20) { - sys.io.p("In Range"); -} else { - sys.io.p("Out of Range"); -} -return 0; +return (10 < something_specific < 20) + ? 1 // Error + : 0; // Expected """; - TestC.runSF("stacked_r_4", src, null, "Out of Range", 0); + TestC.runSF( src, "stacked_r_4", "", 0); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("stacked_r_4", src,0, 2, false); + EvalRisc5 R5 = TestRisc5.build( src, "stacked_r_4", 0, 0, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(0,R5.regs[riscv.A0]); - assertEquals("Out of Range",R5._stdout.toString()); // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("stacked_r_4", src, 0, 2, false); + EvalArm64 arm = TestArm64.build("stacked_r_4", src, 0, 0, false); trap = arm.step(100); assertEquals(0,trap); assertEquals(0,arm.regs[0]); - assertEquals("Out of Range",arm._stdout.toString()); } @Test - public void testStack5() throws IOException { - String src = -""" -var sq = { int x -> - x*x; -}; - -int score = 5; - -if (sq(2) <= score < sq(3)) { -sys.io.p("In Range"); -} else { -sys.io.p("Out of range"); -} - -return 0; -"""; - TestC.runSF("stacked_r_5", src, null, "In Range", 3); - - // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("stacked_r_5", src, 0, 5, false); - int trap = R5.step(100); - assertEquals(0,trap); - assertEquals(0,R5.regs[riscv.A0]); - assertEquals("In Range",R5._stdout.toString()); - - // Evaluate on ARM emulator - EvalArm64 arm = TestArm64.build("stacked_r_5", src, 0, 5, false); - trap = arm.step(100); - assertEquals(0,trap); - assertEquals(0,arm.regs[0]); - assertEquals("In Range",arm._stdout.toString()); - } - - - @Test public void testStack6() { - try { new CodeGen( - """ - return 0 < arg > 1; - """ - ).parse(); fail(); } + try { new CodeGen("return 0 < arg > 1;" ).parse(); fail(); } catch( RuntimeException e ) { assertEquals("Mixing relational directions in a chained relational test",e.getMessage()); } } @Test public void testStack9() throws IOException { - String stack9 = "1"; - String src = -""" -var stack9 = { int x -> - (x < 1) == 1; -}; -"""; - String src2 = -""" -return (0 < 1) == 1; -"""; - TestC.run(src, "stacked_r_9x", null, stack9,0); + String src = "var stack9 = { int x -> (x < 1) == 1; };"; + String src2 = "return (0 < 1) == 1;"; + TestC.runC(src, "stacked_r_9x", "1", 0); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("stacked_r_9", src2, 0, 0, false); + EvalRisc5 R5 = TestRisc5.build( src2, "stacked_r_9", 0, 0, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(1,R5.regs[riscv.A0]); @@ -497,21 +172,12 @@ return (0 < 1) == 1; @Test public void testStack10() throws IOException { - String stack10 = "1"; - String src = -""" -var stack10 = { int x -> - (x < 1) == (1>x); -}; -"""; - String src2 = -""" -return (0 < 1) == (1>0); -"""; - TestC.run(src, "stacked_r_10x", null, stack10,0); + String src ="var stack10 = { int x -> (x < 1) == (1>x);};"; + String src2 ="return (0 < 1) == (1>0);"; + TestC.runC(src, "stacked_r_10x", "1",0); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("stacked_r_10", src2, 0, 0, false); + EvalRisc5 R5 = TestRisc5.build( src2, "stacked_r_10", 0, 0, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(1,R5.regs[riscv.A0]); @@ -525,27 +191,20 @@ return (0 < 1) == (1>0); @Test public void testStack11() throws IOException { - String stack11 = "1"; - String src = -""" -var stack11 = { int x -> - 0 < x < x+1 < 4; -}; -"""; - String src2 = -""" -return 0 < arg < arg+1 < 4; -"""; - TestC.run(src, "stacked_r_11x", null, stack11,2); + String src = "var stack11 = { int x -> 0 < x < x+1 < 4;}; "; + String src2 = "return 0 < arg < arg+1 < 4;"; + TestC.runC(src, "stacked_r_11x", "1", 2); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("stacked_r_11", src2, 1, 2, false); + EvalRisc5 R5 = TestRisc5.build( src2, "stacked_r_11", 1, 0, false); + R5.regs[riscv.A1] = 1; // Arg in A1, Test.class in A0 int trap = R5.step(100); assertEquals(0,trap); assertEquals(1,R5.regs[riscv.A0]); // Evaluate on ARM emulator EvalArm64 arm = TestArm64.build("stacked_r_11", src2, 1, 0, false); + arm.regs[1] = 1; // Arg in r1, Test.class in r0 trap = arm.step(100); assertEquals(0,trap); assertEquals(1,arm.regs[0]); @@ -555,21 +214,12 @@ return 0 < arg < arg+1 < 4; // ((0 != x) != 2) @Test public void testStack12() throws IOException { - String stack11 = "1"; - String src = - """ - var stack12 = { int x -> - 0 != x != 2; - }; - """; - String src2 = - """ - return 0 != arg != 2; - """; - TestC.run(src, "stacked_r_12x", null, stack11,0); + String src = "var stack12 = { int x -> 0 != x != 2; };"; + String src2 = "return 0 != arg != 2;"; + TestC.runC(src, "stacked_r_12x", "1",0); // Evaluate on RISC5 emulator - EvalRisc5 R5 = TestRisc5.build("stacked_r_12", src2, 1, 0, false); + EvalRisc5 R5 = TestRisc5.build( src2, "stacked_r_12", 1, 0, false); int trap = R5.step(100); assertEquals(0,trap); assertEquals(1,R5.regs[riscv.A0]); @@ -586,120 +236,28 @@ return 0 < arg < arg+1 < 4; // ((0 == x) == 1) @Test public void testStack13() throws IOException { - String stack12 = "1"; - String src = - """ - var stack13 = { int x -> - 0 == x == 1; - }; - """; - String src2 = - """ - return 0 == arg == 1; - """; - TestC.run(src, "stacked_r_13x", null, stack12,0); + String src = "var stack13 = { int x -> 0 == x == 1; };"; + String src2 = "return 0 == arg == 1;"; + TestC.runC(src, "stacked_r_13x", "1",0); // Compile once per target, then check false, true and negative inputs. - EvalRisc5 R5 = TestRisc5.build("stacked_r_13", src2, 1, 0, false); + EvalRisc5 R5 = TestRisc5.build( src2, "stacked_r_13", 1, 0, false); for( int arg : new int[]{-1,0,1} ) { R5._pc = 0; - R5.regs[riscv.A0] = arg; + R5.regs[riscv.A1] = arg; assertEquals(0,R5.step(100)); assertEquals(arg==0 ? 1 : 0,R5.regs[riscv.A0]); } EvalArm64 arm = TestArm64.build("stacked_r_13", src2, 1, 0, false); for( int arg : new int[]{-1,0,1} ) { arm._pc = 0; - arm.regs[0] = arg; + arm.regs[1] = arg; assertEquals(0,arm.step(100)); assertEquals(arg==0 ? 1 : 0,arm.regs[0]); } } @Test - public void testFRefFields() { - String src = -""" -// A hypothetical scanner class -struct Scan { - int !x; - u8[~] !buf; - // Skip whitespace - val skip = { !Scan s -> - while( s.buf[s.x] <= ' ' ) - s.x++; - }; - // Peek a character; if matched consume it, else false. - val peek = { !Scan !s, u8 c -> - skip(s); - if( s.buf[s.x] != c ) return false; - s.x++; - return true; - }; -}; -!Scan !s = new Scan{ buf = " q"; }; -return Scan.peek(s,'q'); -"""; - - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("return Phi(Region,0,1);", code._stop.toString()); - assertEquals("1", Eval2.eval(code, 0)); - testCPU(src,"x86_64_v2", "Win64" ,23,null); - }; - - @Test - public void testFRefFields2() { - String src = -""" -// A hypothetical scanner class -struct Scan { - int !x; - u8[~] !buf; - // Skip whitespace - val skip = { -> - while( buf[x] <= ' ' ) - x++; - return self; - }; -}; -~Scan s = new Scan{ buf = " q"; }; -return s.skip().x; -"""; - - try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { org.junit.Assert.assertTrue(e.getMessage().startsWith("Argument #0 isa")); } - }; - - - @Test - public void testMethod() { - String src = -""" -// A hypothetical scanner class -struct Scan { - int !x; - u8[~] !buf; - // Skip whitespace - val skip = { -> - while( buf[x] <= ' ' ) - x++; - }; - val require = { u8 ch -> - skip(); - buf[x++]==ch; - }; -}; -!Scan !s = new Scan{ buf = " [1,2]"; }; -return s.require('['); -"""; - - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("return (.[]==91);", code._stop.toString()); - assertEquals("1", Eval2.eval(code, 0)); - testCPU(src,"x86_64_v2", "Win64" ,4,null); - }; - - @Test public void testSCCP0() { String src = """ @@ -714,7 +272,7 @@ while (cnt) { return x; """; CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("return 1;", code._stop.toString()); + assertEquals("return 1;", code.print()); } @Test @@ -729,7 +287,7 @@ while( cnt-- ) return x; """; CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("return 1;", code._stop.toString()); + assertEquals("return 1;", code.print()); } } diff --git a/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java new file mode 100644 index 00000000..6b4a987d --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java @@ -0,0 +1,435 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.Type; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.type.Field; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeTuple; +import com.seaofnodes.simple.util.Ary; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runners.model.Statement; +import static org.junit.Assert.*; + +public class ClassInitDependenciesTest { + private final ArrayList<Path> dirs = new ArrayList<>(); + + @Rule public final TestRule cleanup = (test, description) -> new Statement() { + @Override public void evaluate() throws Throwable { + // Keep all fixtures if compilation or any later assertion fails. + test.evaluate(); + for( Path dir : dirs ) { + Path[] files; + try( var paths=Files.walk(dir) ) { + files=paths.sorted(Comparator.reverseOrder()).toArray(Path[]::new); + } + for( Path file : files ) Files.delete(file); + } + } + }; + + private Path tempDir(String prefix) throws Exception { + Path build=Path.of("build/init-deps"); + Files.createDirectories(build); + Path dir=Files.createTempDirectory(build,prefix); + dirs.add(dir); + return dir; + } + + private CodeGen compile(String entry, Map<String,String> sources, long seed) throws Exception { + Path root=tempDir("case-"); + Files.writeString(root.resolve("Root.smp"),"val entry="+entry+";"); + for( var source : sources.entrySet() ) { + Path path=root.resolve("Root/"+source.getKey()+".smp"); + Files.createDirectories(path.getParent()); + Files.writeString(path,source.getValue()); + } + return new CodeGen(root.toString(),root.resolve("obj").toString(), + "Root",null,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + } + + private void cycle(String entry, Map<String,String> sources, String expected) throws Exception { + expected="Root."+expected.replace(" -> "," -> Root."); + for( long seed : new long[]{1,123,987654321} ) { + try { compile(entry,sources,seed); fail("Expected initialization cycle"); } + catch( Parser.ParseException ex ) { + assertCycle(ex.getMessage(),expected); + } + } + } + + // The report lists internal SCC edges, including ordinary helper calls. + private HashMap<String,HashSet<String>> reportEdges(String message) { + String[] lines=message.split("\n"); + assertEquals(message,"Cyclic class initialization:",lines[0]); + HashMap<String,HashSet<String>> edges=new HashMap<>(); + for( int i=1; i<lines.length; i++ ) { + if( !lines[i].startsWith(" ") || lines[i].startsWith(" ") ) continue; // Diagnostic/source/caret. + String[] edge=lines[i].trim().split(" -> "); + assertEquals(message,2,edge.length); + edges.computeIfAbsent(edge[0],key -> new HashSet<>()).add(edge[1]); + } + return edges; + } + + // Follow helpers in the diagnostic, stopping at the next class initializer. + private boolean reportedTouch(HashMap<String,HashSet<String>> edges, String from, String to, HashSet<String> seen) { + if( !seen.add(from) || !edges.containsKey(from) ) return false; + for( String dst : edges.get(from) ) + if( dst.equals(to) || (dst.contains(" [fidx ") && reportedTouch(edges,dst,to,seen)) ) return true; + return false; + } + + private void assertCycle(String message, String expected) { + HashMap<String,HashSet<String>> edges=reportEdges(message); + String[] path=expected.split(" -> "); + for( int i=1; i<path.length; i++ ) + assertTrue("Missing "+path[i-1]+" -> "+path[i]+" in:\n"+message, + reportedTouch(edges,path[i-1],path[i],new HashSet<>())); + } + + // Follow ordinary helpers, stopping at the next initializer. This checks + // the class prerequisite relation without requiring a total init order. + private boolean touches(CodeGen code, ClassInitDependencies deps, String from, String to) { + int src=code.link(code._compunits.get(from)).sig().fidx(); + int dst=code.link(code._compunits.get(to)).sig().fidx(); + return touches(code,deps._edgess,src,dst,new BitSet()); + } + + private boolean touches(CodeGen code, int[][] edges, int src, int dst, BitSet seen) { + if( src>=edges.length || seen.get(src) || edges[src]==null ) return false; + seen.set(src); + for( int edge : edges[src] ) { + if( edge==dst ) return true; + FunNode fun=code.link(edge); + if( fun!=null && !fun.isClz() && touches(code,edges,edge,dst,seen) ) return true; + } + return false; + } + + @Test public void testEdgesAndPassiveReferences() throws Exception { + CodeGen code = compile("A",Map.of("A","int value=B.value;", "B","int value=arg|1;"),123); + ClassInitDependencies deps=new ClassInitDependencies(code); + assertTrue(touches(code,deps,"Root/A","Root/B")); + code=compile("A",Map.of("A","val b=B;", "B","val a=A;"),123); + deps=new ClassInitDependencies(code); + assertFalse(touches(code,deps,"Root/A","Root/B")); + assertFalse(touches(code,deps,"Root/B","Root/A")); + assertTrue(touches(code,deps,"Root/A","Root")); + assertTrue(touches(code,deps,"Root/B","Root")); + } + + @Test public void testCycles() throws Exception { + cycle("A",Map.of("A","val get_noInline={ ->\n B.value;\n};\nint value=get_noInline();", + "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","int value=B.value;", "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","val get={ -> B.value; }; int value=get();", + "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;", + "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"); + cycle("P",Map.of("P","int value=A.value;", "P/A","int value=arg;"),"P -> P.A -> P"); + } + + @Test public void testSiblingsAndNonreturningCall() throws Exception { + cycle("P",Map.of("P","val a=A; val b=B;", "P/A","int value=B.value;", + "P/B","int value=A.value;"),"P.A -> P.B -> P.A"); + cycle("A",Map.of("A","val get={ -> val x=B.value; while(1) {} return x; }; int value=get();", + "B","int value=A.value;"),"A -> B -> A"); + // An out-of-line nonreturning call retains its continuation's touches. + cycle("A",Map.of( + "A","int value=1; val stop_noInline={ -> while(1) {} return 1; }; stop_noInline(); return B.value;", + "B","int value=A.value;"),"A -> B -> A"); + String source="val stop_noInline={ -> while(1) {} return 1; }; stop_noInline(); return 2;"; + CodeGen code=new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + assertNull(Eval2.eval(code,0,10)); // Evaluation reaches its loop budget. + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) + new CodeGen(source).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + + @Test public void testOptimizedDependencies() throws Exception { + for( long seed : new long[]{1,123,987654321} ) { + // SCCP learns this condition from a call; the parser cannot decide it. + CodeGen code=compile("A",Map.of( + "A","val test_noInline={ -> false; }; int value=1; if(test_noInline()) value=B.value;", + "B","int value=A.value;"),seed); + ClassInitDependencies deps=new ClassInitDependencies(code); + assertFalse(touches(code,deps,"Root/A","Root/B")); + assertTrue(touches(code,deps,"Root/B","Root/A")); + + code=compile("A",Map.of( + "A","val test_noInline={ -> false; }; val f=test_noInline() ? { -> B.value; } : { -> 1; }; int value=f();", + "B","int value=A.value;"),seed); + assertFalse(touches(code,new ClassInitDependencies(code),"Root/A","Root/B")); + + // An unused helper's body must not become an initializer dependency. + code=compile("A",Map.of("A","val unused={ -> B.value; }; int value=1;", + "B","int value=A.value;"),seed); + assertFalse(touches(code,new ClassInitDependencies(code),"Root/A","Root/B")); + } + // Discarding the value of a field read cannot discard its active touch. + cycle("A",Map.of("A","int value=1; B.value; return 0;", + "B","int value=2; A.value; return 0;"),"A -> B -> A"); + } + + @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);", + "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); + + } + + private void object(Path dir, Path unused, String name, String source) throws Exception { + Path file=dir.resolve("Root/"+name+".smp"); + Files.createDirectories(file.getParent()); + Files.writeString(file,source); + } + + private void module(Path dir, Path unused, String name, Map<String,String> sources) throws Exception { + for( var entry : sources.entrySet() ) object(dir,dir,entry.getKey(),entry.getValue()); + } + + private CodeGen entry(Path dir, String source, long seed) throws Exception { + Files.writeString(dir.resolve("Root.smp"),""); + object(dir,dir,"Main",source); + return new CodeGen(dir.toString(),dir.toString(),"Root/Main",null,seed,true,TypeInteger.BOT); + } + + private CodeGen emit(Path dir, String name, String source) throws Exception { + return entry(dir,source,123).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + } + + @Test public void testCycleReportDirection() throws Exception { + cycle("A",Map.of("A","int value=B.value;", "B","int value=C.value;", + "C","int value=A.value;"),"A -> B -> C -> A"); + for( long seed : new long[]{1,123,987654321} ) { + // Stack order A,B,C is not a cycle: B has no edge to C. All four + // internal edges must be shown; edges to Root/D must be omitted. + try { + compile("A",Map.of("A","int value=B.value+C.value+D.value;", + "B","int value=A.value;", "C","int value=A.value;", + "D","int value=arg;"),seed); + fail("Expected initialization cycle"); + } catch( Parser.ParseException ex ) { + assertEquals(Map.of("Root.A",Set.of("Root.B","Root.C"), + "Root.B",Set.of("Root.A"), "Root.C",Set.of("Root.A")), + reportEdges(ex.getMessage())); + } + } + } + + @Test public void testCycleSourceLocations() throws Exception { + for( String name : new String[]{"read", "read_noInline"} ) { + try { + compile("A",Map.of("A","int value=Helper."+name+"();", + "Helper","val "+name+"={ ->\n B.value;\n};", + "B","int value=A.value;"),123); + fail("Expected initialization cycle"); + } catch( Parser.ParseException ex ) { + 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 ---^")); + if( name.endsWith("noInline") ) { + assertTrue(message,message.contains("A.smp:1:31")); + assertTrue(message,message.contains("int value=Helper.read_noInline();")); + } + } + } + } + + @Test public void testSourceError() { + String src="// π\r\n\treturn 1;\r\n"; + 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:3:1: error: example\n \n ^",SourceError.format(cu,src.getBytes(java.nio.charset.StandardCharsets.UTF_8).length,"example")); + } + + @Test public void testSelfInitialization() throws Exception { + for( String name : new String[]{"read","read_noInline"} ) + for( long seed : new long[]{1,123,987654321} ) + compile("A",Map.of("A","int value=arg; val "+name+"={ -> A.value; }; int result="+name+"();"),seed); + } + + @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[] counts={1,2,2}; + for( long seed : new long[]{1,123,987654321} ) + for( int i=0; i<sources.length; i++ ) { + CodeGen code=compile("A",Map.of("A",sources[i],"B","int value=arg;"),seed); + FunNode fun=code.link(code._compunits.get("Root/A")); + CompUnit b=code._compunits.get("Root/B"); + int[] touches={0}; + code._stop.walk(n -> { + if( n instanceof CLInitNode init && init.fun()==fun && init.cu()==b ) { + touches[0]++; + TypeTuple state=(TypeTuple)init._type; + assertSame(TypeMem.BOT,state._types[1]); + TypeMem flag=(TypeMem)state._types[2]; + assertEquals(b._clz.field("$init")._alias,flag._alias); + assertSame(TypeInteger.TRUE,flag._t); + assertNotNull(init.ready()); + } + return null; + }); + assertEquals(sources[i],counts[i],touches[0]); + } + } + + @Test public void testForwardInstanceTouch() { + String src=""" + struct S { int x=7; }; + val _get={ -> _make().x; }; + val _make={ -> new S; }; + return _get(); + """; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.Parse); + CLInitNode init=code._stop.walk(n -> n instanceof CLInitNode c ? c : null); + assertNotNull("The unresolved receiver needs a speculative check",init); + assertNotNull("The readiness load is present from parsing",init.ready()); + code.driver(CodeGen.Phase.TypeCheck); + assertNull("An instance needs neither CLInit nor its speculative $init load", + code._stop.walk(n -> n instanceof CLInitNode || + (n instanceof LoadNode ld && ld._name.equals("$init")) ? n : null)); + assertEquals("7",Eval2.eval(code,0)); + } + } + + @Test public void testInitializerFlagStore() throws Exception { + // A same-named struct must retain the file class's hidden flag. + CodeGen code=compile("A",Map.of("A","struct A { int value; }; int value=arg;"),123); + CompUnit a=code._compunits.get("Root/A"); + Field flag=a._clz.field("$init"); + assertNotNull(flag); + assertFalse(flag._final); + FunNode fun=code.link(a); + int[] stores={0}; + code._stop.walk(n -> { + if( n instanceof StoreNode st && st._name.equals("$init") && st._alias==flag._alias ) { + stores[0]++; + assertSame(TypeInteger.TRUE,st.val()._type); + assertSame(fun,CFGNode.earlyCFG(st).fun()); + } + return null; + }); + assertEquals(1,stores[0]); + } + + @Test public void testInitializationExecution() throws Exception { + Path dir=tempDir("execution-"); + object(dir,dir,"State","int count=0;"); + object(dir,dir,"B","State.count++; int value=17+arg;"); + String src="int value=arg ? B.value+B.value : 0; return value+100*State.count;"; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=entry(dir,src,seed).driver(CodeGen.Phase.TypeCheck); + assertNotNull(code._stop.walk(n -> n instanceof CLInitNode ? n : null)); + assertEquals("0",Eval2.eval(code,0)); + assertNull(code._stop.walk(n -> n instanceof CLInitNode ? n : null)); + assertEquals("134",Eval2.eval(code,1)); + } + + emit(dir,"Main","val run={int take -> "+src.replace("arg ?","take ?")+" };"); + Path cfile=dir.resolve("check.c"); + Files.writeString(cfile,""" + #include <stdint.h> + __attribute__((CALL_CONV)) extern int64_t run(int64_t take) __asm__("Root.Main.run"); + int main(void) { + return run(0)!=0 || run(1)!=134 || run(1)!=134 || run(0)!=100; + } + """); + String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "check.exe" : "check").toString(); + TestC.linkExe(dir.resolve("Root/Main.o").toString(),TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", + cfile.toString(),null,exe); + assertEquals("",TestC.exec(exe)); + for( String cpu : new String[]{"arm","riscv"} ) + entry(dir,src,123).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + + @Test public void testParentInitializationExecution() throws Exception { + Path dir=tempDir("execution-"); + object(dir,dir,"State","int count=0;"); + module(dir,dir,"P",Map.of("P","val C=C; State.count=State.count*10+1;", + "P/C","State.count=State.count*10+2; int value=arg;")); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=entry(dir,"return P.C.value+State.count;",seed).driver(CodeGen.Phase.TypeCheck); + assertEquals("12",Eval2.eval(code,99)); + } + emit(dir,"Main","val run={ -> P.C.value+State.count; };"); + Path cfile=dir.resolve("check.c"); + Files.writeString(cfile,""" + #include <stdint.h> + __attribute__((CALL_CONV)) extern int64_t run(void) __asm__("Root.Main.run"); + int main(void) { return run()!=12 || run()!=12; } + """); + String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "check.exe" : "check").toString(); + TestC.linkExe(dir.resolve("Root/Main.o").toString(),TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", + cfile.toString(),null,exe); + assertEquals("",TestC.exec(exe)); + } + + @Test public void testNonreturningObject() throws Exception { + Path dir=tempDir("nonreturning-"); + object(dir,dir,"Spin","val spin_noInline={ -> while(1) {} return 7; };"); + CodeGen code=entry(dir,"Spin.spin_noInline(); return 7;",123).driver(CodeGen.Phase.TypeCheck); + assertNull(Eval2.eval(code,0,10)); + } + + + @Test public void testOverlappingFunctionAndClassCycles() throws Exception { + // Fixed function graph: Opto/inlining must not erase the overlap that + // exercises SCC traversal. One clinit is legal, two in the SCC are not. + CodeGen code=compile("A",Map.of("A","val b=B; int value=arg;", "B","int value=arg;"),123); + int f=code._linker._len, g=f+1, a=f+2, b=f+3; + String[] names={"f", "g", "A.<clinit>", "B.<clinit>"}; + for( int i=0; i<names.length; i++ ) { + TypeFunPtr sig=TypeFunPtr.make1((byte)2,false,new Type[0],TypeInteger.BOT,i+f); + CompUnit cu=code._compunits.get(i==3 ? "Root/B" : "Root/A"); + FunNode fun=new FunNode(null,sig,names[i],cu); + fun._type=Type.CONTROL; + code._linker.setX(i+f,fun); + } + ClassInitDependencies deps=new ClassInitDependencies(code); + deps._edgess[f]=new int[]{g}; // f -> g + deps._edgess[g]=new int[]{f}; // g -> f + assertNull("Ordinary mutual recursion is legal",deps.check()); + deps._edgess[a]=new int[]{a}; // A -> A + assertNull("A singleton initializer self-edge is legal",deps.check()); + deps._edgess[a]=new int[]{g}; // A -> g + deps._edgess[f]=new int[]{g,a}; + assertNull("A cycle containing only one initializer is legal",deps.check()); + deps._edgess[a]=new int[]{b}; // A -> B + deps._edgess[b]=new int[]{g}; // B -> g -> f -> A + String forward=deps.check(); + deps._edgess[f]=new int[]{a,g}; + String reverse=deps.check(); + assertTrue("Both successor orders must reject the class cycle: "+forward+", "+reverse, + forward!=null && reverse!=null); + assertCycle(forward,"Root.A -> Root.B -> Root.A"); + assertCycle(reverse,"Root.A -> Root.B -> Root.A"); + } +} diff --git a/src/test/java/com/seaofnodes/simple/Compile.java b/src/test/java/com/seaofnodes/simple/Compile.java deleted file mode 100644 index f3ca1905..00000000 --- a/src/test/java/com/seaofnodes/simple/Compile.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.node.FunNode; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import org.junit.Ignore; -import org.junit.Test; -import static org.junit.Assert.*; - -public class Compile { - - private static Process run(String simple, String c) throws IOException, InterruptedException { - boolean USE_WSL = System.getProperty("os.name").startsWith("Windows"); - CodeGen code = new CodeGen(simple); - code.driver(CodeGen.Phase.Encoding,"x86_64_v2","SystemV"); - if (c != null) for (var n:code._start._outputs) if (n instanceof FunNode f && f._name.equals("main")) f._name = "simple_main"; - code.exportELF("test.o"); - var params = new ArrayList<String>(); - if (USE_WSL) params.add("wsl.exe"); - params.add("gcc"); - params.add("-o"); - params.add("test"); - if (c != null) { - Files.writeString(Path.of("test.c"), c); - params.add("test.c"); - } - params.add("test.o"); - Process p = Runtime.getRuntime().exec(params.toArray(String[]::new)); - assertEquals(0, p.waitFor()); - return Runtime.getRuntime().exec(USE_WSL ? new String[]{"wsl.exe", "./test"}:new String[]{"./test"}); - } - - @Test - @Ignore - public void testCompile() throws IOException, InterruptedException { - var p = run(""" - val testMy = { flt x -> - flt guess = x; - while( 1 ) { - flt next = (x/guess + guess)/2; - if( next == guess ) return guess; - guess = next; - } - }; - """,""" - - #include <stdio.h> - - extern double testMy(double); - - int main(int argc, char** argv) { - printf("%f\\n", testMy(2.0)); - return 0; - } - - """); - assertEquals(0, p.waitFor()); - assertEquals("1.414214\n", new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8)); - } - -} diff --git a/src/test/java/com/seaofnodes/simple/ConstructionTest.java b/src/test/java/com/seaofnodes/simple/ConstructionTest.java deleted file mode 100644 index dd20b746..00000000 --- a/src/test/java/com/seaofnodes/simple/ConstructionTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.isa.eval.EvalArm64; -import com.seaofnodes.isa.eval.EvalRisc5; -import org.junit.Test; -import static org.junit.Assert.*; - -public class ConstructionTest { - private static final String CONSTRUCT = """ - struct Box { - int x=7; - int[] data; - new Box = { int n -> - data=new int[2]; - x=n; - data[0]=x+1; - }; - val get = { -> x+data[0]; }; - }; - val a=new Box(arg); - val b=new Box(arg+10); - return a.get()*100+b.get(); - """; - - @Test public void testConstructorCalls() throws java.io.IOException { - var code=new CodeGen(CONSTRUCT).driver(CodeGen.Phase.FinalizeIR); - assertEquals("525",Eval2.eval(code,2)); - for( String cpu : new String[]{"arm","riscv"} ) { - code=new CodeGen(CONSTRUCT).driver(cpu,"SystemV",null); - byte[] memory=new byte[1<<20]; - byte[] bits=code._encoding.bits(); - System.arraycopy(bits,0,memory,0,bits.length); - if( cpu.equals("arm") ) { - EvalArm64 eval=new EvalArm64(memory,1<<16); - eval.regs[0]=2; - assertEquals(0,eval.step(10000)); - assertEquals(525,eval.regs[0]); - } else { - EvalRisc5 eval=new EvalRisc5(memory,1<<16); - eval.regs[10]=2; - assertEquals(0,eval.step(10000)); - assertEquals(525,eval.regs[10]); - } - } - // Field updates are local SSA values, including the old x used after - // updating y and x. Only constructor exits should write these fields. - String counter="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;"; - var loop=new CodeGen(counter) - .driver(CodeGen.Phase.LocalSched); - assertEquals("0",Eval2.eval(loop,0)); - assertEquals("2018",Eval2.eval(loop,19)); - loop._stop.walk(n -> { - if( n instanceof com.seaofnodes.simple.node.StoreNode st ) - assertEquals("Constructor Stores belong outside the loop",0,st.cfg0().loopDepth()); - return null; - }); - var defaults=new CodeGen("struct Buffer { int[] data=new int[1]; }; val a=new Buffer; val b=new Buffer; a.data[0]=7; return b.data[0];") - .driver(CodeGen.Phase.TypeCheck); - assertEquals("0",Eval2.eval(defaults,0)); - java.nio.file.Path dir=java.nio.file.Path.of("build/objs"); - java.nio.file.Files.createDirectories(dir); - java.nio.file.Files.writeString(dir.resolve("construction.smp"),"val construct={int arg ->"+CONSTRUCT+"}; val count={int arg ->"+counter+"};"); - java.nio.file.Files.writeString(dir.resolve("construction.c"),""" - extern long long construct(long long); - extern long long count(long long); - int main(void) { return construct(2)!=525 || construct(0)!=121 || count(19)!=2018 || count(0)!=0; } - """); - TestC.run("build/objs","construction","",-1); - } - - @Test public void testCompletionBeforePublication() { - String src="struct Box { int[] data; new Box={int n -> if(n) data=new int[2];}; }; return new Box(arg);"; - try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Parser.ParseException e ) { - assertEquals("'Box' is not fully initialized, field 'data' needs to be set in a constructor",e.getMessage()); - } - for( String early : new String[]{ - "val use={!Box b -> b.x;}; struct Box { int x; new Box={->use(self);}; }; return new Box;", - "struct Box { Box? next; new Box={->next=self;}; }; return new Box;", - "struct Wrap { Box? item; }; struct Box { new Box={->new Wrap { item=self; };}; }; return new Box;" - } ) { - try { new CodeGen(early).driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Parser.ParseException e ) { - assertEquals("Cannot publish self before the constructor finishes",e.getMessage()); - } - } - } - - @Test public void testForwardMethod() { - var code=new CodeGen("val run={!Box b -> b.get();}; struct Box { int x=7; val get={->x;}; }; return run(new Box);") - .driver(CodeGen.Phase.FinalizeIR); - assertEquals("7",Eval2.eval(code,0)); - code=new CodeGen("struct Byte { new Byte={int n -> self.x=n; self.x+=3;}; u8 x; }; return new Byte(255).x;") - .driver(CodeGen.Phase.FinalizeIR); - assertEquals("2",Eval2.eval(code,0)); - } -} diff --git a/src/test/java/com/seaofnodes/simple/EscapeTest.java b/src/test/java/com/seaofnodes/simple/EscapeTest.java deleted file mode 100644 index 6d778350..00000000 --- a/src/test/java/com/seaofnodes/simple/EscapeTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.*; -import org.junit.Test; -import static org.junit.Assert.*; - -public class EscapeTest { - private static final String POWER = """ - val power = { int base, int exponent -> - if (exponent <= 0) return 1; - int smaller=power(base,exponent-1); - return base * smaller; - }; - """; - private static final String SUM = """ - val sumPowers = { int n, {int int -> int} f -> - if (n <= 0) return 0; - return f(2,n) + sumPowers(n-1,f); - }; - """; - private static ParmNode base(CodeGen code) { - ParmNode parm=code._start.walk(n -> n instanceof ParmNode p && p._label.equals("base") ? p : null); - assertNotNull(parm); - return parm; - } - private static CodeGen analyze(String src, long seed) { - return new CodeGen(src,TypeInteger.BOT,seed,true).driver(CodeGen.Phase.Opto); - } - - @Test public void testKnownRecursiveCallback() { - String src=POWER+SUM+"return sumPowers(arg,power);"; - for( long seed : new long[]{2,13,123} ) { - CodeGen code=analyze(src,seed); - ParmNode base=base(code); - assertSame(TypeInteger.constant(2),base._type); - assertFalse(base.fun().unknownCallers()); - assertNotNull(code._start.walk(n -> n instanceof CallNode c && "power".equals(c.name()) ? c : null)); - code.driver(CodeGen.Phase.TypeCheck); - assertEquals("14",Eval2.eval(code,3)); - } - } - - @Test public void testReturnedAddresses() { - // Returning the address, or a chain of objects containing it, restores - // arbitrary callers even when every visible call passes base=2. - String holder="struct Holder { {int int -> int} f; }; val h=new Holder { f=power; };"; - for( String tail : new String[]{ - "return power;", - holder+"return h;", - holder+"struct Wrap { Holder child; }; return new Wrap { child=h; };", - "struct Cell { {int int -> int}? !f; }; val h=new Cell; while(arg--) h.f=power; return h;" - } ) { - CodeGen code=analyze(POWER+SUM+"sumPowers(arg,power);"+tail,123); - ParmNode base=base(code); - assertSame(TypeInteger.BOT,base._type); - assertTrue(base.fun().unknownCallers()); - assertTrue(code._start.escapedFIDX(base.fun().sig().fidx())); - code.driver(CodeGen.Phase.TypeCheck); - } - } - - @Test public void testNativeExposure() { - for( String tail : new String[]{ - "retain(power); return sumPowers(arg,power);", - "if(arg) { retain(power); while(true) {} } return sumPowers(arg,power);" - } ) { - String src=POWER+SUM+"{ {int int -> int} -> int } retain=\"C\";"+tail; - CodeGen code=analyze(src,123); - ParmNode base=base(code); - assertSame(TypeInteger.BOT,base._type); - assertTrue(base.fun().unknownCallers()); - code.driver(CodeGen.Phase.TypeCheck); - } - // An external caller can inspect a field written through its argument, - // even though the exposed function itself returns only an integer. - String src="struct Holder { {int int -> int}? !f; };"+POWER+ - "val expose={!Holder h -> h.f=power; return 0;}; return expose;"; - CodeGen code=analyze(src,123); - assertTrue(base(code).fun().unknownCallers()); - code.driver(CodeGen.Phase.TypeCheck); - } -} diff --git a/src/test/java/com/seaofnodes/simple/Eval2.java b/src/test/java/com/seaofnodes/simple/Eval2.java index b32a5175..366bd7f4 100644 --- a/src/test/java/com/seaofnodes/simple/Eval2.java +++ b/src/test/java/com/seaofnodes/simple/Eval2.java @@ -114,14 +114,18 @@ public abstract class Eval2 { // Current active Frame, to avoid having to pass it everywhere public static Frame F; + // Class pointers and initializer self parameters share one object per run. + private static final HashMap<String,Object[]> CLASSES = new HashMap<>(); + public static String eval( CodeGen code, long arg ) { return eval(code,arg,1000); } public static String eval( CodeGen code, long arg, int timeout ) { if( code._start.uctrl()==null ) return ""; // The empty program - SB trace = null; // = new SB(); // TRACE, set to null for off, new SB() for on + SB trace = null; // new SB(); // TRACE, set to null for off, new SB() for on // Force local scheduling phase code.driver(CodeGen.Phase.LocalSched); // Set global, so don't have to pass everywhere CODE = code; + CLASSES.clear(); // Timer for when we run too long. Ticks once per backedge or function call int loopCnt = 0; @@ -134,13 +138,21 @@ public abstract class Eval2 { CFGNode BB = code._start, prior = null; // Return from main hits Stop F.put(BB,new Closure(code._stop,F)); + // For all <clinit> / Clazzes, install their uninitialized memory + for( FunNode fun : code._linker ) + if( fun!=null && fun.isClz() ) { + TypeStruct clz = ((TypeMemPtr)fun.sig().arg(0))._obj; + // Map from the <clinit> self parameter to the + // singleton class structure. + F.put(fun.parm(2),classObject(clz)); + } // ------------ // Evaluate until exit or timeout. Each outer loop step computes // all data nodes under some new Control. while( true ) { - assert BB!=null; + if( BB==null ) return null; traceCtrl(BB,trace); if( trace!=null ) System.out.println(F.p()); @@ -170,6 +182,7 @@ public abstract class Eval2 { case RegionNode r : break; default: throw Utils.TODO(); } + if( BB==null ) return null; assert !BB.isDead(); } } @@ -183,28 +196,37 @@ public abstract class Eval2 { int path = r._inputs.find( prior ); // Parameters read from prior frame, Phis from local frame Frame frame = (r instanceof FunNode ? F._prior : F); - boolean isMain = r instanceof FunNode fun && fun.sig().isa(CodeGen.CODE._main); + boolean isClz = r instanceof FunNode fun && fun.isClz(); // Parallel assign Phis. First parallel read and cache int i; for( i = 0; i < r.nOuts(); i++ ) { + // Due to scheduling, all Phis are first in the outputs; first + // non-Phi is the end of all Phis. if( !(r.out(i) instanceof PhiNode phi) ) break; - if( isMain && phi instanceof ParmNode parm && parm._idx==2 ) - PHICACHE[i] = arg; // Reading the initial arguments to main() + + // Parms read from Call args - + // - except RPC is the actual prior frame + // - except entry <clinit> takes the external program argument + Object val; + if( isClz && prior instanceof StartNode && phi instanceof ParmNode parm && parm._idx==3 ) + PHICACHE[i] = arg; // Reading the initial arguments to <clinit> else { Node n = phi instanceof ParmNode parm - // RPC reads the Call directly; + // RPC uses the prior Call directly; // Parms may not be linked, so read call args directly ? (parm._idx==0 ? prior : prior.in(parm._idx)) // Phis read from path input : phi.in(path); + // Read the frame before overwriting PHICACHE[i] = frame.get(n); } } // Parallel assign; might assign before read, so read from cache for( int j=0; j < i; j++ ) traceData(F.put0(r.out(j),PHICACHE[j]),trace); - // Return point in basic block past last Phi + // Return point in basic block past last Phi; the Phis have been + // evaluated here, and now we need to evaluate the rest of the block return i; } @@ -253,38 +275,42 @@ public abstract class Eval2 { // From here down shamelessly copied from Evaluator, written by @Xmilia private static Object compute( Node n ) { return switch( n ) { - case AddFNode adf -> d(adf.in(1)) + d(adf.in(2)); - case AddNode add -> add.mode()==2 ? (Object)(d(add.in(1)) + d(add.in(2))) : (Object)(x(add.in(1)) + x(add.in(2))); + case AddNode add -> add.mode()==1 + ? (Object)(x(add.in(1)) + x(add.in(2))) + : (Object)(d(add.in(1)) + d(add.in(2))); case AndNode and -> x(and.in(1)) & x(and.in(2)); - case BoolNode.EQF eqf -> d(eqf.in(1)) == d(eqf.in(2)) ? 1L : 0L; - case BoolNode.LEF lef -> d(lef.in(1)) <= d(lef.in(2)) ? 1L : 0L; - case BoolNode.LTF ltf -> d(ltf.in(1)) < d(ltf.in(2)) ? 1L : 0L; case BoolNode.EQ eq -> Objects.equals(val(eq.in(1)), val(eq.in(2))) ? 1L : 0L; // Bool EQ supports pointers, nil, and integers - case BoolNode.LE le -> (le.isFloat() ? d(le.in(1))<=d(le.in(2)) : x(le.in(1))<=x(le.in(2))) ? 1L : 0L; - case BoolNode.LT lt -> (lt.isFloat() ? d(lt.in(1))<d(lt.in(2)) : x(lt.in(1))<x(lt.in(2))) ? 1L : 0L; - case CastNode cast -> val(cast.in(1)); - case GuardNode guard -> val(guard.in(1)); + case BoolNode.LE le -> le.mode()==1 + ? (x(le.in(1)) <= x(le.in(2)) ? 1L : 0L) + : (d(le.in(1)) <= d(le.in(2)) ? 1L : 0L); + case BoolNode.LT lt -> lt.mode()==1 + ? (x(lt.in(1)) < x(lt.in(2)) ? 1L : 0L) + : (d(lt.in(1)) < d(lt.in(2)) ? 1L : 0L); + case GuardNode guard -> val(guard.in(1)); case ConstantNode con -> con(con._con); - case DivFNode dvf -> d(dvf.in(2))==0 ? 0D : d(dvf.in(1)) / d(dvf.in(2)); - case DivNode div -> div.mode()==2 ? (Object)(d(div.in(2))==0 ? 0D : d(div.in(1))/d(div.in(2))) : (Object)(x(div.in(2))==0 ? 0L : x(div.in(1))/x(div.in(2))); + case DivNode div -> div.mode()==1 + ? (Object)(x(div.in(2))==0 ? 0L : x(div.in(1)) / x(div.in(2))) + : (Object)((d(div.in(2))==0 ? 0D : d(div.in(1))) / d(div.in(2))); + case EscapeNode esc -> esc(esc); + case FunPtrNode fptr -> con(fptr._con); case LoadNode ld -> load(ld); - case MinusFNode mnf -> - d(mnf.in(1)); - case MinusNode sub -> sub.mode()==2 ? (Object)(-d(sub.in(1))) : (Object)(-x(sub.in(1))); - case MulFNode mlf -> d(mlf.in(1)) * d(mlf.in(2)); - case MulNode mul -> mul.mode()==2 ? (Object)(d(mul.in(1)) * d(mul.in(2))) : (Object)(x(mul.in(1)) * x(mul.in(2))); + case MemMergeNode merge-> "$mem"; + case MinusNode sub -> sub.mode()==1 ? (Object)(- x(sub.in(1))) : (Object)(- d(sub.in(1))); + case MulNode mul -> mul.mode()==1 + ? (Object)(x(mul.in(1)) * x(mul.in(2))) + : (Object)(d(mul.in(1)) * d(mul.in(2))); case NewNode alloc-> alloc(alloc); case NotNode not -> not(not); case OrNode or -> x(or .in(1)) | x(or .in(2)); case ProjNode proj -> proj._type instanceof TypeMem ? "$mem" : val(proj.in(0)); case ReadOnlyNode read -> val(read.in(1)); case SarNode sar -> x(sar.in(1)) >> x(sar.in(2)); - case EscapeNode esc -> "$mem"; - case MemMergeNode merge-> "$mem"; case ShlNode shl -> x(shl.in(1)) << x(shl.in(2)); case ShrNode shr -> x(shr.in(1)) >>>x(shr.in(2)); case StoreNode st -> store(st); - case SubNode sub -> sub.mode()==2 ? (Object)(d(sub.in(1)) - d(sub.in(2))) : (Object)(x(sub.in(1)) - x(sub.in(2))); - case SubFNode sbf -> d(sbf.in(1)) - d(sbf.in(2)); + case SubNode sub -> sub.mode()==1 + ? (Object)(x(sub.in(1)) - x(sub.in(2))) + : (Object)(d(sub.in(1)) - d(sub.in(2))); case RoundF32Node rnd -> (double)(float)d(rnd.in(1)); case ToIntegerNode cvt -> (long)d(cvt.in(1)); case ToFloatNode toflt-> (double)x(toflt.in(1)); @@ -293,12 +319,6 @@ public abstract class Eval2 { }; } - private static Object not( NotNode not ) { - Object n = val(not.in(1)); - return ( n==null || (n instanceof Double D && D==0.0) || n instanceof Long L && L==0 ) - ? 1L : 0L; - } - // Fetch without unboxing, searching up-Frame static Object val( Node n ) { return F.get(n); } // Fetch and unbox as primitive long @@ -306,7 +326,9 @@ public abstract class Eval2 { // Fetch and unbox as primitive double static double d( Node n ) { Object d = F.get(n); return d==null ? 0 : (Double)d; } // Fetch and unbox a function constant - static TypeFunPtr tfp(Node n) { return (TypeFunPtr)F.get(n); } + static TypeFunPtr tfp(Node n) { + return (TypeFunPtr)F.get(n); + } // Fetch and unbox a closure static Closure clj(Node n) { return (Closure)F.get(n); } @@ -318,8 +340,6 @@ public abstract class Eval2 { case TypeFunPtr tfp -> tfp; case TypeMem mem -> "MEM"; case TypeMemPtr tmp -> { - // Unknown-caller inputs are type placeholders, not heap constants. - if( !tmp.isConstant() ) yield tmp; if( tmp._obj.isAry() ) { // Constant array ptr Type elem = tmp._obj.field("[]")._t; if( elem instanceof TypeConAry con ) { @@ -333,17 +353,36 @@ public abstract class Eval2 { yield new Object[0]; } + } else if( Parser.startsClzPrefix(tmp._obj._name) ) { + yield classObject(tmp._obj); } else { - // Generic TMP (since Simple is not currently making actual - // memory constants), used as a default input to a function; - // should never execute. - yield tmp.toString(); + // Constant object with all zeros + yield alloc(tmp._obj); } } default -> null; }; } + // Escapes of constant memory actually needs to set the memory + private static Object esc(EscapeNode esc) { + if( esc.priv() instanceof ConstantNode con ) { + TypeMem mem = (TypeMem)con._type; + Object val = con(mem._t); + TypeMemPtr tmp = (TypeMemPtr)esc.self()._type; + int idx = tmp._obj.findAlias(esc.fld()._alias); + Object[] fs = (Object[])val(esc.self()); + if( tmp._obj.isAry() ) { + assert idx==0; // length field + assert (Long)val==fs.length; + } else { + fs[idx] = val; + } + } + return "$mem"; + } + + // Convert array size to array element count private static int offToIdx( long off, TypeStruct t) { off -= t.aryBase(); @@ -353,10 +392,10 @@ public abstract class Eval2 { } // Builds and returns a pointer Object - private static Object alloc(NewNode alloc) { - TypeStruct type = alloc._ptr._obj; + private static Object[] alloc(NewNode alloc) { + TypeStruct type = alloc._ts; if( type.isAry() ) { - long sz = (Long)val(alloc.size()); + long sz = (Long)val(alloc.in(1)); long x = offToIdx(sz, type); int n = (int)x; if( n!=x || n<0 ) @@ -373,6 +412,10 @@ public abstract class Eval2 { return ary; } + return alloc(type); + } + private static Object[] alloc(TypeStruct type) { + assert !type.isAry(); int num = type._fields.length; Object[] ptr = new Object[num]; for( int i=0; i<num; i++ ) @@ -380,15 +423,22 @@ public abstract class Eval2 { return ptr; } + private static Object[] classObject(TypeStruct type) { + Object[] ptr = CLASSES.get(type._name); + if( ptr==null ) CLASSES.put(type._name,ptr=alloc(type)); + return ptr; + } + + private static Object not( NotNode not ) { + Object n = val(not.in(1)); + return ( n==null || (n instanceof Double D && D==0.0) || n instanceof Long L && L==0 ) + ? 1L : 0L; + } + private static Object load( LoadNode ld ) { Object f = val(ld.ptr()); // Check for dense constant array if( f instanceof TypeMemPtr tmp ) { - //assert tmp._obj._con != TypeConAry.BOT; - //if( ld._name.equals("#") ) - // return (long)tmp._obj._con.len(); - //int idx = offToIdx(x(ld.off()),tmp._obj); - //return tmp._obj._con.at(idx); throw Utils.TODO(); } Object[] fs = (Object[])f; @@ -447,12 +497,11 @@ public abstract class Eval2 { case TypeMemPtr tmp -> { if( visit.containsKey(x) ) yield sb.p("$cyclic"); visit.put(x,x); - assert !tmp.isFRef(); Object[] xs = (Object[])x; // Array of fields if( tmp._obj.isAry() ) { Type elem = tmp._obj._fields[1]._t; - if( elem == TypeInteger.U8 ) { + if( elem.isa(TypeInteger.U8) ) { // Shortcut u8[] as a String for( Object o : xs ) sb.p((char)(long)(Long)o); @@ -477,9 +526,9 @@ public abstract class Eval2 { case TypeRPC rpc -> sb.p(x.toString()); case TypeMem mem -> sb.p("$mem"); case TypeTuple tt -> { - if( tt._types.length>1 && tt._types[0] instanceof TypeMemPtr ) + if( tt._types.length>1 && tt._types[1] instanceof TypeMemPtr ) // Assume a NewNode - yield _print( tt._types[0], x, sb, visit ); + yield _print( tt._types[1], x, sb, visit ); throw Utils.TODO(); } case Type tt -> { diff --git a/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java index e91dc384..f583e2d2 100644 --- a/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java +++ b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java @@ -58,6 +58,8 @@ public class FinalConstructionTest { extern long long buildFinal(long long); int main(void) { return buildFinal(0)!=1 || buildFinal(3)!=15 || buildFinal(5)!=63; } """); - TestC.run("build/objs","finalConstruction","",-1); + TestC.run("val buildFinal={int arg ->"+LOOP+"};","finalConstruction",null,TestC.CALL_CONVENTION, + TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", + dir.resolve("finalConstruction.c").toString(),"",-1); } } diff --git a/src/test/java/com/seaofnodes/simple/FuzzerWrap.java b/src/test/java/com/seaofnodes/simple/FuzzerWrap.java index b022bec9..68bbe918 100644 --- a/src/test/java/com/seaofnodes/simple/FuzzerWrap.java +++ b/src/test/java/com/seaofnodes/simple/FuzzerWrap.java @@ -7,19 +7,37 @@ import org.junit.Test; import static org.junit.Assert.assertTrue; /** - * Normal tests run chapter-local regression seeds. Open failures and exploratory - * fuzzing are opt-in (run directly in the IDE or temporarily remove Ignore). - * Seeds depend on this chapter's generator; do not copy them between chapters. + * To use the fuzzer run the <code>fuzzPeeps</code> methods. + * This can be done from IntelliJ Gui or remove the `@Ignore` annotation and start the test case from the command line + * via <code>mvn clean test -Dtest=com.seaofnodes.simple.Fuzzer08Test#fuzzPeeps</code> */ public class FuzzerWrap { private static final long[] REGRESSION_SEEDS = { + 375135762521757909L, // bad LCA + -148471672577312953L, // bulk mem + -5037182906211190034L, // dead Guard control + -6359653295501938199L, // monotonicity, escape is dead + 6506797708065910879L, // required field without a user constructor + 650692970082394944L, // recursive bulk mem split + 8335100609598232836L, // ternary result killed by region cleanup + 2520958273643234516L, // nested guard merge + -3772838984504063325L, // fuzzer regression + -4628356252269023530L, // idepth cache version wrap + -4022199781524079249L, // fuzzer regression + 8824787517620928178L, // unknown-mode unary minus during SCCP + -673501011619761901L, // peephole killed self + -8212834489697770130L, // bad keep/unkeep in parser + 478567486335112458L, // dying escape node + -5114548365532699915L, // dominate flags dependencies + -4710931876754133070L, // Tests monotonic phi widening }; private static final long[] OPEN_FAILING_SEEDS = { }; - @Test public void fuzzPeepsRegression () { fuzzPeepsSeeds(REGRESSION_SEEDS); } + + @Test public void fuzzPeepsRegression () { fuzzPeepsSeeds( REGRESSION_SEEDS); } @Test @Ignore public void fuzzPeepsOpenFailures() { fuzzPeepsSeeds(OPEN_FAILING_SEEDS); } private static void fuzzPeepsSeeds(long... seeds) { @@ -30,29 +48,31 @@ public class FuzzerWrap { @Ignore @Test - public void fuzzPeepsLarge() { + public void fuzzPeepsRandom() { + Random R = new Random(System.currentTimeMillis()); var fuzzer = new Fuzzer(); - for (int i=0; i<1000000; i++) - fuzzer.fuzzPeeps(i); + for (int i=0; i<100; i++) + fuzzer.fuzzPeeps(R.nextLong()); assertTrue(fuzzer.noExceptions()); } - @Test @Ignore - public void fuzzPeepsRandom() { - Random R = new Random(System.currentTimeMillis()); + @Ignore + @Test + public void fuzzPeepsLarge() { var fuzzer = new Fuzzer(); - for (int i=0; i<100; i++) - fuzzer.fuzzPeeps( R.nextLong()); + for (int i=0; i<1000000; i++) + fuzzer.fuzzPeeps(i); assertTrue(fuzzer.noExceptions()); } - @Test @Ignore + @Test public void fuzzPeepTiming() { var fuzzer = new Fuzzer(); int max_nid=0; for (int i=0; i<1000000; i++) max_nid = fuzzer.fuzzPeepTiming(i, max_nid); } + } diff --git a/src/test/java/com/seaofnodes/simple/IncompleteTypesTest.java b/src/test/java/com/seaofnodes/simple/IncompleteTypesTest.java deleted file mode 100644 index f2f7d882..00000000 --- a/src/test/java/com/seaofnodes/simple/IncompleteTypesTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.CodeGen; -import org.junit.Test; -import com.seaofnodes.simple.type.TypeInteger; -import static org.junit.Assert.assertEquals; - -public class IncompleteTypesTest { - @Test public void testForwardFieldAndNumericMode() { - // At the addition, neither the field's type nor its offset is known. - String src = """ - val add = { Later p -> return p.x + 1; }; - struct Later { flt x; }; - return add(new Later { x = 2.5; }); - """; - var code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("3.5",Eval2.eval(code,0)); - } - - @Test public void testForwardStore() { - // Resolve the field's alias and integer width after its declaration. - String src = """ - val set = { !Later p, int n -> p.x = n; return p.x; }; - struct Later { u8 x; }; - return set(new Later,258); - """; - var code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("2",Eval2.eval(code,0)); - for( String update : new String[]{"p.x += n","p.x++"} ) { - String updates = "val set = { !Later p, int n -> p.x=255; return "+update+"; }; " + - "struct Later { u8 x; }; return set(new Later,3);"; - // Seeds 2 and 13 let the Load resolve before the symbolic Store. - // It must wait for that Store's alias, not read an earlier Store. - for( long seed : new long[]{2,13,123} ) { - var result = new CodeGen(updates,TypeInteger.BOT,seed,true).driver(CodeGen.Phase.TypeCheck); - assertEquals(update.equals("p.x++") ? "255" : "2",Eval2.eval(result,0)); - } - } - } -} diff --git a/src/test/java/com/seaofnodes/simple/LiteralTest.java b/src/test/java/com/seaofnodes/simple/LiteralTest.java deleted file mode 100644 index 447ef46e..00000000 --- a/src/test/java/com/seaofnodes/simple/LiteralTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.type.TypeConAryB; -import com.seaofnodes.simple.type.TypeInteger; -import com.seaofnodes.simple.type.TypeMemPtr; -import org.junit.Test; -import static org.junit.Assert.*; - -public class LiteralTest { - private static final String[] ESCAPES = {"\\0", "\\n", "\\t", "\\r", "\\\\", "\\\"", "\\'"}; - private static final int[] VALUES = {0, 10, 9, 13, 92, 34, 39}; - - @Test public void testStringEscapes() { - for (int i = 0; i < ESCAPES.length; i++) - assertString(ESCAPES[i], new byte[]{(byte)VALUES[i]}); - assertString("a\\\"b\\\\c\\0d", new byte[]{97, 34, 98, 92, 99, 0, 100}); - assertString("", new byte[]{}); - assertString("plain", new byte[]{112, 108, 97, 105, 110}); - } - - private static void assertString(String text, byte[] expected) { - var code = new CodeGen("return \"" + text + "\";").driver(CodeGen.Phase.TypeCheck); - TypeMemPtr ptr = (TypeMemPtr)code.expr()._type; - TypeConAryB bytes = (TypeConAryB)ptr._obj.field("[]")._t; - assertArrayEquals(text, expected, bytes._ary); - } - - @Test public void testCharacterEscapes() { - for (int i = 0; i < ESCAPES.length; i++) - assertCharacter(ESCAPES[i], VALUES[i]); - assertCharacter("A", 65); - } - - private static void assertCharacter(String text, int expected) { - var code = new CodeGen("return '" + text + "';").driver(CodeGen.Phase.TypeCheck); - assertEquals(text, expected, ((TypeInteger)code.expr()._type).value()); - } - - @Test public void testMalformedStrings() { - assertBadLiteral("return \"\\q\";", "Unknown string escape"); - assertBadLiteral("return \"abc", "Unclosed string"); - assertBadLiteral("return \"abc\\", "Unclosed string"); - // The quote closes the string at EOF; the missing token is the semicolon. - assertBadLiteral("return \"plain\"", ";"); - } - - @Test public void testMalformedCharacters() { - assertBadLiteral("return '\\q';", "Unknown character escape"); - assertBadLiteral("return '", "Unclosed character"); - assertBadLiteral("return '\\", "Unclosed character"); - assertBadLiteral("return '';", "Syntax error"); - assertBadLiteral("return 'ab';", "Syntax error"); - assertBadLiteral("return 'a", "Syntax error"); - } - - private static void assertBadLiteral(String src, String message) { - try { - new CodeGen(src).parse(); - fail("Accepted malformed literal: " + src); - } catch (Parser.ParseException e) { - assertTrue(e.getMessage(), e.getMessage().contains(message)); - } - } -} diff --git a/src/test/java/com/seaofnodes/simple/Main.java b/src/test/java/com/seaofnodes/simple/Main.java index ca1d0b5b..1248a6cf 100644 --- a/src/test/java/com/seaofnodes/simple/Main.java +++ b/src/test/java/com/seaofnodes/simple/Main.java @@ -11,7 +11,7 @@ public class Main { String src = Files.readString(Path.of(args[0])); int arg = Integer.valueOf(args[1]); CodeGen code = new CodeGen(src); - code.driver(CodeGen.Phase.LocalSched); + code.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule).localSched(); System.out.println(code._stop); System.out.println(Eval2.eval(code,arg,100000)); } diff --git a/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java b/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java index 397d644c..adeba136 100644 --- a/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java +++ b/src/test/java/com/seaofnodes/simple/MemoryEncodingTest.java @@ -1,18 +1,8 @@ package com.seaofnodes.simple; -import com.seaofnodes.isa.eval.EvalRisc5; - -import com.seaofnodes.isa.eval.EvalArm64; - import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.codegen.RegAllocTestSupport; import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.node.cpus.arm.arm; -import com.seaofnodes.simple.node.cpus.riscv.riscv; -import com.seaofnodes.simple.type.TypeMemPtr; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; +import java.util.BitSet; import org.junit.Test; import static org.junit.Assert.*; @@ -65,117 +55,50 @@ public class MemoryEncodingTest { return sum; """ }; - for( int p=0; p<sources.length; p++ ) { - CodeGen code=new CodeGen(sources[p]).driver(CodeGen.Phase.FinalizeIR); - if( p==0 ) assertNull(code._stop.walk(n -> - n instanceof LoadNode ld && ld.ptr() instanceof ProjNode ptr && - ptr.in(0) instanceof NewNode nn && - nn.size()._type==com.seaofnodes.simple.type.TypeInteger.constant(110) ? ld : null)); - for( int arg=0; arg<7; arg++ ) { - long expected=switch(p) { - case 0 -> 106*arg+(arg>=2 ? 2 : 0); - case 1 -> (arg&1)==0 ? 140 : 60; - case 2 -> 130; - default -> 16; - }; - assertEquals(Long.toString(expected),Eval2.eval(code,arg)); - } - } - } - - private static final String[] SOURCES = { - Chapter16Test.CONSTRUCTOR_MEMORY, - Chapter18Test.CALL_MEMORY, - Chapter18Test.RECURSIVE_MEMORY, - Chapter18Test.INLINE_MEMORY, - "int[] !a = new int[7]; a[arg]=arg+3; return a[0]+1;", - """ - int[] !a = new int[7]; - for( int i=0; i<7; i++ ) a[i]=i+arg; - for( int i=0; i<6; i++ ) a[i+1]+=a[i]; - return a[6]; - """ - }; - - private static long expected(int program, long arg) { - return switch(program) { - case 0 -> arg==0 ? 111249 : 222351+arg; - case 1 -> arg==0 ? 10101325 : 20202322; - case 2 -> (10*(arg+1)+arg*(arg+1)/2)*100+10+arg; - case 3 -> 400944+arg*10001; - case 4 -> arg==0 ? 4 : 1; - case 5 -> 21+7*arg; - default -> throw new AssertionError(); - }; - } - - @Test public void testEmulatedMemory() throws IOException { - for( String target : new String[]{"riscv","arm"} ) - for( int p=0; p<SOURCES.length; p++ ) { - CodeGen code = new RegAllocTestSupport.CheckedCodeGen(SOURCES[p]).driver(target,"SystemV",null); - int entry=0; - for( var n : code._start._outputs ) - if( n instanceof FunNode fun && "main".equals(fun._name) ) - entry=code._encoding._opStart[fun._nid]; + // Exercise both unresolved Store aliases and nested value-Phi cleanup. + for( long seed : new long[]{0,9,126} ) + for( int p=0; p<sources.length; p++ ) { + CodeGen code=new CodeGen(sources[p],seed,true).driver(CodeGen.Phase.FinalizeIR); + if( p==0 ) assertNull(code._stop.walk(n -> + n instanceof LoadNode ld && ld.ptr() instanceof ProjNode ptr && + ptr.in(0) instanceof NewNode nn && + nn.size()._type==com.seaofnodes.simple.type.TypeInteger.constant(110) ? ld : null)); for( int arg=0; arg<7; arg++ ) { - byte[] image = new byte[1<<20]; - byte[] bits = code._encoding.bits(); - System.arraycopy(bits,0,image,0,bits.length); - long result; - if( target.equals("riscv") ) { - EvalRisc5 cpu = new EvalRisc5(image,1<<16); - cpu._pc=entry; - cpu.regs[riscv.A0]=arg; - assertEquals(target+" program "+p+" arg "+arg,0,cpu.step(100000)); - result=cpu.regs[riscv.A0]; - } else { - EvalArm64 cpu = new EvalArm64(image,1<<16); - cpu._pc=entry; - cpu.regs[arm.X0]=arg; - assertEquals(target+" program "+p+" arg "+arg,0,cpu.step(100000)); - result=cpu.regs[arm.X0]; - } - assertEquals(target+" program "+p+" arg "+arg,expected(p,arg),result); + long expected=switch(p) { + case 0 -> 106*arg+(arg>=2 ? 2 : 0); + case 1 -> (arg&1)==0 ? 140 : 60; + case 2 -> 130; + default -> 16; + }; + assertEquals(Long.toString(expected),Eval2.eval(code,arg)); } } } - @Test public void testNeverMemory() { - // A varying pointer prevents store sinking: the synthetic exit needs both slices. - for( String target : new String[]{"x86_64_v2","riscv","arm"} ) { - CodeGen code = new CodeGen(""" - struct S { int x; int y; }; - !S !a=new S; !S !b=new S; !S !p=a; - if( arg ) while( 1 ) { p.x+=arg; p.y+=p.x; p=p==a ? b : a; } - return a.x+a.y; - """).driver(CodeGen.Phase.LoopTree); - assertNotNull(code._stop.walk(n -> { - if( n instanceof MemMergeNode mem && - mem.in(0) instanceof CProjNode exit && exit.in(0) instanceof NeverNode ) { - var fields = ((TypeMemPtr)Parser.TYPES.get("S"))._obj._fields; - assertTrue(mem.alias(fields[0]._alias) instanceof MemPhiNode); - assertTrue(mem.alias(fields[1]._alias) instanceof MemPhiNode); - return mem; - } - return null; - })); - code.driver(CodeGen.Phase.RegAlloc,target,"SystemV"); - RegAllocTestSupport.checkRegisters(code); - code.encode(); - } + @Test public void testKeepReadsBeforeWrites() { + var code = new CodeGen(""" + struct S { int x; int y; }; + !S !a = new S; !S !b = new S; + a.x=arg; b.x=arg+1; + !S !p=a; !S !q=b; + if (arg<0) { p=b; q=a; } + int v; + if (arg>1) { v=p.x; p.x=41; p.y=5; } + else { v=q.x; q.x=42; } + return v; + """).driver(CodeGen.Phase.FinalizeIR); + StopNode stop = code._stop; + assertEquals(2,countMemoryNodes(stop,LoadNode.class,new BitSet())); + assertEquals("-1",Eval2.eval(code,-1)); + assertEquals("1",Eval2.eval(code,0)); + assertEquals("3",Eval2.eval(code,3)); } - @Test public void testNativeMemory() throws IOException { - Files.createDirectories(Path.of("build/objs")); - for( int p=0; p<SOURCES.length; p++ ) { - String file="build/objs/memory"+p; - StringBuilder c = new StringBuilder("extern long long memory(long long);\nint main() {\n"); - for( int arg=0; arg<7; arg++ ) - c.append("if(memory(").append(arg).append(")!=").append(expected(p,arg)).append("LL) return ").append(arg+1).append(";\n"); - c.append("return 0;\n}\n"); - Files.writeString(Path.of(file+".c"),c); - TestC.run("val memory = { int arg -> "+SOURCES[p]+" };", - TestC.CALL_CONVENTION,null,"",file+".c",file,"S","",-1); - } + private static int countMemoryNodes(Node n, Class<?> kind, BitSet seen) { + if (n==null || seen.get(n._nid)) return 0; + seen.set(n._nid); + int cnt=kind.isInstance(n) ? 1 : 0; + for (Node def : n._inputs) cnt+=countMemoryNodes(def,kind,seen); + return cnt; } } diff --git a/src/test/java/com/seaofnodes/simple/MergeSortTest.java b/src/test/java/com/seaofnodes/simple/MergeSortTest.java index 453bd35c..f5d3d146 100644 --- a/src/test/java/com/seaofnodes/simple/MergeSortTest.java +++ b/src/test/java/com/seaofnodes/simple/MergeSortTest.java @@ -2,12 +2,10 @@ package com.seaofnodes.simple; import com.seaofnodes.isa.eval.EvalRisc5; -import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.node.cpus.riscv.riscv; import com.seaofnodes.simple.util.SB; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; + import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -17,52 +15,52 @@ public class MergeSortTest { @Test public void testMergeSort0() throws IOException { // Test merge sort on a small array of primes String src = -""" - val merge_sort = { int[] a -> - int[] !b = new int[a#]; - copy_array (a, 0, a#, b); - split_merge(a, 0, a#, b); - }; - - val split_merge = { int[] !b, int begin, int end, int[] a -> - if (end - begin <= 1) - return 0; - int middle = (end + begin) / 2; - split_merge(a, begin, middle, b); - split_merge(a, middle, end, b); - merge(b, begin, middle, end, a); - return 0; - }; - - val merge = { int[] !b, int begin, int middle, int end, int[] a -> - int i = begin, j = middle; - - for (int k = begin; k < end; k++) { - // && and || - bool cond = false; - if (i < middle) { - if (j >= end) cond = true; - else if (a[i] <= a[j]) cond = true; - } - if (cond) b[k] = a[i++]; - else b[k] = a[j++]; - } - }; - - val copy_array = { int[] !a, int begin, int end, int[] b -> - for (int k = begin; k < end; k++) - b[k] = a[k]; - }; +""" +val merge_sort = { int[] a -> + int[] !b = new int[a#]; + _copy_array (a, 0, a#, b); + _split_merge(a, 0, a#, b); +}; + +val _split_merge = { int[] !b, int begin, int end, int[] a -> + if (end - begin <= 1) + return 0; + int middle = (end + begin) / 2; + _split_merge(a, begin, middle, b); + _split_merge(a, middle, end, b); + _merge(b, begin, middle, end, a); + return 0; +}; + +val _merge = { int[] !b, int begin, int middle, int end, int[] a -> + int i = begin, j = middle; + + for (int k = begin; k < end; k++) { + // && and || + bool cond = false; + if (i < middle) { + if (j >= end) cond = true; + else if (a[i] <= a[j]) cond = true; + } + if (cond) b[k] = a[i++]; + else b[k] = a[j++]; + } +}; + +val _copy_array = { int[] !a, int begin, int end, int[] b -> + for (int k = begin; k < end; k++) + b[k] = a[k]; +}; """; int[] primes = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, }; SB sb = new SB().p(primes.length).p("["); for( int prime : primes ) sb.p(prime).p(", "); String sprimes = sb.p("]").toString(); - TestC.run(src, "sort", null, sprimes, 34); + TestC.runC(src, "sort", sprimes, 34); - EvalRisc5 R5 = TestRisc5.build("src/test/java/com/seaofnodes/simple/progs",src, "sort", "merge_sort", 0, 36, false); + 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 diff --git a/src/test/java/com/seaofnodes/simple/ModulesTest.java b/src/test/java/com/seaofnodes/simple/ModulesTest.java new file mode 100644 index 00000000..be54b43d --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/ModulesTest.java @@ -0,0 +1,248 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalRisc5; + +import com.seaofnodes.isa.eval.EvalArm64; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.codegen.ParseAll; +import com.seaofnodes.simple.node.CallNode; +import com.seaofnodes.simple.node.FunNode; +import com.seaofnodes.simple.node.LoopNode; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class ModulesTest { + + @Test public void testFlagSpills() throws IOException { + com.seaofnodes.simple.codegen.FlagSpillTestSupport.run(); + } + + @Ignore + @Test + public void testJig() { + String src = "{ i32 -> i32 } close = \"C\"; val main = { u8[] a -> if (a# > 1) return close(0); return a[0]; };"; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.Encoding,"riscv","SystemV"); + assertEquals("return (arg+3);", code.print()); + assertEquals("3",Eval2.eval(code,0)); + } + + @Test public void testConditionalClassInitX86() throws IOException { + Path build=Path.of("build/objs"); + Files.createDirectories(build); + Path dir=Files.createTempDirectory(build,"init_flags_"); + Files.createDirectories(dir.resolve("Main")); + Files.writeString(dir.resolve("Main/B.smp"),"int value=17+arg;"); + // GCM can hoist the fused B.$init compare above the test of take. + // Its flags then cross another flag definition and need a legal split. + new CheckedCodeGen(dir.toString(),dir.toString(), + "Main","val run={int take -> return take ? B.value : 0; };",123,true,TypeInteger.BOT) + .driver(CodeGen.Phase.Encoding,"x86_64_v2","win64"); + // Remove successful fixtures; preserve the object if compilation fails. + Files.delete(dir.resolve("Main/B.smp")); + Files.delete(dir.resolve("Main")); + Files.delete(dir); + } + + @Test public void testPublicInstanceMethod() { + String src = """ + struct Scan { + val visible = { u8 c -> c==255; }; + val _hidden = { u8 c -> c==1; }; + }; + struct _Private { + val hiddenClassMethod = { u8 c -> c==2; }; + }; + return new Scan.visible(44); + """; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.Parse); + FunNode visible = null; + int checked = 0; + for( FunNode fun : code._linker ) { + if( fun==null || fun._name==null ) continue; + if( fun._name.equals("visible") ) { + assertTrue(code.publicFIDX(fun.sig().fidx())); + visible = fun; + checked++; + } + if( fun._name.equals("_hidden") || fun._name.equals("hiddenClassMethod") ) { + assertFalse(code.publicFIDX(fun.sig().fidx())); + checked++; + } + } + assertEquals(3,checked); + code.driver(CodeGen.Phase.TypeCheck); + assertTrue(visible.unknownCallers()); + assertEquals(TypeInteger.U8,visible.parm(3)._type); + assertEquals("0",Eval2.eval(code,0)); + } + } + + @Test public void testNeverReturnPointerRiscV() { + new CodeGen(""" + struct _S { int x; }; + !_S !s = new _S; + if(arg) while(1) { s.x += arg; } + return s; + """).driver(CodeGen.Phase.Encoding,"riscv","SystemV"); + } + + @Test public void testInlineNeverExit() { + 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);" + }; + for( int i=0; i<sources.length; i++ ) { + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(sources[i],seed,true).driver(CodeGen.Phase.TypeCheck); + assertNull("Calls should inline",code._start.walk(n -> n instanceof CallNode ? n : null)); + assertNotNull("The non-exiting path must survive",code._start.walk(n -> n instanceof LoopNode && n!=code._start ? n : null)); + if( i==1 ) assertEquals("14",Eval2.eval(code,0,10)); + assertNull(Eval2.eval(code,1,10)); + } + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) + new CheckedCodeGen(sources[i]).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + } + + @Test + public void testExternData() throws IOException { + String src = """ + i32 counter="C"; + i8 small="C"; + f64 fraction="C"; + {int} bump="C"; + val test_global={ -> + val before=counter; + counter=before; + counter=++counter+1; + counter+=1; + val mid=bump(); + small++; + fraction=fraction+0.5; + return before*10000+mid*100+counter; + }; + """; + TestC.runC(src,"extern_data","",-1); + } + + @Test + public void testExternDataEncoding() { + for( String cpu : new String[]{"arm","riscv"} ) { + CodeGen code = new CheckedCodeGen("i32 counter=\"C\"; counter=counter+3; return counter;"); + // Exercise signed low-12 relocation arithmetic, past the first page. + int addr = 0x3800; + code._externDataAddresses.put("counter",addr); + code.driver(cpu,"SystemV",true,false); + byte[] image = new byte[1<<20]; + var bits = code._encoding._bits; + System.arraycopy(bits.buf(),0,image,0,bits.size()); + if( cpu.equals("arm") ) { + EvalArm64 arm = new EvalArm64(image,1<<16); + arm.st4(addr,-7); + assertEquals(0,arm.step(1000)); + assertEquals(-4,arm.regs[0]); + assertEquals(-4,arm.ld4s(addr)); + } else { + EvalRisc5 r5 = new EvalRisc5(image,1<<16); + r5.st4(addr,-7); + assertEquals(0,r5.step(1000)); + assertEquals(-4,r5.regs[10]); + assertEquals(-4,r5.ld4s(addr)); + } + } + } + + + @Test public void testPrintingConstantPool() throws Exception { + com.seaofnodes.simple.codegen.PrintRegTestSupport.checkConstantPool(); + } + + + @Test + public void testForwardConstructor() { + CodeGen code = new CodeGen("src/test/java/com/seaofnodes/simple/test_smp/forward_ctor", + "build/objs/forward_ctor_parse", + "m",null,123L,true,TypeInteger.BOT); + code.driver(CodeGen.Phase.TypeCheck); + } + + + @Test + public void testPostfixFieldUpdate() { + String src = """ + struct V { + u32 !len = 0; + i64[] !buf; + new V = { i64[] b -> buf=b; }; + val grow = { i64 n -> self; }; + val add = { i64 x -> grow(1).buf[len++] = x; self; }; + }; + return new V(new i64[1]).add(7).len; + """; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + + @Test + public void testStringEscapes() { + String src = "u8[~] s=\"A\\n\\t\\r\\\\\\\"B\"; " + + "return s#==7 && s[0]=='A' && s[1]==10 && s[2]==9 && s[3]==13 && " + + "s[4]==92 && s[5]==34 && s[6]=='B';"; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + + @Test + public void testCharacterEscapes() { + CodeGen code = new CodeGen("return '\\n'==10 && '\\t'==9 && '\\r'==13;") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + + @Test + public void testFuzzerUnresolvedNumericLoop() { + CodeGen code = new CodeGen(""" + arg=0; + while(arg+0) + while(-arg) + arg=0; + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("return 0;",code.print()); + } + + @Test + public void testFuzzerBadLCA() { + new CodeGen(""" + int v0=arg; + if(v0&&--v0?0:arg) {} + """).driver(CodeGen.Phase.LocalSched); + } + + + // Recursive search (TODO: gzip, archives) and delete all .o files + + + @Test + public void testConstantClassInitialization() throws IOException { + TestC.runSF("val x = 42; return 0;","constantClassInt","",0); + TestC.runSF("val f = { -> 7; }; return 0;","constantClassFunction","",0); + } + + +} diff --git a/src/test/java/com/seaofnodes/simple/MutabilityTest.java b/src/test/java/com/seaofnodes/simple/MutabilityTest.java index 51e31a17..8bd77c06 100644 --- a/src/test/java/com/seaofnodes/simple/MutabilityTest.java +++ b/src/test/java/com/seaofnodes/simple/MutabilityTest.java @@ -22,20 +22,20 @@ public class MutabilityTest { } @Test public void testIndependentBindingsAndViews() { - ok("struct P { int x; }; !P ~p=new P; p.x=7; return p.x;", "7"); - ok("struct P { int x; }; ~P !p=new P{x=3;}; p=new P{x=7;}; return p.x;", "7"); - bad("struct P { int x; }; !P ~p=new P; p=new P; return 0;"); - bad("struct P { int x; }; ~P !p=new P; p.x=7; return 0;"); + ok("struct P { int x; new P = { int v -> x=v; }; }; !P ~p=new P(0); p.x=7; return p.x;", "7"); + ok("struct P { int x; new P = { int v -> x=v; }; }; ~P !p=new P(3); p=new P(7); return p.x;", "7"); + bad("struct P { int x; new P = { int v -> x=v; }; }; !P ~p=new P(0); p=new P(0); return 0;"); + bad("struct P { int x; new P = { int v -> x=v; }; }; ~P !p=new P(0); p.x=7; return 0;"); bad("int ~x=3; x=7; return x;"); ok("int !x=3; x=7; return x;", "7"); } @Test public void testDeepViewAndAliases() { - ok("struct P { int x; }; !P ~p=new P; ~P ~r=p; p.x=7; return r.x;", "7"); - bad("struct P { int x; }; struct Box { !P p; }; !P q=new P; Box b=new Box{p=q;}; b.p.x=7; return 0;"); - bad("struct P { int x; }; P p=new P; !P q=p; q.x=7; return 0;"); - bad("struct P { P? next; int x; }; !P p=new P{next=new P;}; p.next.x=7; return 0;"); - bad("struct P { !P? next; int x; }; P p=new P{next=new P;}; p.next.x=7; return 0;"); + ok("struct P { int x; new P = { int v -> x=v; }; }; !P ~p=new P(0); ~P ~r=p; p.x=7; return r.x;", "7"); + bad("struct P { int x; new P = { int v -> x=v; }; }; struct Box { !P p; new Box = { !P v -> p=v; }; }; !P p=new P(0); Box b=new Box(p); b.p.x=7; return 0;"); + bad("struct P { int x; new P = { int v -> x=v; }; }; P p=new P(0); !P q=p; q.x=7; return 0;"); + bad("struct P { P? next; int x; new P = { P? v -> next=v; }; }; !P p=new P(new P(null)); p.next.x=7; return 0;"); + bad("struct P { !P? next; int x; new P = { !P? v -> next=v; }; }; P p=new P(new P(null)); p.next.x=7; return 0;"); } @Test public void testInferencePreservesAccess() { @@ -53,18 +53,12 @@ public class MutabilityTest { bad("struct P { int x; }; val f={ P?[] a -> return a#; }; !P?[] a=new !P?[1]; return f(a);"); } - @Test public void testConstructorScope() { - ok("struct P { int ~x; }; P p=new P{if(arg) x=3; else x=7;}; return p.x;", "7"); - bad("int ~outer=1; struct P { int x; }; P p=new P{outer=7;}; return outer;"); - bad("struct P { int x; }; P p=new P{int ~local=1; local=7;}; return p.x;"); - } - @Test public void testArrayLayers() { bad("struct P { int x; }; !P?[] rw=new !P?[1]; P?[] ro=rw; rw[0]=new P; ro[0].x=7; return 0;"); ok("u8[] a=new u8[2]; a[0]=7; return a[0];", "7"); - ok("struct P { int x; }; P?[] a=new P?[1]; a[0]=new P{x=7;}; return a[0].x;", "7"); - bad("struct P { int x; }; P?[] a=new P?[1]; a[0]=new P; a[0].x=7; return 0;"); - ok("struct P { int x; }; !P?[] a=new !P?[1]; a[0]=new P; a[0].x=7; return a[0].x;", "7"); + ok("struct P { int x; new P = { int v -> x=v; }; }; P?[] a=new P?[1]; a[0]=new P(7); return a[0].x;", "7"); + bad("struct P { int x; new P = { int v -> x=v; }; }; P?[] a=new P?[1]; a[0]=new P(0); a[0].x=7; return 0;"); + ok("struct P { int x; new P = { int v -> x=v; }; }; !P?[] a=new !P?[1]; a[0]=new P(0); a[0].x=7; return a[0].x;", "7"); bad("u8[~] a=new u8[1]; a[0]=7; return 0;"); ok("u8[~]?[] rows=new u8[~]?[1]; rows[0]=new u8[1]; return rows#;", "1"); bad("u8[~]?[] rows=new u8[~]?[1]; rows[0]=new u8[1]; rows[0][0]=7; return 0;"); diff --git a/src/test/java/com/seaofnodes/simple/NativeAddressTest.java b/src/test/java/com/seaofnodes/simple/NativeAddressTest.java deleted file mode 100644 index 6d21ca5f..00000000 --- a/src/test/java/com/seaofnodes/simple/NativeAddressTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.codegen.Encoding; -import com.seaofnodes.isa.eval.EvalArm64; -import com.seaofnodes.isa.eval.EvalRisc5; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.Test; -import static org.junit.Assert.*; - -public class NativeAddressTest { - @Test public void testReturnedNativeAddress() throws IOException { - Path dir = Path.of("build/objs"); - Files.createDirectories(dir); - Files.writeString(dir.resolve("nativeAddress.smp"), """ - val getAbs = { -> - { i32 -> i32 } abs = "C"; - return abs; - }; - """); - Files.writeString(dir.resolve("nativeAddress.c"), """ - typedef int (*func)(int); - extern func getAbs(void); - int main(void) { - func f = getAbs(); - return f(-7) != 7 || f(9) != 9; - } - """); - TestC.run("build/objs", "nativeAddress", "", -1); - } - - @Test public void testEmulatedNativeAddress() throws IOException { - // No native call: return the address itself, patched to the emulator's - // known write entry. Both ports share Encoding's symbol lookup. - String src = "{ int -> int } write = \"C\"; return write;"; - for( String cpu : new String[]{"arm","riscv"} ) { - CodeGen code = new CodeGen(src).driver(cpu,"SystemV",null); - byte[] bits = code._encoding.bits(); - byte[] image = new byte[1<<20]; - System.arraycopy(bits,0,image,0,bits.length); - if( cpu.equals("arm") ) { - EvalArm64 eval = new EvalArm64(image,1<<16); - assertEquals(0,eval.step(100)); - assertEquals(Encoding.SENTINEL_WRITE,eval.regs[0]); - } else { - EvalRisc5 eval = new EvalRisc5(image,1<<16); - assertEquals(0,eval.step(100)); - assertEquals(Encoding.SENTINEL_WRITE,eval.regs[10]); - } - } - } -} diff --git a/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java b/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java new file mode 100644 index 00000000..fa914d28 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java @@ -0,0 +1,68 @@ +package com.seaofnodes.simple; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; + +public class NativeExecutionTest { + // A subprocess with deterministic stdout, stderr, and process status. + public static class Child { + public static void main(String[] args) { + System.out.println("before exit"); + System.err.println("child diagnostic"); + System.exit(Integer.parseInt(args[0])); + } + } + + private static String child(int exit) throws IOException { + String java = Path.of(System.getProperty("java.home"),"bin","java").toString(); + return TestC.exec(java,"-cp",System.getProperty("java.class.path"), + Child.class.getName(),Integer.toString(exit)); + } + + private static void checkFailure(int exit) throws IOException { + try { + child(exit); + fail("Accepted process exit "+exit); + } catch( IOException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("exit code: "+exit)); + assertTrue(e.getMessage(),e.getMessage().contains("before exit")); + assertTrue(e.getMessage(),e.getMessage().contains("child diagnostic")); + } + } + + @Test public void testNonzeroExit() throws IOException { checkFailure(7); } + + @Test public void testFullWindowsExitCode() throws IOException { + assumeTrue(TestC.OS.startsWith("Windows")); + checkFailure(256); // Narrowing to a byte incorrectly reports success. + checkFailure(2816); // Observed Cygwin status for the Hello World crash. + } + + @Test public void testSuccessfulOutput() throws IOException { + assertEquals("before exit"+System.lineSeparator(),child(0)); + } + + @Test public void testHelloWorldArtifacts() throws IOException { + new Chapter22Test().testHelloWorld(); + // Model a parallel run paused between Chapter 22's link and execution. + // Chapter 25 must not replace the executable Chapter 22 is about to run. + String exe = "build/objs/helloWorld"+(TestC.OS.startsWith("Windows") ? ".exe" : ""); + assertEquals("Hello, World!",TestC.exec(exe)); + } + + @Test public void testDriverNonzeroExit() throws Exception { + Path dir = Files.createTempDirectory(Path.of("build/objs"),"driver-exit-"); + Path src = dir.resolve("fail.smp"); + Files.writeString(src,"return 7;"); + try { + Simple.main(new String[]{src.toString()}); + fail("Driver accepted a failed executable"); + } catch( IOException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("exit code: 7")); + } + } +} diff --git a/src/test/java/com/seaofnodes/simple/PhaseTest.java b/src/test/java/com/seaofnodes/simple/PhaseTest.java index 899f42fe..5ff7fcd6 100644 --- a/src/test/java/com/seaofnodes/simple/PhaseTest.java +++ b/src/test/java/com/seaofnodes/simple/PhaseTest.java @@ -11,7 +11,7 @@ import static org.junit.Assert.*; public class PhaseTest { private static final String SRC = "int sum=0; for(int i=0; i<arg; i++) sum+=i; return sum;"; - @Test public void testStopAndResume() { + @Test public void testIdealPhasesAndResume() { ArrayList<String> phases = new ArrayList<>(); CodeGen code = new CodeGen(SRC); code._obs = new GraphObserver<Node>() { @@ -19,38 +19,25 @@ public class PhaseTest { @Override public void after(Node n, Node repl, boolean applied) {} @Override public void phase(String phase) { phases.add(phase); } }; - code.driver(Phase.Iter); - assertEquals(Phase.Iter,code._phase); - assertEquals("Parse Iter",String.join(" ",phases)); code.driver(Phase.TypeCheck); - code.driver(Phase.TypeCheck); // Repeated requests do not rerun phases. - code.driver(Phase.LoopTree); - assertEquals(Phase.LoopTree,code._phase); + code.driver(Phase.TypeCheck); // A repeated request does not rerun anything. code.driver(Phase.LocalSched); - assertEquals("Parse Iter Opto IterPostOpto FinalizeIR TypeCheck LoopTree Schedule LocalSched",String.join(" ",phases)); + assertEquals("Parse Iter Opto IterPostOpto FinalizeIR TypeCheck Lower LoopTree Schedule LocalSched", + String.join(" ",phases)); + assertNull(code._mach); assertEquals("45",Eval2.eval(code,10)); } - @Test public void testResumeMachineCompilation() { - CodeGen staged = new CodeGen(SRC).driver(Phase.TypeCheck); - staged.driver(Phase.Select,"x86_64_v2","SystemV"); - staged.driver(Phase.Encoding,"x86_64_v2","SystemV"); - byte[] bits = staged._encoding._bits.toByteArray(); - CodeGen direct = new CodeGen(SRC).driver(Phase.Encoding,"x86_64_v2","SystemV"); - assertArrayEquals(bits,direct._encoding._bits.toByteArray()); - } - - @Test public void testOptoRequiresIter() { - CodeGen code = new CodeGen(SRC).parse(); - try { - code.opto(); - fail("Opto must not implicitly run Iter"); - } catch( AssertionError expected ) { - assertEquals("Cannot run Opto after Parse",expected.getMessage()); + @Test public void testEncodingAndResume() { + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) { + CodeGen code = new CodeGen(SRC).driver(Phase.TypeCheck); + code.driver(Phase.LoopTree,cpu,"SystemV"); + code.driver(Phase.Select,cpu,"SystemV"); + code.driver(Phase.Encoding,cpu,"SystemV"); + byte[] staged = code._encoding._bits.toByteArray(); + CodeGen direct = new CodeGen(SRC).driver(Phase.Encoding,cpu,"SystemV"); + assertArrayEquals(staged,direct._encoding._bits.toByteArray()); } - code.iter().opto(); - assertEquals(Phase.Opto,code._phase); - code.driver(Phase.LocalSched); - assertEquals("45",Eval2.eval(code,10)); } + } diff --git a/src/test/java/com/seaofnodes/simple/SccpTest.java b/src/test/java/com/seaofnodes/simple/SccpTest.java deleted file mode 100644 index cad1b7de..00000000 --- a/src/test/java/com/seaofnodes/simple/SccpTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.TypeInteger; -import org.junit.Test; -import static org.junit.Assert.*; - -public class SccpTest { - @Test public void testCallArgumentFlow() { - var code = new CodeGen("val f={ int x -> x+1; }; return f(1)+f(2);").parse().iter().opto(); - int[] parms = {0}; - code._start.walk(n -> { - if( n instanceof ParmNode p && "x".equals(p._label) ) { - parms[0]++; - TypeInteger t = (TypeInteger)p._type; - assertEquals(1,t._min); - assertEquals(2,t._max); - assertFalse(p.fun().in(1) instanceof StartNode); - } - return null; - }); - assertEquals(1,parms[0]); - code.driver(CodeGen.Phase.LocalSched); - assertEquals("5",Eval2.eval(code,0)); - } -} diff --git a/src/test/java/com/seaofnodes/simple/SchedulerTest.java b/src/test/java/com/seaofnodes/simple/SchedulerTest.java deleted file mode 100644 index 6156700d..00000000 --- a/src/test/java/com/seaofnodes/simple/SchedulerTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.evaluator.Evaluator; -import com.seaofnodes.simple.node.StopNode; -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class SchedulerTest { - - @Test public void testMemoryPhiAcrossSiblingBranch() { - // The inner memory Phi defines the load's memory. The outer Phi's - // other input comes from a branch not dominated by that definition. - String src = """ - struct S { int x; }; !S !a=new S; !S !b=new S; - a.x=10; b.x=20; !S !p=a; if(arg>3) p=b; - int result=0; - if(arg>0) { - if(arg>2) p.x=30; else p.x=40; - result=a.x; - } - return result; - """; - var code = new CodeGen(src).driver(CodeGen.Phase.LocalSched); - long[] expected = {0,40,40,30,10,10}; - for( int arg=0; arg<expected.length; arg++ ) - assertEquals(Long.toString(expected[arg]),Eval2.eval(code,arg)); - } - - - private static void assertObj(Object obj, String name, Object... fields) { - assertEquals(Evaluator.Obj.class, obj.getClass()); - var o = (Evaluator.Obj)obj; - assertEquals(name, o.struct()._name); - Assert.assertArrayEquals(fields, o.fields()); - } - - @Test - @Ignore - public void testJig() { - CodeGen code = new CodeGen(""" -struct S { - int f; -} -!S !v=new S; -v.f = 2; -int i=v.f; -if (arg) v.f=1; -return i; -"""); - code.parse();//.iter().opto().iterPostOpto().finalizeIR(); - var eval = new Evaluator(code._stop); - assertEquals(2L, eval.evaluate(1, 10)); - } - - @Test - public void testStoreInIf() { - CodeGen code = new CodeGen(""" -struct S { - int f; -}; -!S !v0=new S; -if(arg) v0.f=1; -return v0; -"""); - code.driver(CodeGen.Phase.FinalizeIR); - var eval = new Evaluator(code._stop); - assertObj(eval.evaluate(0, 10), "S", 0L); - assertObj(eval.evaluate(1, 10), "S", 1L); - } - - @Test - public void testStoreInIf2() { - CodeGen code = new CodeGen(""" -struct S { - int f; -}; -!S !v=new S; -v.f = 2; -int i=new S.f; -i=v.f; -if (arg) v.f=1; -return i; -"""); - code.driver(CodeGen.Phase.FinalizeIR); - var eval = new Evaluator(code._stop); - assertEquals(2L, eval.evaluate(0, 10)); - assertEquals(2L, eval.evaluate(1, 10)); - } - -} diff --git a/src/test/java/com/seaofnodes/simple/Simple.java b/src/test/java/com/seaofnodes/simple/Simple.java index 20d6fe71..437fcd81 100644 --- a/src/test/java/com/seaofnodes/simple/Simple.java +++ b/src/test/java/com/seaofnodes/simple/Simple.java @@ -1,74 +1,66 @@ 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.print.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; import com.seaofnodes.simple.util.Utils; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; -import java.util.stream.Collectors; +import java.util.ArrayList; public class Simple { - public static final String PORTS = "com.seaofnodes.simple.node.cpus"; - - static final int DUMP_AFTER_PARSE = 1<<Phase.Parse.ordinal(); - static final int DUMP_AFTER_ITER = 1<<Phase.Iter.ordinal(); - static final int DUMP_AFTER_OPTO = 1<<Phase.Opto.ordinal(); - static final int DUMP_AFTER_ITER_POST_OPTO = 1<<Phase.IterPostOpto.ordinal(); - static final int DUMP_AFTER_FINALIZE_IR = 1<<Phase.FinalizeIR.ordinal(); - static final int DUMP_AFTER_TYPE_CHECK = 1<<Phase.TypeCheck.ordinal(); - static final int DUMP_AFTER_LOOP_TREE = 1<<Phase.LoopTree.ordinal(); - static final int DUMP_AFTER_SELECT = 1<<Phase.Select.ordinal(); - static final int DUMP_AFTER_GCM = 1<<Phase.Schedule.ordinal(); - static final int DUMP_AFTER_LOCAL_SCHED = 1<<Phase.LocalSched.ordinal(); - static final int DUMP_AFTER_REG_ALLOC = 1<<Phase.RegAlloc.ordinal(); - static final int DUMP_AFTER_ENCODE = 1<<Phase.Encoding.ordinal(); - - static final int DUMP_FINAL = 1<<16; - - static final int DUMP_PASS_NAME = 1<<30; - static final int DUMP_AFTER_ALL = ((1<<Phase.values().length)-1) | DUMP_PASS_NAME; - - static final int DUMP_DISASSEMBLE = 1<<31; - static void usage() { System.out.println( """ -simple [options] <input-file> ...args... +simple [options] input-file.smp ...program args... +Options are applied in-order. Options: - --dump - dump final intermediate representation - --dump-after-parse - dump intermediate representation after parsing - --dump-after-iter - dump intermediate representation after Iter - --dump-after-opto - dump intermediate representation after Opto - --dump-after-iter-post-opto - dump intermediate representation after IterPostOpto - --dump-after-finalize-ir - dump intermediate representation after FinalizeIR - --dump-after-type-check - dump intermediate representation after type checking - --dump-after-loop-tree - dump intermediate representation after loop-tree construction - --dump-after-select - dump intermediate representation after instruction selection - --dump-after-gcm - dump intermediate representation after GCM - --dump-after-local-sched - dump intermediate representation after local scheduling - --dump-after-reg-alloc - dump intermediate representation after register allocation - --dump-after-encode - dump intermediate representation after encoding - --dump-after-all - dump intermediate representation after all passes - -S - dump generated assembler code - --eval - evaluate the compiled code in emulator + --help - This text + + --root root/ - Project root directory; this must end in a '/' and input + files are relative paths to this. If missing, the one input + file path is treated as its own root directory. + + -o output - Project output directory; similar to --root, if 'output' ends in a + '/', then all result files use relative paths to 'output', using a input file + root-relative path. If --root is missing, the one result file is named 'output' + + --eval - (slowly) evaluate the compiled code in emulator --run - run the compiled code natively; this is the default - --dump-size - print the size of generated code - --dump-time - print compilation and execution times + --main - export the top-level initializer as C `main` --cpu <cpu-name> - use specific CPU (x86_64_v2, riscv, arm) - --abi <abi-name> - use speific ABI variant (SystemV) + --abi <abi-name> - use specific ABI variant (SystemV) --target - print native CPU and ABI --version - --help + -S - dump generated assembler code + --dump-size - print the size of generated code + --dump-time - print compilation and execution times + --dump-after-parse - dump intermediate representation after parse + --dump-after-iter - dump intermediate representation after pessimistic iter pass + --dump-after-opto - dump intermediate representation after SCCP + --dump-after-iter-post-opto - dump after SCCP-driven peepholes and inlining + --dump-after-finalize-ir - dump after field layout and IR finalization + --dump-after-type-check - dump intermediate representation after type check pass + --dump-after-lower - dump expanded class initialization before loop trees + --dump-after-select - dump intermediate representation after instruction selection pass + --dump-after-gcm - dump intermediate representation after GCM pass + --dump-after-local-sched - dump intermediate representation after local scheduling pass + --dump-after-reg-alloc - dump intermediate representation after register allocation pass + --dump-after-encode - dump intermediate representation after encoding pass + --dump-after-all - dump intermediate representation after all passes + --dump - dump final intermediate representation only """ ); System.exit(0); } - static void bad_usage() { bad("Invalid usage (use --help)"); } + static void bad_usage() { throw bad("Invalid usage (use --help)"); } static RuntimeException bad(String err) { System.err.println("ERROR: "+err); System.exit(1); @@ -81,40 +73,37 @@ Options: System.out.println(TestC.CALL_CONVENTION); } - static void dump(CodeGen code, int dump, int pass) { - if( (dump & pass)==0 ) return; - if( (dump & DUMP_PASS_NAME)!=0 ) System.err.println("Final:"); - System.err.println(code); - } - static void print_compilation_times(CodeGen code) { - long total=0; - for( Phase phase : Phase.values() ) { - long time=code._times[phase.ordinal()]; - total+=time; - System.out.printf("%-24s %.3f sec%n",phase+" Time:",time/1e3); - } - System.out.printf("TOTAL COMPILATION TIME: %.3f sec%n",total/1e3); + long[] times = code._times; + for( int i=0; i<code._times.length; i++ ) + System.out.printf("%20s %.3f sec%n", Phase.values()[i], times[i]); } - static String getInput() throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { - return reader.lines().collect(Collectors.joining("\n")); - } + static String getInput() { + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + StringBuilder text=new StringBuilder(); + try { + for( String line; (line=reader.readLine())!=null; ) text.append(line).append('\n'); + } catch( IOException e ) { throw new RuntimeException(e); } + return text.toString(); } public static void main(String[] args) throws Exception { + String root = null; String input_filename = null; boolean do_eval = false; boolean do_run = true; boolean do_codegen = false; - boolean do_print_size = false; - boolean do_print_time = false; + boolean emit_main = false; + boolean print_time = false; + boolean print_size = false; + boolean print_asm = false; int dump = 0; int first_arg = 0; String src = null; String cpu = null; String abi = null; + String out = null; // Parse command line int i; for( i = 0; i < args.length; i++ ) { @@ -126,26 +115,35 @@ Options: switch (arg) { case "-": input_filename=""; break; - case "--dump": dump |= DUMP_FINAL; break; - case "--dump-after-parse": dump |= DUMP_AFTER_PARSE; break; - case "--dump-after-finalize-ir": dump |= DUMP_AFTER_FINALIZE_IR; break; - case "--dump-after-iter-post-opto": dump |= DUMP_AFTER_ITER_POST_OPTO; break; - case "--dump-after-iter": dump |= DUMP_AFTER_ITER; break; - case "--dump-after-opto": dump |= DUMP_AFTER_OPTO; break; - case "--dump-after-type-check": dump |= DUMP_AFTER_TYPE_CHECK; break; - case "--dump-after-loop-tree": dump |= DUMP_AFTER_LOOP_TREE; break; - case "--dump-after-select": dump |= DUMP_AFTER_SELECT; break; - case "--dump-after-gcm": dump |= DUMP_AFTER_GCM; break; - case "--dump-after-local-sched": dump |= DUMP_AFTER_LOCAL_SCHED; break; - case "--dump-after-reg-alloc": dump |= DUMP_AFTER_REG_ALLOC; break; - case "--dump-after-encode": dump |= DUMP_AFTER_ENCODE; break; - case "--dump-after-all": dump |= DUMP_AFTER_ALL; break; - case "-S": dump |= DUMP_DISASSEMBLE; break; + case "--dump-after-parse": dump |= 1<<Phase.Parse .ordinal() ; break; + case "--dump-after-iter": dump |= 1<<Phase.Iter .ordinal() ; break; + case "--dump-after-opto": dump |= 1<<Phase.Opto .ordinal() ; break; + case "--dump-after-iter-post-opto": dump |= 1<<Phase.IterPostOpto.ordinal(); break; + case "--dump-after-finalize-ir": dump |= 1<<Phase.FinalizeIR.ordinal() ; break; + case "--dump-after-type-check": dump |= 1<<Phase.TypeCheck .ordinal() ; break; + case "--dump-after-lower": dump |= 1<<Phase.Lower .ordinal() ; break; + case "--dump-after-loop-tree": dump |= 1<<Phase.LoopTree .ordinal() ; break; + case "--dump-after-select": dump |= 1<<Phase.Select .ordinal() ; break; + case "--dump-after-gcm": dump |= 1<<Phase.Schedule .ordinal() ; break; + case "--dump-after-local-sched": dump |= 1<<Phase.LocalSched.ordinal() ; break; + case "--dump-after-reg-alloc": dump |= 1<<Phase.RegAlloc .ordinal() ; break; + case "--dump-after-encode": dump |= 1<<Phase.Encoding .ordinal() ; break; + case "--dump-after-export": dump |= 1<<Phase.Export .ordinal() ; break; + 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 "--run": do_run = true ; break; case "--norun": do_run = false; break; - case "--dump-size": do_print_size = true; break; - case "--dump-time": do_print_time = true; break; + case "--main": emit_main = true; break; + case "--dump-size": print_size = true; break; + case "--dump-time": print_time = true; break; + case "-o": if (out != null || i + 1 >= args.length || args[i + 1].charAt(0) == '-') bad_usage(); + out = args[++i]; do_codegen=true; + break; + case "--root": if( root!=null || i+1>=args.length ) bad_usage(); + root=args[++i]; + break; case "--cpu": if (cpu != null || i + 1 >= args.length || args[i + 1].charAt(0) == '-') bad_usage(); cpu = args[++i]; break; @@ -159,58 +157,97 @@ Options: } } + // Break the paths into these parts: + // - A module path, given from "--root" or inferred from the one input file + // - Some sources [modpath/relpath/file.smp]*, all relative to the module path. + // - A build path, given from "-o" or inferred from the one input file + // + // All the sources are compiled, with outputs in [buildpath/relpath/file.o]* + if (input_filename == null) throw bad("no input file' (use --help)"); if( !input_filename.isEmpty() && !input_filename.endsWith(".smp") ) throw bad("File extension must be .smp"); - String base = input_filename.substring(0,input_filename.length()-4); + Path inPath = Path.of(input_filename); + + // Figure out the module path + Path modPath; + if( root == null ) { + modPath = inPath.getParent(); + if( modPath == null ) + modPath = Path.of("."); + } else { + modPath=Path.of(root); + inPath=modPath.resolve(inPath); + } + + // Compute the module-relative paths for all sources + Path srcRelPath = modPath.relativize(inPath); + int n = srcRelPath.getNameCount(); + Path relPath = n==1 ? null : srcRelPath.subpath(0,n-1); + // Compute source/class name + String srcNameExt = srcRelPath.getName(n-1).toString(); + String srcName = srcRelPath.toString().replace('\\','/'); + srcName=srcName.substring(0,srcName.length()-4); + + // Compute the build path. + Path outPath; + if( out == null ) { + // If no output given, the build dir is the modPath + outPath = modPath; + } else { + // If the output is a directory, that is our out path + outPath = Path.of(out); + if( !out.endsWith("/") ) { + // If the output is a file, the directory is our outPath + Path fullOut = outPath; + outPath = outPath.getParent(); + if( outPath == null ) + outPath = Path.of("."); + Path foo = outPath.relativize(fullOut); + if( !foo.toString().equals(srcName+".o") ) + throw bad("Output path needs to be of the form "+srcName+".o"); + } + } + String outBase = outPath.resolve(srcName).toString(); - if (do_run || (dump & DUMP_DISASSEMBLE) != 0 || do_print_size) { + if( do_run || print_asm || print_size ) { if (cpu == null) cpu = TestC.CPU_PORT; if (abi == null) abi = TestC.CALL_CONVENTION; do_codegen = true; } + if( cpu == null ) cpu = TestC.CPU_PORT; + if( abi == null ) abi = TestC.CALL_CONVENTION; // Read input file - try { - src = input_filename.isEmpty() ? getInput() : Files.readString(Path.of(input_filename)); - } catch( IOException e ) { throw bad("Cannot read input file: "+input_filename); } + src = input_filename.isEmpty() ? getInput() : null; // Compilation pipeline - CodeGen code = new CodeGen(src); - - code.driver(do_codegen ? Phase.Encoding : Phase.LocalSched, - do_codegen ? cpu : null, abi, dump); - if( do_codegen ) code.exportELF(base+".o"); - - dump(code, dump, DUMP_FINAL); + CodeGen code = new CodeGen(modPath.toString(), outPath.toString(), 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); - if (do_codegen && (dump & DUMP_DISASSEMBLE) != 0) { + if( do_codegen && print_asm ) System.out.println(code.asm()); - } - if (do_print_size) { - System.out.printf( "Code Size: %d%n", code._encoding._bits.size()); - } + if( print_size ) + System.out.printf( "Code Size: %d%n", code._encoding._bits.size()); - if (do_print_time) { + if( print_time ) print_compilation_times(code); - } - if (do_eval) { + if( do_eval ) { // TODO: Support for evaluation of functions with different argument numbers and types long t = System.currentTimeMillis(); long arg = (i+1 < args.length) ? Integer.parseInt(args[i+1]) : 0; System.out.println(Eval2.eval(code, arg, 100000)); - if (do_print_time) { - System.out.printf( "EXECUTION TIME: %.3f sec%n", - (System.currentTimeMillis() - t) / 1e3); - } + if( print_time ) + System.out.printf( "EXECUTION TIME: %.3f sec%n", (System.currentTimeMillis() - t) / 1e3); } - if (do_run) { + if( do_run ) { if ( !TestC.CPU_PORT.equals( cpu ) || !TestC.CALL_CONVENTION.equals( abi ) ) throw bad("cannot run code on not native target"); - String exe = TestC.OS.startsWith("Windows") ? base+".exe" : base; - String result = TestC.gcc(base+".o", null, null, true, exe); + String exe = TestC.OS.startsWith("Windows") ? outBase+".exe" : outBase; + String result = TestC.gcc(outBase+".o", null, null, "", TestC.linkObjs(null), exe); System.out.print(result); } } diff --git a/src/test/java/com/seaofnodes/simple/SpillStats.java b/src/test/java/com/seaofnodes/simple/SpillStats.java index ad044a52..944f7ef5 100644 --- a/src/test/java/com/seaofnodes/simple/SpillStats.java +++ b/src/test/java/com/seaofnodes/simple/SpillStats.java @@ -1,13 +1,15 @@ package com.seaofnodes.simple; import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; +import com.seaofnodes.simple.type.TypeInteger; +import java.nio.file.*; import java.util.Map; import java.util.TreeMap; import org.junit.runner.*; import org.junit.runner.notification.*; -// Run existing tests and sum actual allocations, including ones without goldens. -// Helpers call record after allocation; ordinary JUnit runs collect nothing. +// Earlier cohorts freeze source/target membership; Chapter 25 retains native checks. public class SpillStats extends RunListener { private static SpillStats ACTIVE; private Description _test; @@ -15,30 +17,25 @@ public class SpillStats extends RunListener { private final Map<String,long[]> _totals = new TreeMap<>(); @Override public void testStarted(Description test) { _test = test; } - public static void record(CodeGen code, String cohort, String cpu, String abi) { - if( ACTIVE==null ) return; - String test = ACTIVE._test.getTestClass().getSimpleName()+"."+ACTIVE._test.getMethodName(); + @Override public void testFinished(Description test) { _test = null; } + + public static void record25(CodeGen code) { + if( ACTIVE==null || ACTIVE._test==null || ACTIVE._test.getTestClass()!=ModulesTest.class ) return; + ACTIVE.record(code,"Chapter25",ACTIVE._test.getMethodName(),code._mach.getClass().getSimpleName(),code._callingConv); + } + + private void record(CodeGen code, String cohort, String test, String cpu, String abi) { for( String key : new String[]{cohort+","+cpu+","+abi,cohort+",ALL,ALL"} ) { - long[] sum = ACTIVE._totals.computeIfAbsent(key,k -> new long[3]); + long[] sum = _totals.computeIfAbsent(key,k -> new long[3]); sum[0]++; sum[1] += code._regAlloc._spills; sum[2] += code._regAlloc._spillScaled; } System.out.println("allocation,"+cohort+","+test+","+cpu+","+abi+","+ code._regAlloc._spills+","+code._regAlloc._spillScaled); } - // The same native helpers serve old and new program cohorts. - public static void recordNative(CodeGen code, String cpu, String abi) { - String cohort = ACTIVE!=null && (ACTIVE._test.getTestClass()==Chapter24Test.class || ACTIVE._test.getTestClass()==Chapter24AllocTest.class) - ? "Chapter24" : ACTIVE!=null && ACTIVE._test.getTestClass()==Chapter23Test.class - ? "Chapter23" : ACTIVE!=null && ACTIVE._test.getTestClass()==Chapter22Test.class - ? "Chapter22" : "Chapter21"; - record(code,cohort,cpu,abi); - } - - // Keep running every CPU and native result check when measuring quality. - // A changed golden still makes spill-stats fail at the end. + // Keep native result checks running during a quality comparison. public static void checkSpills(int expected, int actual) { - if( expected==-1 ) return; + if( expected==-1 || CodeGen.iterSeedOverridden() ) return; try { org.junit.Assert.assertEquals("Expect spills:",expected,actual,Math.max(1,expected>>3)); } catch( AssertionError error ) { if( ACTIVE==null ) throw error; @@ -47,27 +44,49 @@ public class SpillStats extends RunListener { } } - public static void main(String[] args) throws ClassNotFoundException { - if( args.length==0 ) args = new String[]{"Chapter20Test","Chapter21Test","Chapter21AllocTest","Chapter22Test","Chapter23Test","Chapter23AllocTest","Chapter24Test","Chapter24AllocTest","BrainFuckTest","MergeSortTest"}; - Class<?>[] tests = new Class<?>[args.length]; - for( int i=0; i<args.length; i++ ) - tests[i] = Class.forName("com.seaofnodes.simple."+args[i]); + public static void main(String[] args) throws Exception { ACTIVE = new SpillStats(); + Path dir = Path.of("src/test/java/com/seaofnodes/simple/spill"); + int failures=0, attempts=0; + for( String line : Files.readAllLines(dir.resolve("cohorts.tsv")) ) { + if( line.startsWith("#") || line.isBlank() ) continue; + attempts++; + String[] row = line.split("\t"); + try { + String src = Files.readString(dir.resolve(row[5])); + CodeGen code = new CheckedCodeGen(null,null,null,src,123L,true,TypeInteger.BOT) + .driver(CodeGen.Phase.RegAlloc,row[2],row[3]); + ACTIVE.record(code,row[0],row[1],row[2],row[3]); + } catch( Throwable error ) { + failures++; + System.err.println(line); + error.printStackTrace(); + } + } + // Include the library's allocation even when make's sys.o is up to date. + try { + CodeGen code = new CheckedCodeGen("src/main/smp","build/spill-sys","sys", + Files.readString(Path.of("src/main/smp/sys.smp")),456L,true,TypeInteger.BOT) + .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION); + ACTIVE.record(code,"Chapter25","sys",TestC.CPU_PORT,TestC.CALL_CONVENTION); + } catch( Throwable error ) { + failures++; + error.printStackTrace(); + } JUnitCore junit = new JUnitCore(); junit.addListener(ACTIVE); - Result result = junit.run(tests); - boolean passed = result.wasSuccessful() && ACTIVE._qualityFailures==0; - if( !result.wasSuccessful() ) System.out.println("FAILED test run: totals are not a complete comparison."); - else if( !passed ) System.out.println("Spill expectations differ; allocation and runtime checks passed."); + Result result = junit.run(ModulesTest.class); + for( Failure failure : result.getFailures() ) System.err.println(failure.getTrace()); + failures += result.getFailureCount(); + if( failures!=0 ) System.out.println("FAILED: totals are not a complete comparison."); System.out.println("cohort,cpu,abi,allocations,splits,scaled"); for( var row : ACTIVE._totals.entrySet() ) { long[] sum = row.getValue(); System.out.println(row.getKey()+","+sum[0]+","+sum[1]+","+sum[2]); } - for( Failure failure : result.getFailures() ) System.err.println(failure.getTrace()); - System.out.println("Tests: "+result.getRunCount()+", failures: "+result.getFailureCount()); - System.out.println("Spill golden failures: "+ACTIVE._qualityFailures); + System.out.println("Historical allocations: "+attempts+"; native tests: "+result.getRunCount()+"; failures: "+failures+"; spill golden failures: "+ACTIVE._qualityFailures); + failures += ACTIVE._qualityFailures; ACTIVE = null; - if( !passed ) System.exit(1); + if( failures!=0 ) System.exit(1); } } diff --git a/src/test/java/com/seaofnodes/simple/StaticDataTest.java b/src/test/java/com/seaofnodes/simple/StaticDataTest.java new file mode 100644 index 00000000..b8f5f1d1 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/StaticDataTest.java @@ -0,0 +1,172 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.StaticData; +import com.seaofnodes.simple.node.ConstantNode; +import com.seaofnodes.simple.node.StoreNode; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeStruct; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import com.seaofnodes.isa.eval.EvalArm64; +import com.seaofnodes.isa.eval.EvalRisc5; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; + +public class StaticDataTest { + private static final String SRC="val n=42; val f={int x -> x+n;}; return 0;"; + + private static StaticData.ObjectData root(CodeGen code) { + for( var obj : code._encoding._data.entries() ) + if( obj.symbol.endsWith("Test.$class") ) return obj; + throw new AssertionError("Missing Test class object"); + } + + // Read the image before <clinit>, then call the function through its data slot. + @Test public void testEmulatorImage() { + for( String cpu : new String[]{"arm","riscv"} ) { + CodeGen code=new CodeGen(SRC).driver(CodeGen.Phase.TypeCheck); + assertNull("Constant fields fold before instruction selection", + code._stop.walk(n -> n instanceof StoreNode st && + (st._name.equals("n") || st._name.equals("f")) ? n : null)); + assertNotNull(code._stop.walk(n -> n instanceof ConstantNode && + n._type instanceof TypeMem mem && mem.isConstant() && + mem._t==TypeInteger.constant(42) ? n : null)); + assertNotNull("The runtime initialization flag still needs its Store", + code._stop.walk(n -> n instanceof StoreNode st && + st._name.equals("$init") ? n : null)); + code.driver(cpu,"SystemV",true,false); + var e=code._encoding; + var obj=root(code); + TypeStruct ts=(TypeStruct)obj.type; + int cp=(e._bits.size()+15)&-16, sd=(cp+e._cpool.size()+15)&-16; + byte[] image=new byte[1<<20]; + System.arraycopy(e._bits.buf(),0,image,0,e._bits.size()); + System.arraycopy(e._cpool.buf(),0,image,cp,e._cpool.size()); + System.arraycopy(e._sdata.buf(),0,image,sd,e._sdata.size()); + int n=sd+obj.offset+ts.offset(ts.find("n")); + int f=sd+obj.offset+ts.offset(ts.find("f")); + if( cpu.equals("arm") ) { + var arm=new EvalArm64(image,1<<16); + assertEquals(42,arm.ld4s(n)); + arm._pc=arm.ld4s(f); + arm.regs[0]=7; + assertEquals(0,arm.step(1000)); + assertEquals(49,arm.regs[0]); + } else { + var r5=new EvalRisc5(image,1<<16); + assertEquals(42,r5.ld4s(n)); + r5._pc=r5.ld4s(f); + r5.regs[10]=7; + assertEquals(0,r5.step(1000)); + assertEquals(49,r5.regs[10]); + } + } + } + + @Test public void testNativeImage() throws Exception { + Path dir=Path.of("build/objs/static-data"); + Files.createDirectories(dir); + CodeGen code=new CodeGen(null,dir.toString(),"Test",SRC,123L,true, + TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + TypeStruct ts=(TypeStruct)root(code).type; + Path driver=dir.resolve("driver.c"); + Files.writeString(driver,""" + #include <stdint.h> + #include <string.h> + extern unsigned char data[] __asm__("Test.$class"); + typedef int64_t (__attribute__((CALL_CONV)) *fun)(int64_t); + int main(void) { + int64_t n; uint32_t f; + memcpy(&n,data+%d,sizeof(n)); + memcpy(&f,data+%d,sizeof(f)); + return n!=42 || ((fun)(uintptr_t)f)(7)!=49; + } + """.formatted(ts.offset(ts.find("n")),ts.offset(ts.find("f")))); + String exe=dir.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); + TestC.linkExe(dir.resolve("Test.o").toString(), + TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi", + driver.toString(),null,exe); + assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); + } + + // Native entries must agree too, both in static fields (before <clinit>) + // and when code materializes the address. The native body may live above 4G. + @Test public void testNativeFunctionIdentity() throws Exception { + Path dir=Path.of("build/objs/static-native-functions"); + Files.createDirectories(dir); + int[] offsets=new int[2]; + for( int i=0; i<2; i++ ) { + String name=i==0 ? "A" : "B"; + String src="{ i32 -> i32 } abs = \"C\"; val get"+name+"={ -> abs; }; return 0;"; + CodeGen code=new CodeGen(null,dir.toString(),name,src,123L,true,TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); + assertNull("The native address is initialized in the static image", + code._stop.walk(n -> n instanceof StoreNode st && st._name.equals("abs") ? n : null)); + code.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + for( var obj : code._encoding._data.entries() ) + if( obj.symbol.equals(name+".$class") ) { + TypeStruct ts=(TypeStruct)obj.type; + offsets[i]=ts.offset(ts.find("abs")); + } + } + Path driver=dir.resolve("driver.c"); + Files.writeString(driver,""" + #include <stdint.h> + #include <string.h> + extern unsigned char a[] __asm__("A.$class"), b[] __asm__("B.$class"); + typedef int32_t (__attribute__((CALL_CONV)) *fun)(int32_t); + __attribute__((CALL_CONV)) extern fun getA(void), getB(void); + int main(void) { + uint32_t x,y; + memcpy(&x,a+%d,sizeof(x)); + memcpy(&y,b+%d,sizeof(y)); + fun f=(fun)(uintptr_t)x; + if(!x || x!=y || f!=getA() || f!=getB()) return 1; + return f(-7)!=7; + } + """.formatted(offsets[0],offsets[1])); + String exe=dir.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); + String conv=TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi"; + // Either object's weak definition can win; every reference must agree. + for( String first : new String[]{"A","B"} ) { + String second=first.equals("A")?"B":"A"; + TestC.linkExe(dir.resolve(first+".o").toString(),conv,driver.toString(), + new Ary<>(new String[]{dir.resolve(second+".o").toString()}),exe); + assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); + } + } + + // Two source files must reference the same mutable class object. + @Test public void testSharedClass() throws Exception { + Path dir=Path.of("build/objs/static-data-shared"); + Files.createDirectories(dir); + Files.createDirectories(dir.resolve("Main")); + String[] names={"Globals","A","B"}; + String[] sources={"int count=0;", + "val a={ -> Globals; };", "val b={ -> Globals; };"}; + for( int i=0; i<names.length; i++ ) + Files.writeString(dir.resolve("Main/"+names[i]+".smp"),sources[i]); + new CodeGen(dir.toString(),dir.toString(),"Main","val a={ -> A.a(); }; val b={ -> B.b(); };",123L,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + Path driver=dir.resolve("driver.c"); + Files.writeString(driver,""" + #include <stdint.h> + __attribute__((CALL_CONV)) extern int64_t *a(void),*b(void); + int main(void) { + int64_t *x=a(),*y=b(); + if(x!=y) return 1; + *x=37; + return *y!=37; + } + """); + String exe=dir.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); + TestC.linkExe(dir.resolve("Main.o").toString(), + TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi", + driver.toString(),null,exe); + assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); + } +} diff --git a/src/test/java/com/seaofnodes/simple/TestArm64.java b/src/test/java/com/seaofnodes/simple/TestArm64.java index 086bf9c7..f1089cf0 100644 --- a/src/test/java/com/seaofnodes/simple/TestArm64.java +++ b/src/test/java/com/seaofnodes/simple/TestArm64.java @@ -3,13 +3,10 @@ package com.seaofnodes.simple; import com.seaofnodes.isa.eval.EvalArm64; import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; - - import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.FunNode; import com.seaofnodes.simple.node.cpus.arm.arm; -import junit.framework.Assert; -import java.nio.file.Files; -import java.nio.file.Path; +import com.seaofnodes.simple.util.BAOS; import java.io.IOException; import static org.junit.Assert.assertEquals; @@ -17,35 +14,40 @@ import static org.junit.Assert.assertEquals; // Runs 32-bit ARM code in an emulator public class TestArm64 { - public static EvalArm64 build( String file, int arg, int spills, boolean print ) throws IOException { - String dir = "src/test/java/com/seaofnodes/simple/progs"; - String src = Files.readString(Path.of(dir + "/"+file+".smp")); - return build(dir,src, file, arg, spills, print); - } - - public static EvalArm64 build( String name, String src, int arg, int spills, boolean print ) throws IOException { - String dir = "src/test/java/com/seaofnodes/simple/progs"; - return build(dir, src ,name, arg, spills, print); - } - // Compile and run a simple program - public static EvalArm64 build( String dir, String src, String file, int arg, int spills, boolean print ) throws IOException { + public static EvalArm64 build( String main, String src, int arg, int spills, boolean print ) throws IOException { // Compile and export Simple - CodeGen code = new CheckedCodeGen(src).driver("arm", "SystemV",null); - SpillStats.recordNative(code,"arm","SystemV"); - if( print ) { code.print_as_hex(); System.out.print(code.asm()); } + CodeGen code = new CheckedCodeGen(src).driver("arm", "SystemV",true,false); // Allocation quality not degraded - SpillStats.checkSpills(spills,code._regAlloc._spillScaled); + int delta = spills>>3; + if( delta == 0 ) delta =1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) assertEquals("Expect spills:", spills, code._regAlloc._spillScaled, delta); // Image byte[] image = new byte[1<<20]; // A megabyte (1024*1024 bytes) EvalArm64 ARM = new EvalArm64(image, 1<<16); - // Code at offset 0 - System.arraycopy(code._encoding.bits(), 0, image, 0, code._encoding.bits().length); + int start = 0; // Code at offset 0 + int cpool = fill(code._encoding._bits ,image,start); + int sdata = fill(code._encoding._cpool,image,cpool); + int end = fill(code._encoding._sdata,image,sdata); + // Initial incoming int arg ARM.regs[arm.X0] = arg; // malloc memory starts at 64K and goes up + + // Look up starting PC or zero if not given + if( main!=null ) + for( FunNode fun : code._linker ) + if( fun != null && !fun.isDead() && fun._compunit == code.compunit() && main.equals(fun._name) ) + ARM._pc = code._encoding.opStart(fun); + return ARM; } + + // Fill bits into image; return end point rounded up. + private static int fill( BAOS bits, byte[] image, int off) { + System.arraycopy(bits.buf(), 0, image, off, bits.size()); + return (off+bits.size() + 15) & -16; + } } diff --git a/src/test/java/com/seaofnodes/simple/TestC.java b/src/test/java/com/seaofnodes/simple/TestC.java index 3b3aabc0..164168f6 100644 --- a/src/test/java/com/seaofnodes/simple/TestC.java +++ b/src/test/java/com/seaofnodes/simple/TestC.java @@ -5,6 +5,8 @@ import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.util.Ary; import com.seaofnodes.simple.util.Utils; + +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -14,93 +16,171 @@ import static org.junit.Assert.*; // Call a C driver program, which calls a Simple-generated .o, and self-checks. // Expects GCC from the default CLI, compiles C, compiles Simple, links and runs. public abstract class TestC { + public static final int TEST_TIMEOUT_SECONDS = 60; public static final String OS = System.getProperty("os.name"); public static final String CPU = System.getProperty("os.arch"); - public static final String CALL_CONVENTION = OS.startsWith("Windows") ? "Win64" : "SystemV"; + public static final String CALL_CONVENTION = OS.startsWith("Windows") ? "win64" : "SystemV"; public static final String CPU_PORT = switch( CPU ) { case "amd64" -> "x86_64_v2"; default -> throw Utils.TODO("Map Yer CPU Port Here"); }; + public static final String CPU_ABI = CPU_PORT+"_"+CALL_CONVENTION; + public static final String RELEASE_SYS_DIR = "build/release/"+CPU_ABI; - public static void run( String file, int spills ) throws IOException { run(file,"",spills); } - - public static void run( String file, String expected, int spills ) throws IOException { - run("src/test/java/com/seaofnodes/simple/progs",file,expected,spills); + public static String runtimeObject() { + 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(); } - // Compile and run a simple program - public static void run( String dir, String file, TypeInteger arg, String expected, int spills, boolean standalone) throws IOException { - // Files - String cfile = dir+"/"+file+".c" ; - String sfile = dir+"/"+file+".smp"; - String efile = "build/objs/"+file; + public static final String C_DRIVERS_DIR = "src/test/java/com/seaofnodes/simple/progs/"; - // Compile and export Simple - String src = Files.readString(Path.of(sfile)); - run(src,CALL_CONVENTION,arg, "",standalone? null: cfile,efile,"S",expected,spills); + /** + * Compile, link and run + * - WITH a C driver + * - using the default OS/CPU calling convention. + * - no lib sys + */ + public static void runC( String src, String base, String expected, int spills) throws IOException { + String cfile = C_DRIVERS_DIR+base+".c"; + run(src, base, null, CALL_CONVENTION, "", cfile, expected, spills); } - // link with c and also inline - public static void run(String src, String file, TypeInteger arg, String expected, int spills) throws IOException { - String dir = "src/test/java/com/seaofnodes/simple/progs"; - String cfile = dir+"/"+file+".c"; - String efile = "build/objs/"+file; - run(src, CALL_CONVENTION, arg, "", cfile, efile, "S", expected, spills); + /** + * Compile, link and run + * - withOUT a C driver + * - using the default OS/CPU calling convention. + * - no lib sys + */ + public static void runSF( String src, String base, String expected, int spills ) throws IOException { + run(src, base, null, CALL_CONVENTION, null,null, expected,spills); } - public static void run(String dir, String file, String expected, int spills ) throws IOException { - run(dir, file,null, expected, spills, false); + /** + * Compile, link and run + * - withOUT a C driver + * - using the default OS/CPU calling convention. + * - WITH a test lib sys + */ + public static void runSYS( String src, String base, String expected, int spills ) throws IOException { + Ary<String> externPaths = new Ary<>(new String[]{RELEASE_SYS_DIR}); + run(src, base, externPaths, CALL_CONVENTION, null,null, expected,spills); } - // Do not link with c file - no inline. - public static void runS(String file, String expected, int spills ) throws IOException { - run("src/test/java/com/seaofnodes/simple/progs", file, null, expected, spills, true); - } + /** + * Compile, link and run - general case + * + * @param src Program source to compile + * @param base Compiled binary final base name + * @param externPaths External symbol search path + * @param simple_conv Argument convention when calling Simple code + * @param c_conv Argument convention when calling C code, or null if not + * linking against a C driver program + * @param cfile Associated C program which will drive the Simple program, + * or null if not linking against a C driver program + * @param expected Expected stdout string + * @param spills Expected limit on spill code, used to validate the + * register allocation is reasonable + */ + public static void run( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv, String cfile, String expected, int spills ) throws IOException { + run(src, base, externPaths, simple_conv, c_conv, cfile, null, expected, spills); + } - // Do not link with c file - just inline with source. - public static void runSF(String name, String src, TypeInteger arg, String expected, int spills ) throws IOException { - String efile = "build/objs/"+name; - run(src,CALL_CONVENTION,arg, "",null,efile,"S",expected,spills); + public static void runArgs( String src, String base, Ary<String> externPaths, String simple_conv, + String expected, int spills, String... programArgs ) throws IOException { + run0(src,base,externPaths,simple_conv,null,null,null,expected,spills,programArgs); } - public static void runS(String file, TypeInteger arg, String expected, int spills ) throws IOException { - run("src/test/java/com/seaofnodes/simple/progs", file, arg, expected, spills, true); + public static void run( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv, String cfile, String stdin, String expected, int spills ) throws IOException { + run0(src,base,externPaths,simple_conv,c_conv,cfile,stdin,expected,spills,new String[0]); } + private static void run0( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv, + String cfile, String stdin, String expected, int spills, String[] programArgs ) throws IOException { + String exe = compile(src,base,externPaths,simple_conv,c_conv,cfile,spills); - public static void run( String src, String simple_conv, TypeInteger arg, String c_conv, String cfile, String efile, String xtn, String expected, int spills ) throws IOException { - String bin = efile+xtn; - String obj = bin+".o"; - String exe = OS.startsWith("Windows") ? bin+".exe" : bin; + String[] execArgs = new String[programArgs.length+1]; + execArgs[0] = exe; + System.arraycopy(programArgs,0,execArgs,1,programArgs.length); + String result = execStdin(stdin, 0, execArgs ); + assertEquals(expected,result); + } + + public static String compile( String src, String base, Ary<String> externPaths, String simple_conv, String c_conv, + String cfile, int spills ) throws IOException { + // Simple file base-name example: + // foo.smp -> + // build/objs/foo.o - object file + // build/objs/foo.exe - linked executable Windows + // build/objs/foo - linked executable Linux + String pathBase = "build/objs/"+base; + String obj = pathBase+".o"; + String exe = pathBase+(OS.startsWith("Windows") ? ".exe" : ""); // Compile simple, emit ELF - CodeGen code = new CheckedCodeGen(src, arg).driver( CPU_PORT, simple_conv, obj); - SpillStats.recordNative(code,CPU_PORT,simple_conv); + CodeGen code = new CheckedCodeGen(null,"build/objs",base,src,126L,true,TypeInteger.BOT); + code.driver( CPU_PORT, simple_conv, false, cfile==null ); - String result = gcc(obj, c_conv, cfile, false, exe ); - assertEquals(expected,result); + linkExe(obj, c_conv, cfile, linkObjs(externPaths), exe); // Allocation quality not degraded SpillStats.checkSpills(spills,code._regAlloc._spillScaled); + return exe; } + // Link with gcc, and execute the resulting binary, returning stdout as a + // String. Any errors *assert* instead of returning some error code; this + // utility is meant to execute inside a JUnit test which will catch the + // assert and flag the test as failed. + public static String gcc( String main, double ignore, String... objs ) throws IOException { + String BLDDIR = "build/objs/test0/"; + String exe = BLDDIR+main+(OS.startsWith("Windows") ? ".exe" : ""); + linkExe(objs[0],null,null,null,exe); + // Module tests intentionally use the program exit code as their result. + ExecResult result = execute(null,0,exe); + return result.exit==0 ? result.out : "exec exit code: "+result.exit; + } + + // Link with gcc, and execute the resulting binary, returning stdout as a + // String. Any errors *assert* instead of returning some error code; this + // utility is meant to execute inside a JUnit test which will catch the + // assert and flag the test as failed. public static String gcc( String obj, String c_conv, String cfile, boolean stdin, String... args ) throws IOException { + return gcc(obj,c_conv,cfile,stdin ? "" : null,null,args); + } + + public static String gcc( String obj, String c_conv, String cfile, String stdin, String... args ) throws IOException { + return gcc(obj,c_conv,cfile,stdin,null,args); + } + + public static String gcc( String obj, String c_conv, String cfile, String stdin, Ary<String> linkObjs, String... args ) throws IOException { + linkExe(obj,c_conv,cfile,linkObjs,args[0]); + return execStdin(stdin,0,args); + } + public static void linkExe( String obj, String c_conv, String cfile, Ary<String> linkObjs, 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 // pointers as 4bytes. var params = new Ary<>(String.class); params.add("gcc"); + params.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie"); if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main` + params.add(obj); + // 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()); + if( linkObjs != null ) + params.addAll(linkObjs.asAry()); params.addAll(new String[] { - obj, - "-lm", // Picks up 'sqrt' for newtonFloat tests to compare - "-g", - "-o", - args[0], + "-lm", // Picks up 'sqrt' for newtonFloat tests to compare + "-g", + "-o", + exe, }); // Calling convention for C calls, if any if( cfile!=null ) { @@ -109,29 +189,85 @@ public abstract class TestC { } // Run GCC to link (optionally compile C driver code) - Process gcc = new ProcessBuilder(params.asAry()).redirectErrorStream(true).start(); - int exit; - try { - boolean normal = gcc.waitFor(2, TimeUnit.SECONDS); - exit = normal ? gcc.exitValue() : -1; // no exit??? - } catch( InterruptedException e ) { - throw new IOException("interrupted"); - } - String result = new String(gcc.getInputStream().readAllBytes()); - if( exit!=0 ) { - System.err.println("gcc error code: "+exit); - System.err.println(result); - } - assertEquals( 0, exit ); - //assertTrue(result.isEmpty()); // No data in error stream + exec(TEST_TIMEOUT_SECONDS,params.asAry()); + } + + public static String exec( String... args ) throws IOException { + return execStdin(null,0,args); + } + + // Tests can bound execution without imposing a deadline on the CLI driver. + public static String exec( int seconds, String... args ) throws IOException { + return execStdin(null,seconds,args); + } + private static String execStdin( String stdin, int seconds, String... args ) throws IOException { + ExecResult result = execute(stdin,seconds,args); + if( result.exit!=0 ) + throw new IOException(String.join(" ",args)+"\nexec exit code: "+result.exit+ + "\nstdout:\n"+result.out+"\nstderr:\n"+result.err); + return result.out; + } + + private record ExecResult(int exit, String out, String err) {} + + private static ExecResult execute( String stdin, int seconds, String... args ) throws IOException { // Execute results ProcessBuilder smp = new ProcessBuilder(args); - if( stdin ) smp.redirectInput(ProcessBuilder.Redirect.INHERIT); - Process p = smp.start(); - try { exit = p.waitFor(); } catch( InterruptedException e ) { throw new IOException("interrupted"); } - result = new String(p.getInputStream().readAllBytes()); - assertEquals("Program exit status",0,exit); - return result; + Path stdinFile = null; + Path stdoutFile = Files.createTempFile("simple-stdout",".txt"); + Path stderrFile = Files.createTempFile("simple-stderr",".txt"); + Process p = null; + try { + smp.redirectOutput(stdoutFile.toFile()); + smp.redirectError(stderrFile.toFile()); + if( stdin!=null && stdin.isEmpty() ) smp.redirectInput(ProcessBuilder.Redirect.INHERIT); + else if( stdin!=null ) { + stdinFile = Files.createTempFile("simple-stdin",".txt"); + Files.writeString(stdinFile,stdin); + smp.redirectInput(stdinFile.toFile()); + } + p = smp.start(); + if( stdin==null ) + p.getOutputStream().close(); + if( seconds>0 && !p.waitFor(seconds,TimeUnit.SECONDS) ) { + String process = "pid="+p.pid()+", cpu="+ + p.info().totalCpuDuration().map(Object::toString).orElse("unknown"); + p.destroyForcibly(); + boolean stopped = p.waitFor(5,TimeUnit.SECONDS); + throw new IOException(String.join(" ",args)+"\ntimed out after "+seconds+" seconds ("+process+")"+ + (stopped ? "" : "; process did not terminate")+ + "\nstdout:\n"+Files.readString(stdoutFile)+ + "\nstderr:\n"+Files.readString(stderrFile)); + } + // Windows statuses have 32 bits; narrowing can turn a failure into 0. + int exit = seconds>0 ? p.exitValue() : p.waitFor(); + return new ExecResult(exit,Files.readString(stdoutFile),Files.readString(stderrFile)); + } catch( InterruptedException e ) { + if( p!=null ) p.destroyForcibly(); + Thread.currentThread().interrupt(); + throw new IOException("interrupted",e); + } finally { + if( stdinFile!=null ) Files.deleteIfExists(stdinFile); + Files.deleteIfExists(stdoutFile); + Files.deleteIfExists(stderrFile); + } + } + + static Ary<String> linkObjs(Ary<String> externPaths) { + if( externPaths == null ) + return null; + Ary<String> objs = new Ary<>(String.class); + for( String path : externPaths ) { + File file = new File(path); + if( file.isFile() && file.getName().endsWith(".o") ) + objs.add(file.toString()); + else if( file.isDirectory() ) { + File sys = new File(file,"sys.o"); + if( sys.isFile() ) + objs.add(sys.toString()); + } + } + return objs.isEmpty() ? null : objs; } } diff --git a/src/test/java/com/seaofnodes/simple/TestRisc5.java b/src/test/java/com/seaofnodes/simple/TestRisc5.java index f55e7d0b..10c75d3f 100644 --- a/src/test/java/com/seaofnodes/simple/TestRisc5.java +++ b/src/test/java/com/seaofnodes/simple/TestRisc5.java @@ -4,54 +4,52 @@ import com.seaofnodes.isa.eval.EvalRisc5; import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.Encoding; import com.seaofnodes.simple.node.FunNode; -import com.seaofnodes.simple.node.Node; import com.seaofnodes.simple.node.cpus.riscv.riscv; +import com.seaofnodes.simple.util.BAOS; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; + import static org.junit.Assert.*; // Runs 32-bit R5 code in an emulator public abstract class TestRisc5 { - public static EvalRisc5 build( String file, int arg, int spills, boolean print ) throws IOException { - String dir = "src/test/java/com/seaofnodes/simple/progs"; - String src = Files.readString(Path.of(dir + "/"+file+".smp")); - return build(dir,src, file, null, arg, spills, print); - } - - public static EvalRisc5 build( String name, String src, int arg, int spills, boolean print ) throws IOException { - String dir = "src/test/java/com/seaofnodes/simple/progs"; - return build(dir, src ,name, null, arg, spills, print); - } - - // Compile and run a simple program - public static EvalRisc5 build( String dir, String src, String file, String main, int arg, int spills, boolean print ) throws IOException { + public static EvalRisc5 build( String src, String main, int arg, int spills, boolean print ) throws IOException { // Compile and export Simple - CodeGen code = new CheckedCodeGen(src).driver("riscv", "SystemV",null); - SpillStats.recordNative(code,"riscv","SystemV"); - if( print ) { code.print_as_hex(); System.out.print(code.asm()); } + CodeGen code = new CheckedCodeGen(src).driver("riscv", "SystemV",true,false); // Allocation quality not degraded - SpillStats.checkSpills(spills,code._regAlloc._spillScaled); + int delta = spills>>3; + if( delta==0 ) delta = 1; + if( spills != -1 && !CodeGen.iterSeedOverridden() ) + assertEquals("Expect spills:",spills,code._regAlloc._spillScaled,delta); // Image byte[] image = new byte[1<<20]; // A megabyte (1024*1024 bytes) EvalRisc5 R5 = new EvalRisc5(image, 1<<16); - // Code at offset 0 - System.arraycopy(code._encoding.bits(), 0, image, 0, code._encoding.bits().length); + int start = 0; // Code at offset 0 + int cpool = fill(code._encoding._bits ,image,start); + int sdata = fill(code._encoding._cpool,image,cpool); + int end = fill(code._encoding._sdata,image,sdata); + // Initial incoming int arg R5.regs[riscv.A0] = arg; // malloc memory starts at 64K and goes up // Look up starting PC or zero if not given if( main!=null ) - for( Node use : code._start._outputs ) - if( use instanceof FunNode fun && main.equals(fun._name) ) - R5._pc = code._encoding._opStart[fun._nid]; + for( FunNode fun : code._linker ) + if( fun != null && !fun.isDead() && fun._compunit == code.compunit() && main.equals(fun._name) ) + R5._pc = code._encoding.opStart(fun); return R5; } + // Fill bits into image; return end point rounded up. + private static int fill( BAOS bits, byte[] image, int off) { + System.arraycopy(bits.buf(), 0, image, off, bits.size()); + return (off+bits.size() + 15) & -16; + } + } diff --git a/src/test/java/com/seaofnodes/simple/TestRunner.java b/src/test/java/com/seaofnodes/simple/TestRunner.java new file mode 100644 index 00000000..5389967e --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/TestRunner.java @@ -0,0 +1,17 @@ +package com.seaofnodes.simple; + +import org.junit.internal.TextListener; +import org.junit.runner.JUnitCore; +import org.junit.runner.Request; + +/** Make's single-class/single-method JUnit entry point. */ +public class TestRunner { + public static void main(String[] args) throws ClassNotFoundException { + String name=args[0]; + Class<?> test=Class.forName(name.contains(".") ? name : "com.seaofnodes.simple."+name); + Request request=args[1].isEmpty() ? Request.aClass(test) : Request.method(test,args[1]); + JUnitCore runner=new JUnitCore(); + runner.addListener(new TextListener(System.out)); + System.exit(runner.run(request).wasSuccessful() ? 0 : 1); + } +} diff --git a/src/test/java/com/seaofnodes/simple/TypeTest.java b/src/test/java/com/seaofnodes/simple/TypeTest.java index c7ba8caa..f6099607 100644 --- a/src/test/java/com/seaofnodes/simple/TypeTest.java +++ b/src/test/java/com/seaofnodes/simple/TypeTest.java @@ -27,11 +27,11 @@ public class TypeTest { @Test public void testCyclicLeafKinds() { TypeStruct bottom = TypeStruct.make("B14LeafKinds",false, - Field.make("x",Type.BOTTOM,2,false, false)); + Field.make("x",Type.BOTTOM,2,false)); TypeStruct one = TypeStruct.make("B14LeafKinds",false, - Field.make("x",TypeFloat.constant(1.0),2,false, false)); + Field.make("x",TypeFloat.constant(1.0),2,false)); TypeStruct two = TypeStruct.make("B14LeafKinds",false, - Field.make("x",TypeFloat.constant(2.0),2,false, false)); + Field.make("x",TypeFloat.constant(2.0),2,false)); // Meeting creates a new struct while cyclic equality is active. Its // float leaf must compare unequal to the previously interned BOTTOM. @@ -39,29 +39,25 @@ public class TypeTest { assertSame(TypeFloat.F32,meet._fields[0]._t); assertSame(meet,two.meet(one)); assertSame(meet,TypeStruct.make("B14LeafKinds",false, - Field.make("x",TypeFloat.F32,2,false, false))); + Field.make("x",TypeFloat.F32,2,false))); assertSame(bottom,meet.meet(bottom)); assertSame(meet.dual(),one.dual().join(two.dual())); } - // Test basic properties and GLB + // Test basic type properties @Test public void testTypeAdHoc() { Assert.assertEquals( TypeScalar.BOT, TypeInteger.TRUE.meet(TypeNil.NIL) ); Assert.assertEquals( TypeScalar.BOT, TypeInteger.TOP .meet(TypeNil.NIL) ); TypeStruct s1 = TypeStruct.make("s1",false, - Field.make("a", TypeInteger.BOT,-1, false, false), - Field.make("b", TypeInteger.BOT,-2, false, false) ); + Field.make("a", TypeInteger.BOT,-1, false), + Field.make("b", TypeInteger.BOT,-2, false) ); TypeStruct s2 = TypeStruct.make("s2",false, - Field.make("a", TypeInteger.BOT,-3, false, false), - Field.make("b", TypeInteger.BOT,-4, false, false) ); - TypeStruct x1ro = (TypeStruct)s1.makeRO(); + Field.make("a", TypeInteger.BOT,-3, false), + Field.make("b", TypeInteger.BOT,-4, false) ); TypeMemPtr p1 = TypeMemPtr.make(s1); - Assert.assertEquals(x1ro, ((TypeMemPtr)p1.glb(false))._obj); Assert.assertNotEquals(s1, s1.dual()); - TypeStruct s1dglb = ((TypeMemPtr)p1.dual().glb(false))._obj; - Assert.assertTrue(x1ro.isa(s1dglb)); TypeMem m1 = TypeMem.make(2,TypeNil.NIL); TypeMem m2 = TypeMem.make(3,TypeInteger.U16); @@ -73,13 +69,12 @@ public class TypeTest { Assert.assertNotEquals(m3, m4); Assert.assertEquals(TypeStruct.BOT, s1.meet(s2)); - Assert.assertEquals(TypeMem .BOT, m1.meet(m2)); - Assert.assertEquals(TypeMem.make(1,Type.BOTTOM), m1.meet(m3)); - Assert.assertEquals(TypeMem .BOT, m3.meet(m4)); + TypeMem scalarMemBot = TypeMem.make(1,TypeScalar.BOT,false,false,false,XInt.FULL,XInt.FULL); + Assert.assertEquals(scalarMemBot, ((TypeMem)m1.meet(m2)).makeFrom( XInt.FULL, XInt.FULL)); + Assert.assertEquals(TypeMem.make(1,TypeScalar.BOT), m1.meet(m3)); + Assert.assertEquals(scalarMemBot, ((TypeMem)m3.meet(m4)).makeFrom( XInt.FULL, XInt.FULL)); - Assert.assertEquals(TypeMem.make(2,Type.BOTTOM), m1.glb(false)); - Assert.assertEquals(TypeMem.make(2,Type.XNIL), m1.dual()); - Assert.assertEquals(m4.dual(), m4.glb(false).dual()); + Assert.assertEquals(TypeMem.make(2,Type.XNIL,true,true,true, XInt.FULL, XInt.FULL), m1.dual()); TypeMemPtr ptr1 = TypeMemPtr.make(s1); Assert.assertEquals(s1, ptr1._obj); @@ -94,15 +89,8 @@ public class TypeTest { Assert.assertEquals(s2, ptr2nil._obj); Assert.assertNotEquals(ptr1, ptr2); - Type p1glb = ptr1.glb(false); - Assert.assertNotEquals(ptr1, p1glb ); - Assert.assertTrue(((TypeMemPtr)p1glb)._ro); - Assert.assertTrue(ptr1nil.isa(p1glb)); - Assert.assertEquals(ptr1, ptr1.dual().dual()); - Assert.assertTrue(p1glb.makeRO().isa( ptr1.dual().glb(false))); Assert.assertEquals(TypeMemPtr.makeNullable(TypeStruct.BOT).withAccess(false), ptr1.meet(ptr2nil)); - Assert.assertEquals(ptr1nil, ptr1.meet(TypeNil.NIL)); TypeMemPtr TOP = TypeMemPtr.TOP; TypeMemPtr BOT = TypeMemPtr.makeNullable(TypeStruct.BOT); @@ -120,7 +108,7 @@ public class TypeTest { Type nullableptr1_dual = ptr1nil.dual(); // Cyclic check - TypeStruct S1 = ((TypeMemPtr)TypeStruct.SFLT2.field("s1")._t)._obj; + TypeStruct S1 = ((TypeMemPtr)TypeStruct.SFLT1.field("s1")._t)._obj.close(); Assert.assertFalse(s1.isFinal()); Type s1ro = S1.makeRO(); Type s1ro2 = S1.makeRO(); @@ -128,9 +116,6 @@ public class TypeTest { Assert.assertTrue(s1ro.isFinal()); Assert.assertFalse(S1.isConstant()); - Type s1glb = S1.field("s2")._t.glb(false); - Type s1glb2 = S1.field("s2")._t.glb(false); - Assert.assertSame(s1glb,s1glb2); } // Test theoretical properties. @@ -163,7 +148,7 @@ public class TypeTest { Type t02 = t0.join(t2); Type t12 = t1.join(t2); Type mt = t02.meet(t12); - assertSame(mt,t12); + assertSame(t0+" isa "+t1+" joined "+t2, mt,t12); } } @@ -189,8 +174,8 @@ public class TypeTest { Type d1 = t1.dual(); Type ta = dm.meet(d1); Type tb = dm.meet(d0); - assertSame(ta,d1); - assertSame(tb,d0); + assertSame(t0+" & "+t1+" mt="+mt, ta,d1); + assertSame(t0+" & "+t1+" mt="+mt, tb,d0); } private static void assoc( Type t0, Type t1, Type t2 ) { @@ -198,31 +183,23 @@ public class TypeTest { Type t12 = t1 .meet(t2 ); Type t01_2 = t01.meet(t2 ); Type t0_12 = t0 .meet(t12); - assertSame(t01_2,t0_12); + assertSame(t0+" & "+t1+" & "+t2, t01_2,t0_12); } // Test cyclic types and meets @Test public void testCyclic0() { - Type d0 = TypeStruct.SFLT2.dual(); + Type d0 = TypeStruct.SFLT1.dual(); Type d1 = d0.dual(); - assertSame(TypeStruct.SFLT2,d1); - } - - @Test - public void testGLB() { - Type[] ts = Type.gather(); - for( Type t0 : ts ) - if( !(t0 instanceof Field || t0 instanceof TypeStruct || t0 instanceof TypeTuple || t0 instanceof TypeConAry ) ) - Assert.assertTrue(t0.isa(t0.glb(false))); + assertSame(TypeStruct.SFLT1,d1); } @Test public void testList() { TypeStruct list = TypeStruct.open("List"); TypeMemPtr plist = TypeMemPtr.makeNullable(list); - list = list.add(Field.make("next", plist, 2, false, false ) ); - list = list.add(Field.make("x", TypeInteger.BOT, 3, false, false)); + list = list.add(Field.make("next", plist, 2, false) ); + list = list.add(Field.make("x", TypeInteger.BOT, 3, false)); // Make a cyclic type list = list.close(); // Fields are mutable @@ -230,6 +207,17 @@ public class TypeTest { TypeStruct flist = (TypeStruct)list.makeRO(); Assert.assertTrue(flist.isFinal()); Assert.assertNotSame( list, flist ); + } + @Test + public void testOpen() { + TypeStruct open = TypeStruct.make("List",false); + TypeMemPtr plist = TypeMemPtr.makeNullable(open); + TypeStruct list = open.add(Field.make("next", plist, 2, false) ); + list = list.add(Field.make("x", TypeInteger.BOT, 3, false)); + // Make a cyclic type + list = list.close(); + Type mt = open.meet(list); + Assert.assertEquals(list,mt); } } diff --git a/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java index 00d86c63..445acb61 100644 --- a/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java +++ b/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java @@ -26,7 +26,7 @@ public class FlagSpillTestSupport { @Override public void encoding(Encoding enc) { throw new AssertionError(); } } - public static void run() throws Exception { + public static void run() throws IOException { StringBuilder asm = new StringBuilder(".text\n"); for( boolean isArm : new boolean[]{false,true} ) for( boolean stack : new boolean[]{false,true} ) { @@ -59,10 +59,12 @@ public class FlagSpillTestSupport { assertTrue(splits>=2); assertEquals(stack,slots!=0); - code._regAlloc=alloc; - FunNode frame = new FunNode(null,null,"flags",(Node)null); + // Keep the allocated registers, supplying a frame for encoding stack offsets. + FunNode frame = new FunNode(null,null,"flags",null,(Node)null); + code._regAlloc=new RegAlloc(code); + for( Node n : block._outputs ) + if( alloc.regnum(n)>=0 ) code._regAlloc.newLRG(n,frame)._reg=alloc.regnum(n); Encoding save = new Encoding(code), restore = new Encoding(code); - save._fun=restore._fun=frame; Encoding enc = save; for( Node n : block._outputs ) { if( n==kill ) enc=restore; @@ -124,15 +126,9 @@ public class FlagSpillTestSupport { } """); String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "flags.exe" : "flags").toString(); - exec("gcc","-DCALL_CONV="+(TestC.OS.startsWith("Windows") ? "ms_abi" : "sysv_abi"), + TestC.exec("gcc","-DCALL_CONV="+(TestC.OS.startsWith("Windows") ? "ms_abi" : "sysv_abi"), "-o",exe,dir.resolve("flags.c").toString(),dir.resolve("flags.s").toString()); - exec(exe); - } - - private static void exec(String... command) throws Exception { - Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); - String output = new String(process.getInputStream().readAllBytes(),java.nio.charset.StandardCharsets.UTF_8); - assertEquals(String.join(" ",command)+"\n"+output,0,process.waitFor()); + TestC.exec(exe); } private static void bytes(Encoding dst,Encoding src) { diff --git a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java index b9b49c9f..b255bcf7 100644 --- a/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java +++ b/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java @@ -7,9 +7,9 @@ public class PrintRegTestSupport { var code = new CodeGen("return 0;").parse(); var alloc = new RegAlloc(code); var n = new ConstantNode(TypeInteger.ZERO); - var a = new LRG((short)1); - var b = new LRG((short)2); - var c = new LRG((short)3); + var a = new LRG((short)1,null); + var b = new LRG((short)2,null); + var c = new LRG((short)3,null); a._reg = -1; c._leader = b; b._leader = a; @@ -23,4 +23,54 @@ public class PrintRegTestSupport { assertSame(b,c._leader); assertSame(a,b._leader); } + public static void checkConstantPool() throws Exception { + var code = new CodeGen("return 0;").parse(); + code._phase = CodeGen.Phase.Encoding; + var enc = new Encoding(code); + var t = com.seaofnodes.simple.type.TypeStruct.make("PrintPool",false, + com.seaofnodes.simple.type.Field.make("x",TypeInteger.ZERO,2,true)); + var n = new ConstantNode(TypeInteger.ZERO); + enc.largeConstant(n,t,0,2); + int size = t.size(); // Encoding lays out data before printing the pool. + for( int i=0; i<size; i++ ) enc._cpool.write(0); + // Layout queries are forbidden during printing, even if they would + // happen to return a cached answer without changing the type. + boolean[] printing = {false}; + var scalar = new com.seaofnodes.simple.type.Type(com.seaofnodes.simple.type.Type.BOTTOM._type) { + @Override public int alignment() { + assertFalse("alignment queried by printer",printing[0]); + return 0; + } + @Override public int size() { + assertFalse("size queried by printer",printing[0]); + return 1; + } + @Override public String str() { return "LayoutProbe"; } + }; + var scalarNode = new ConstantNode(TypeInteger.TRUE); + enc._bigCons.put(scalarNode,new Encoding.Relo(scalarNode,scalar,(byte)0,(byte)2)); + enc._cpool.write(0); + printing[0] = true; + var offsets = com.seaofnodes.simple.type.TypeStruct.class.getDeclaredField("_offs"); + offsets.setAccessible(true); + offsets.set(t,null); // Printer must not lazily rebuild the cached layout. + var field = com.seaofnodes.simple.type.Type.class.getDeclaredField("VISIT"); + field.setAccessible(true); + var visit = (java.util.Map<Object,Object>)field.get(null); + var marker = new Object(); + visit.put(marker,t); + var method = com.seaofnodes.simple.print.ASMPrinter.class.getDeclaredMethod( + "printConstantPool",int.class,com.seaofnodes.simple.util.SB.class, + com.seaofnodes.simple.util.BAOS.class,java.util.HashMap.class,boolean.class,String.class); + method.setAccessible(true); + try { + var sb = new com.seaofnodes.simple.util.SB(); + method.invoke(null,0,sb,enc._cpool,enc._bigCons,true,"Constant Pool"); + assertTrue(sb.toString().contains("PrintPool")); + assertTrue(sb.toString().contains("LayoutProbe")); + assertNull(offsets.get(t)); + assertSame(t,visit.get(marker)); + assertEquals(1,visit.size()); + } finally { visit.clear(); } + } } diff --git a/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java index 7a21c3a7..4398827e 100644 --- a/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java +++ b/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java @@ -22,11 +22,6 @@ public class RegAllocTestSupport { @Override public Node copy() { return new Op(_def,_use,_kill,_clone,(Node)null); } @Override public void encoding(Encoding enc) { throw new AssertionError(); } } - private static SplitNode testSplit(CodeGen code, MachNode def) { - LRG lrg = new LRG((short)0); - lrg._machDef = def; - return code._mach.split("test",(byte)0,lrg); - } private static CodeGen graph() { CodeGen code = new CodeGen(""); code._mach = new x86_64_v2(code); @@ -34,12 +29,18 @@ public class RegAllocTestSupport { return code; } + private static SplitNode testSplit(CodeGen code, MachNode def) { + LRG lrg = new LRG((short)0,null); + lrg._machDef = def; + return code._mach.split(lrg,"test",(byte)0); + } + public static void uncoloredFixedNeighbor() throws Exception { CodeGen code = graph(); RegAlloc alloc = new RegAlloc(code); Op def = new Op(new RegMask(3L),null,null,false,code._start); - LRG lrg = alloc.newLRG(def); - LRG neighbor = new LRG((short)99); + LRG lrg = alloc.newLRG(def,null); + LRG neighbor = new LRG((short)99,null); neighbor._mask = A; lrg.addNeighbor(neighbor); // Uncolored, but its only possible color is A. var bias = IFG.class.getDeclaredMethod("biasColorNeighbors",RegAlloc.class,Node.class,RegMask.class); @@ -64,7 +65,7 @@ public class RegAllocTestSupport { public static void union() { CodeGen code = graph(); Op use = new Op(null,B,null,false,code._start); - LRG a = new LRG((short)1), b = new LRG((short)2); + LRG a = new LRG((short)1,null), b = new LRG((short)2,null); a._mask = new RegMask(3L); b._mask = new RegMask(6L); b.machUse(use,(short)2,true); @@ -136,7 +137,7 @@ public class RegAllocTestSupport { RegAlloc alloc = new RegAlloc(code); RegionNode block = new RegionNode(null,code._start,code._start); Op def = new Op(new RegMask(7L),null,null,false,block); - LRG lrg = alloc.newLRG(def); + LRG lrg = alloc.newLRG(def,null); lrg._machDef = def; lrg._mask = new RegMask(0L); Op ab = new Op(null,new RegMask(3L),null,false,block,def); @@ -168,10 +169,14 @@ public class RegAllocTestSupport { // Check the scheduled graph before encoding adds untyped branches or rewrites tail calls. public static class CheckedCodeGen extends CodeGen { public CheckedCodeGen(String src) { super(src); } - public CheckedCodeGen(String src, TypeInteger arg) { super(src,arg); } + public CheckedCodeGen(String modDir, String buildDir, + String srcName, String src, long seed, boolean reset, TypeInteger arg) { + super(modDir,buildDir,srcName,src,seed,reset,arg); + } @Override public CodeGen regAlloc() { super.regAlloc(); checkRegisters(this); + com.seaofnodes.simple.SpillStats.record25(this); return this; } } @@ -225,14 +230,14 @@ public class RegAllocTestSupport { CodeGen code = graph(); RegAlloc alloc = new RegAlloc(code); Op def = new Op(A,null,null,false,code._start); - alloc.newLRG(def)._reg=0; + alloc.newLRG(def,null)._reg=0; SplitNode hi = testSplit(code,def); hi.setDef(0,code._start); hi.setDef(1,def); - alloc.newLRG(hi)._reg=1; + alloc.newLRG(hi,null)._reg=1; new Op(null,null,killed ? A : null,false,code._start); SplitNode lo = testSplit(code,hi); lo.setDef(0,code._start); lo.setDef(1,hi); - alloc.newLRG(lo)._reg=0; + alloc.newLRG(lo,null)._reg=0; Op use = new Op(null,A,null,false,code._start,lo); var post = RegAlloc.class.getDeclaredMethod("postColor"); post.setAccessible(true); post.invoke(alloc); @@ -241,9 +246,9 @@ public class RegAllocTestSupport { CodeGen pre = graph(); RegAlloc uncolored = new RegAlloc(pre); Op def0 = new Op(A,null,null,false,pre._start); - uncolored.newLRG(def0)._mask=A; + uncolored.newLRG(def0,null)._mask=A; Op fixed = new Op(A,null,null,false,pre._start); - uncolored.newLRG(fixed)._mask=A; + uncolored.newLRG(fixed,null)._mask=A; SplitNode copy = testSplit(pre,def0); copy.setDef(0,pre._start); copy.setDef(1,def0); assertFalse("A fixed definition clobbers even before coloring",uncolored.sameBlockNoClobber(copy)); @@ -254,13 +259,14 @@ public class RegAllocTestSupport { CodeGen code = graph(); RegAlloc alloc = new RegAlloc(code); Op init = new Op(A,null,null,false,code._start); - LoopNode loop = new LoopNode(null,code._start); + LoopNode loop = new LoopNode(null,null,code._start); PhiNode phi = new PhiNode("loop",loop,init,null); + phi._type = TypeInteger.BOT; Op back = new Op(A,A,null,false,loop,phi); phi.setDef(2,back); Op use = new Op(null,A,null,false,loop,phi); loop.setDef(2,loop); - LRG lrg = alloc.newLRG(phi); + LRG lrg = alloc.newLRG(phi,null); lrg._mask = A; alloc.union(lrg,init); alloc.union(lrg,back); lrg.selfConflict(back); @@ -283,10 +289,10 @@ public class RegAllocTestSupport { CodeGen code = graph(); RegAlloc alloc = new RegAlloc(code); Op def = new Op(A,null,null,false,code._start); - alloc.newLRG(def)._reg=0; + alloc.newLRG(def,null)._reg=0; SplitNode copy = testSplit(code,def); copy.setDef(0,code._start); copy.setDef(1,def); - alloc.newLRG(copy)._reg=1; + alloc.newLRG(copy,null)._reg=1; if( kind==3 ) new Op(null,null,A,false,code._start); int twoAddress = kind==2 ? 1 : 0; RegMask mask = kind==1 ? B : kind==5 ? null : new RegMask(3L); @@ -310,12 +316,12 @@ public class RegAllocTestSupport { SplitNode copy = testSplit(code,def); copy.setDef(0,code._start); copy.setDef(1,def); Op use = new Op(null,new RegMask(3L),null,false,code._start,copy); - LRG from = alloc.newLRG(def), to = alloc.newLRG(copy); + LRG from = alloc.newLRG(def,null), to = alloc.newLRG(copy,null); from._mask = kind==1 ? A : new RegMask(3L); to._mask = kind==1 ? B : new RegMask(3L); from.machDef(def,false); from.machUse(copy,(short)1,false); to.machDef(copy,false); to.machUse(use,(short)1,false); - LRG left = new LRG((short)100), right = new LRG((short)101); + LRG left = new LRG((short)100,null), right = new LRG((short)101,null); if( kind==2 ) { from.addNeighbor(to); to.addNeighbor(from); } if( kind>=3 ) { from.addNeighbor(left); left.addNeighbor(from); diff --git a/src/test/java/com/seaofnodes/simple/codegen/X86EncodingTest.java b/src/test/java/com/seaofnodes/simple/codegen/X86EncodingTest.java deleted file mode 100644 index 0914d916..00000000 --- a/src/test/java/com/seaofnodes/simple/codegen/X86EncodingTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.seaofnodes.simple.codegen; - -import com.seaofnodes.simple.node.Node; -import com.seaofnodes.simple.node.cpus.x86_64_v2.CmpMemX86; -import com.seaofnodes.simple.node.cpus.x86_64_v2.NotX86; -import com.seaofnodes.simple.node.cpus.x86_64_v2.MulIX86; -import org.junit.Test; -import static org.junit.Assert.*; - -public class X86EncodingTest { - - @Test - public void testLogicalNotRegisters() { - CodeGen code = new CodeGen("return !arg;") - .driver(CodeGen.Phase.Select, "x86_64_v2", "SystemV"); - var not = code._stop.walk(n -> n instanceof NotX86 ? (NotX86)n : null); - assertNotNull("Must exercise logical not", not); - int[][] registers = {{0,0}, {1,1}, {7,7}, {9,9}, {1,9}, {9,1}, {6,0}, {15,8}}; - // TEST src,src; SETZ dstb; MOVZX dst32,dstb. Include overlapping - // registers, low-byte registers requiring REX, and extended registers. - String[] expected = { - "4885c00f94c00fb6c0", "4885c90f94c10fb6c9", - "4885ff400f94c7400fb6ff", "4d85c9410f94c1450fb6c9", - "4d85c90f94c10fb6c9", "4885c9410f94c1450fb6c9", - "4885c0400f94c6400fb6f6", "4d85c0410f94c7450fb6ff" - }; - for (int i = 0; i < registers.length; i++) { - int dst = registers[i][0], src = registers[i][1]; - var enc = new FixedRegisterEncoding(code, not, dst, not.in(1), src); - not.encoding(enc); - assertArrayEquals("Logical not dst=" + dst + ", src=" + src, - java.util.HexFormat.of().parseHex(expected[i]), enc._bits.toByteArray()); - } - } - - @Test - public void testLogicalNotExecution() throws java.io.IOException { - var dir = java.nio.file.Path.of("build/objs"); - java.nio.file.Files.createDirectories(dir); - java.nio.file.Files.writeString(dir.resolve("x86LogicalNot.smp"), - "val logicalNot = { int x -> return !x; };"); - java.nio.file.Files.writeString(dir.resolve("x86LogicalNot.c"), """ - #include <limits.h> - extern long long logicalNot(long long); - int main(void) { - long long inputs[] = {0, 1, -1, LLONG_MIN, LLONG_MAX, 1LL << 32}; - for (unsigned i = 0; i < sizeof(inputs)/sizeof(inputs[0]); i++) - if (logicalNot(inputs[i]) != (inputs[i] == 0)) return 1; - return 0; - } - """); - com.seaofnodes.simple.TestC.run("build/objs", "x86LogicalNot", "", -1); - } - - @Test - public void testMultiplyImmediateRegisters() { - // Destination, source, expected REX and ModRM. Cross the register-8 - // boundary in both directions, with low/low and high/high controls. - int[][] cases = {{1, 9, 0x49, 0xC9}, {9, 1, 0x4C, 0xC9}, - {1, 2, 0x48, 0xCA}, {9, 10, 0x4D, 0xCA}}; - for (int immediate : new int[]{11, -11, 123456789, -123456789}) { - CodeGen code = new CodeGen("return arg * " + immediate + ";") - .driver(CodeGen.Phase.Select, "x86_64_v2", "SystemV"); - var mul = code._stop.walk(n -> n instanceof MulIX86 ? n : null); - assertNotNull("Must exercise immediate multiply", mul); - for (int[] regs : cases) { - var enc = new FixedRegisterEncoding( - code, mul, regs[0], mul.in(1), regs[1]); - ((MulIX86)mul).encoding(enc); - boolean small = immediate == 11 || immediate == -11; - assertEquals("REX for dst=" + regs[0] + ", src=" + regs[1], regs[2], enc.read1(0)); - assertEquals(small ? 0x6B : 0x69, enc.read1(1)); - assertEquals(regs[3], enc.read1(2)); - assertEquals(small ? 4 : 7, enc._bits.size()); - assertEquals(immediate, small ? (byte)enc.read1(3) : enc.read4(3)); - } - } - } - @Test - public void testMemoryCompare64Immediate32() { - for (int immediate : new int[]{123456789, -123456789}) { - String src = "int[] !a = new int[2]; a[0] = arg; return a[arg & 1] == " + immediate + ";"; - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.Encoding, "x86_64_v2", "SystemV"); - var cmp = code._stop.walk(n -> n instanceof CmpMemX86 ? n : null); - assertNotNull("Must exercise a memory comparison", cmp); - var enc = code._encoding; - int start = enc._opStart[cmp._nid]; - - // REX.W, 81 /7, SIB with scale 8, disp8, and a sign-extended imm32. - assertEquals(0x48, enc.read1(start) & 0xF8); - assertEquals(0x81, enc.read1(start + 1)); - assertEquals(0x7C, enc.read1(start + 2)); - assertEquals(0xC0, enc.read1(start + 3) & 0xC0); - assertEquals(8, enc.read1(start + 4)); - assertEquals(immediate, enc.read4(start + 5)); - assertEquals("CMP r/m64 uses four immediate bytes", 9, enc._opLen[cmp._nid]); - - // SETE must immediately follow the CPU's nine-byte CMP instruction. - assertEquals(0x0F, enc.read1(start + 9)); - assertEquals(0x94, enc.read1(start + 10)); - } - } - - /** Exercise instruction encoders with explicit registers, independent of allocator choices. */ - private static class FixedRegisterEncoding extends Encoding { - private final Node _dst, _src; - private final short _dstReg, _srcReg; - - FixedRegisterEncoding(CodeGen code, Node dst, int dstReg, Node src, int srcReg) { - super(code); - _dst = dst; - _src = src; - _dstReg = (short)dstReg; - _srcReg = (short)srcReg; - } - - @Override public short reg(Node n) { - if (n == _dst) return _dstReg; - if (n == _src) return _srcReg; - throw new AssertionError("Unexpected register lookup"); - } - } -} diff --git a/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java b/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java deleted file mode 100644 index 3f4613ff..00000000 --- a/src/test/java/com/seaofnodes/simple/evaluator/Evaluator.java +++ /dev/null @@ -1,381 +0,0 @@ -package com.seaofnodes.simple.evaluator; - -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.*; -import com.seaofnodes.simple.util.Utils; -import java.util.*; - -public class Evaluator { - - private static final Object MEMORY = new Object(); - - public record Obj(TypeStruct struct, Object[] fields) { - private void init(IdentityHashMap<Obj, Integer> objs) { - var oid = objs.get(this); - if (oid != null) { - if (oid != 0) objs.put(this, 0); - return; - } - objs.put(this, -1); - for (var obj : fields) { - if (obj instanceof Obj o) o.init(objs); - } - } - private static int p(StringBuilder sb, IdentityHashMap<Obj, Integer> objs, int id, String indentation, String step, String sep, Object obj) { - if (obj instanceof Obj o) return o.p(sb, objs, id, indentation, step, sep); - sb.append(obj); - return id; - } - private int p(StringBuilder sb, IdentityHashMap<Obj, Integer> objs, int id, String indentation, String step, String sep) { - var cid = objs.get(this); - assert cid != null; - if (cid > 0) { - sb.append("obj@").append(cid); - return id; - } - if (struct._name.equals("[u8]")) { - sb.append('"'); - for (int i=struct._fields.length-1; i<fields.length; i++) { - var v = fields[i]; - if (v == null) v = 0; - assert v instanceof Number; - var n = ((Number)v).byteValue() & 0xFF; - if (n >= 0x20 && n < 0x80) { - if (n == 0x22 || n == 0x5C) sb.append("\\"); - sb.append((char)n); - } else { - sb.append("\\x").append("0123456789abcdef".charAt(n >> 4)).append("0123456789abcdef".charAt(n & 0xF)); - } - } - sb.append('"'); - return id; - } - sb.append("Obj<").append(struct._name).append(">"); - if (cid == 0) { - cid = id++; - objs.put(this, cid); - sb.append("@").append(cid); - } - sb.append("{"); - if (struct._fields.length == 0) { - assert !struct.isAry(); - sb.append("}"); - return id; - } - String nextIndent = indentation + step; - int e = struct._fields.length - 1; - for(int i=0; i<e; i++) { - sb.append(nextIndent).append(struct._fields[i]._fname).append("="); - id = p(sb, objs, id, nextIndent, step, sep, fields[i]); - sb.append(sep); - } - sb.append(nextIndent).append(struct._fields[e]._fname).append("="); - if (struct.isAry()) { - sb.append("["); - if (fields.length > e) { - String innerIndent = nextIndent + step; - sb.append(innerIndent); - id = p(sb, objs, id, nextIndent, step, sep, fields[e]); - for (int i = e+1; i < fields.length; i++) { - sb.append(sep).append(innerIndent); - id = p(sb, objs, id, nextIndent, step, sep, fields[i]); - } - sb.append(nextIndent); - } - sb.append("]"); - } else { - id = p(sb, objs, id, nextIndent, step, sep, fields[e]); - } - sb.append(indentation).append("}"); - return id; - } - private StringBuilder p(StringBuilder sb, String indentation, String step, String sep) { - var objs = new IdentityHashMap<Obj, Integer>(); - init(objs); - p(sb, objs, 1, indentation, step, sep); - return sb; - } - @Override - public String toString() { - if (struct._name.equals("[u8]")) { - var sb = new StringBuilder(); - for (int i=struct._fields.length-1; i<fields.length; i++) { - var v = fields[i]; - if (v == null) v = 0; - assert v instanceof Number; - var n = ((Number)v).byteValue() & 0xFF; - sb.append((char)n); - } - return sb.toString(); - } - return p(new StringBuilder(), "", "", ",").toString(); - } - public String pretty() { - return p(new StringBuilder(), "\n", " ", "").toString(); - } - } - - private static int getFieldIndex(TypeStruct struct, MemOpNode memop, long off) { - int idx = struct.find(memop._name); - if( idx >= 0 ) return idx; - throw new AssertionError("Field "+memop._name+" not found in struct " + struct._name); - } - - public enum Status { - TIMEOUT, - FALLTHROUGH - } - - /** - * Find the start node from some node in the graph or null if there is no start node - */ - private static StartNode findStart(BitSet visit, Node node) { - if (node == null) return null; - if( visit.get(node._nid) ) return null; - visit.set(node._nid); - StartNode start = node instanceof StartNode ? (StartNode) node : null; - for( Node def : node._inputs ) { - var res = findStart(visit,def); - if( res != null ) start = res; - } - for( Node use : node._outputs ) { - var res = findStart(visit,use); - if( res != null ) start = res; - } - return start; - } - - private final Object[] values; - private final ArrayList<Object> phiCache = new ArrayList<>(); - private final StartNode start; - private final Scheduler.Block startBlock; - - public Evaluator(Node graph) { - var visited = new BitSet(); - this.start = findStart(visited, graph); - if (this.start == null) { - this.values = null; - this.startBlock = null; - return; - } - this.startBlock = Scheduler.schedule(this.start); - this.values = new Object[visited.length()]; - } - - private long div(DivNode div) { - long in2 = vall(div.in(2)); - return in2 == 0 ? 0 : vall(div.in(1)) / in2; - } - private double div(DivFNode div) { - double in2 = vald(div.in(2)); - return in2 == 0 ? 0 : vald(div.in(1)) / in2; - } - - private static long offToIdx(long off, TypeStruct t) { - off -= t.aryBase(); - int scale = t.aryScale(); - long mask = (1L << scale)-1; - assert (off & mask) == 0; - return off>>scale; - } - - private Object alloc(NewNode alloc) { - TypeStruct type = alloc._ptr._obj; - Object[] body=null; - int num; - if( type.isAry() ) { - long sz = (Long)val(alloc.size()); - long n = offToIdx(sz, type); - if( n < 0 ) - throw new NegativeArraySizeException(""+n); - body = new Object[(int)n+1]; // Array body - var elem = type._fields[1]._t; - if (elem instanceof TypeInteger) { - for (int i=0; i<n; i++) body[i+1] = 0L; - } else if (elem instanceof TypeFloat) { - for (int i = 0; i < n; i++) body[i+1] = 0D; - } else { - assert elem instanceof TypeMemPtr; - } - // Length value - body[0] = n; - } else { - body = new Object[num = type._fields.length]; - for( int i=0; i<num; i++ ) - body[i] = switch( alloc._ptr._obj._fields[i]._t ) { - case TypeInteger ti -> 0L; - case TypeFloat tf -> 0D; - default -> null; - }; - } - return new Object[]{new Obj(type,body),null}; - } - - private Object load(LoadNode load) { - var from = (Obj)val(load.ptr()); - var off = vall(load.off()); - var idx = getFieldIndex(from.struct, load, (Long)off); - if( idx==from.struct._fields.length-1 && from.struct.isAry() ) { - long len = from.fields.length - from.struct._fields.length + 1; - long i = offToIdx(off, from.struct); - if( i < 0 || i >= len ) - throw new ArrayIndexOutOfBoundsException("Array index out of bounds " + i + " < " + len); - return from.fields[(int)i+from.struct._fields.length-1]; - - } else - return from.fields[idx]; - } - - private Object store(StoreNode store) { - var to = (Obj)val(store.ptr()); - var off = vall(store.off()); - var val = val(store.val()); - var idx = getFieldIndex(to.struct, store, (Long)off); - if( idx==to.struct._fields.length-1 && to.struct.isAry() ) { - long len = to.fields.length - to.struct._fields.length + 1; - long i = offToIdx(off, to.struct); - if( i < 0 || i >= len ) - throw new RuntimeException("Array index out of bounds " + off + " <= " + len); - to.fields[(int)i+to.struct._fields.length-1] = val; - } else - to.fields[idx] = val; - return null; - } - - private static boolean isTrue(Object obj) { - if (obj == null) return false; - if (obj instanceof Number n) return n.longValue() != 0; - return true; - } - - private Object val(Node node) { - return values[node._nid]; - } - - private long vall(Node node) { - var v = val(node); - if (v instanceof Number n) return n.longValue(); - throw new AssertionError("Not a long " + v); - } - private double vald(Node node) { - var v = val(node); - if (v instanceof Number n) return n.doubleValue(); - if( v == null ) return 0; - throw new AssertionError("Not a double " + v); - } - - private Object cons(ConstantNode cons) { - var type = cons.compute(); - if (type instanceof TypeInteger i) return i.isConstant() ? i.value() : Integer.MAX_VALUE; - if (type instanceof TypeFloat i) return i.value(); - return null; - } - - private Object exec(Node node) { - return switch (node) { - case ConstantNode cons -> cons(cons); - case AddNode add -> vall(add.in(1)) + vall(add.in(2)); - case AddFNode add -> vald(add.in(1)) + vald(add.in(2)); - case BoolNode.EQF eq -> vald(eq.in(1)) == vald(eq.in(2)) ? 1L : 0L; - case BoolNode.LEF le -> vald(le.in(1)) <= vald(le.in(2)) ? 1L : 0L; - case BoolNode.LTF lt -> vald(lt.in(1)) < vald(lt.in(2)) ? 1L : 0L; - case BoolNode.EQ eq -> Objects.equals(val(eq.in(1)), val(eq.in(2))) ? 1L : 0L; - case BoolNode.LE le -> vall(le.in(1)) <= vall(le.in(2)) ? 1L : 0L; - case BoolNode.LT lt -> vall(lt.in(1)) < vall(lt.in(2)) ? 1L : 0L; - case DivNode div -> div(div); - case DivFNode div -> div(div); - case MinusNode minus -> -vall(minus.in(1)); - case MinusFNode minus -> -vald(minus.in(1)); - case MulNode mul -> vall(mul.in(1)) * vall(mul.in(2)); - case MulFNode mul -> vald(mul.in(1)) * vald(mul.in(2)); - case NotNode not -> isTrue(val(not.in(1))) ? 0L : 1L; - case SubNode sub -> vall(sub.in(1)) - vall(sub.in(2)); - case SubFNode sub -> vald(sub.in(1)) - vald(sub.in(2)); - case ShlNode shl -> vall(shl.in(1)) << vall(shl.in(2)); - case ShrNode shr -> vall(shr.in(1)) >>> vall(shr.in(2)); - case SarNode sar -> vall(sar.in(1)) >> vall(sar.in(2)); - case AndNode and -> vall(and.in(1)) & vall(and.in(2)); - case OrNode or -> vall(or .in(1)) | vall(or .in(2)); - case XorNode xor -> vall(xor.in(1)) ^ vall(xor.in(2)); - case CastNode cast -> val(cast.in(1)); - case RoundF32Node rnd -> (double)(float)vald(rnd.in(1)); - case ToIntegerNode cvt -> (long)vald(cvt.in(1)); - case ToFloatNode cast -> (double)vall(cast.in(1)); - case LoadNode load -> load(load); - case StoreNode store -> store(store); - case NewNode alloc -> alloc(alloc); - case CProjNode cproj -> ((Object[])val(cproj.ctrl()))[cproj._idx]; - case ProjNode proj -> ((Object[])val( proj.in(0) ))[ proj._idx]; - case EscapeNode esc -> null; - case MemMergeNode mem -> null; - case ReadOnlyNode ro -> val(ro.in(1)); - case CallNode call -> Utils.TODO(); // should not reach here, go the IfNode route - default -> throw new AssertionError("Unexpected node " + node); - }; - } - - /** - * Run the graph until either a return is found or the number of loop iterations are done. - */ - public Object evaluate(long parameter, int loops) { - if (start == null) return Status.TIMEOUT; - var s = new Object[start.compute()._types.length]; - s[1] = MEMORY; - s[2] = parameter; - values[start._nid] = s; - int i=0; - Scheduler.Block block = this.startBlock; - for(;;) { - for(; i<block.nodes().length;i++) values[block.nodes()[i]._nid] = exec(block.nodes()[i]); - i=0; - switch (block.exit()) { - case null: - return Status.FALLTHROUGH; - case ReturnNode ret : return val(ret .expr()); - case IfNode ifn: - block = block.next()[isTrue(val(ifn.in(1))) ? 0 : 1]; - if (block == null) return Status.FALLTHROUGH; - break; - case RegionNode region: - if (loops-- <= 0) return Status.TIMEOUT; - int exit = block.exitId(); - assert exit > 0 && region.nIns() > exit; - block = block.next()[0]; - assert block != null; - for (; i < block.nodes().length; i++) { - if (!(block.nodes()[i] instanceof PhiNode phi)) break; - var val = region instanceof FunNode fun && "main".equals(fun._name) && ((ParmNode)phi)._idx==2 && exit==1 - ? parameter - : val(phi.in(exit)); - phiCache.add(val); - } - for (i=0; i<phiCache.size(); i++) { - values[block.nodes()[i]._nid] = phiCache.get(i); - } - phiCache.clear(); - break; - default: - throw Utils.TODO("Unexpected control node " + block.exit()); - } - } - } - - public static Object evaluate(StopNode graph) { - return evaluate(graph, 0); - } - - public static Object evaluate(StopNode graph, long parameter) { - return evaluate(graph, parameter, 1000); - } - - public static Object evaluate(StopNode graph, long parameter, int loops) { - var res = evaluateWithResult(graph, parameter, loops); - if (res == Status.TIMEOUT) throw new RuntimeException("Timeout"); - return res; - } - - public static Object evaluateWithResult(StopNode graph, long parameter, int loops) { - return new Evaluator(graph).evaluate(parameter, loops); - } - -} diff --git a/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java b/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java deleted file mode 100644 index c837bd6f..00000000 --- a/src/test/java/com/seaofnodes/simple/evaluator/Scheduler.java +++ /dev/null @@ -1,605 +0,0 @@ -package com.seaofnodes.simple.evaluator; - -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.util.Utils; -import java.util.*; - -/** - * A late scheduler for test cases. - * <br> - * <br> - * This scheduler is used for tests and is not the final scheduler. - * <br> - * <br> - * The scheduler schedules not as late as possible. Since it is just for test cases this is fine. - * A better scheduler will try to place nodes outside of loops. Due to the schedule late strategy - * this scheduler might place initialization code in loops. - * <br> - * We schedule late here since memory edges have some implicit information. There can only be one - * memory edge (for a field) at a time. However, before <code>if</code> statements this might split. - * When scheduling early a store might be moved out of the loop but then there are two edges alive. - * The one through the store node and the adjacent one. This is an invalid schedule and instead of - * handling this case a late scheduler does not have this problem as memory edges are combined - * through phi nodes. - * <br> - * <br> - * A bit about this scheduler. It first discovers all alive nodes and allocates side data for them. - * Then the control flow graph is build. Then, all phis are placed and finally all the remaining nodes - * in an order where a node is placed when all alive uses are placed. Finally, the output is produced. - */ -public class Scheduler { - - /** - * A basic block with a schedule of the containing nodes. - * - * @param nodes The nodes in the final schedule of this block - * @param exit The exit node of this block. - * @param exitId In case the next block is a region this gives the entry id into the region. - * @param next The next blocks. - */ - public record Block(Node[] nodes, Node exit, int exitId, Block[] next) {} - - /** - * Definition of a block in the building process. - */ - private static class BasicBlock { - - /** - * Dominator of this block - */ - final BasicBlock dom; - /** - * Id of this block where <code>a.depth < b.depth</code> means that a is before b or - * both are in different branches. - */ - final int depth; - /** - * Previous blocks - */ - final BasicBlock[] prev; - /** - * Entry to this block - */ - final Node entry; - /** - * Schedules node in reverse order. - */ - final ArrayList<Node> reverseSchedule = new ArrayList<>(); - - /** - * Initialize the block. - * @param entry The node that started this block - * @param depth The depth of this node. All previous nodes need to have a lower value. - * @param prev The previous blocks. - */ - BasicBlock(Node entry, int depth, BasicBlock... prev) { - BasicBlock dom = null; - int max = -1; - for(var bb: prev) { - max = Math.max(max, bb.depth); - dom = dom==null?bb:dom(dom, bb); - } - assert depth > max; - this.dom = dom; - this.depth = depth; - this.entry = entry; - this.prev = prev; - } - BasicBlock(Node entry, BasicBlock prev) { - this(entry,prev.depth+1,prev); - } - - } - - /** - * Ancillary data for nodes used during the scheduling process. - */ - private static class NodeData { - - /** - * The node this ancillary data is used for. - */ - final Node node; - - /** - * Number of alive users not yet scheduled. - */ - int users = 1; - - /** - * Block into which the node should be placed so far. - * This will be updated with every placed use. - */ - BasicBlock block; - - /** - * Initialize ancillary data for a node - * @param node The node this ancillary data is associated with. - */ - NodeData(Node node) { - this.node = node; - } - - } - - /** - * Ancillary data for nodes. - */ - private final IdentityHashMap<Node, NodeData> data = new IdentityHashMap<>(); - /** - * List of nodes which can be placed since all users are placed. - */ - private final Stack<NodeData> scheduleQueue = new Stack<>(); - - /** - * Get the ancillary data for an alive node. - * @param node The node for which the ancillary data is requested. - * @return The ancillary data for the node. - */ - NodeData d(Node node) { - return od(node).orElseThrow(AssertionError::new); - } - - /** - * Get the ancillary data for a potentially dead node. - * @param node The node for which the ancillary data is requested. - * @return The ancillary data for the node an alive node or - * <code>Optional.empty()</code> for <code>null</code> or dead nodes. - */ - Optional<NodeData> od(Node node) { - return Optional.ofNullable(data.get(node)); - } - - /** - * Checks a node for validity - * @param data The node to check. - * @return true if all placed inputs are before this node. - */ - private boolean isValid(NodeData data) { - return data.node._inputs.stream() - .map(i->i==null?null:d(i)) - .map(d->d==null||d.users>0?null:d.block) - .allMatch(d->d==null||dom(d, data.block)==d); - } - - /** - * Checks that node is not XCtrl. - * @param node The node to check. - * @return true if the node is not XCtrl. - */ - private static boolean isNotXCtrl(Node node) { - return !(node instanceof ConstantNode c && c.compute() == Type.XCONTROL) && !(node instanceof XCtrlNode); - } - - /** - * Return the dominator of <code>a</code> and <code>b</code> - * @param a Block a - * @param b Block b - * @return The dominator of <code>a</code> and <code>b</code> - */ - private static BasicBlock dom(BasicBlock a, BasicBlock b) { - while (a!=b) { - if (a.depth >= b.depth) a=a.dom; - if (b.depth > a.depth) b=b.dom; - } - return a; - } - - /** - * Return if a is before b. - * @param a Block a - * @param b Block b - * @return true is a is before b. - */ - private static boolean isBefore(BasicBlock a, BasicBlock b) { - while (b.depth > a.depth) { - while (b.dom.depth > a.depth) b = b.dom; - for (int i=0; i<b.prev.length-1; i++) { - if (isBefore(a, b.prev[i])) return true; - } - b = b.prev[b.prev.length-1]; - } - return a == b; - } - - /** - * Is node placed during the control flow graph build. - * @param data Node to check. - * @return true if node is placed during the control flow graph build. - */ - private static boolean isPinnedNode(NodeData data) { - return data.node instanceof CFGNode || data.node instanceof PhiNode; - } - - /** - * Refine placement of a node. - * @param data The node to refine. - * @param block The block before the node should be scheduled. - */ - private void refinePlacement(NodeData data, BasicBlock block) { - assert !isPinnedNode(data); - data.block = data.block == null ? block : dom(data.block, block); - assert isValid(data); - } - - /** - * Refine placement if before is happening before data - * @param data The node for which the placement should be refined. - * @param before The node before which the data node should be placed. - */ - private void optionalRefinePlacement(NodeData data, Node before) { - od(before).ifPresent(d->{ - // before might be in a different branch. So check this case with isBefore - // and only refine the placement if it is not in a different branch. - if (isBefore(d.block, data.block)) refinePlacement(data, d.block); - }); - } - - /** - * Schedule all nodes not yet scheduled. - */ - private void doSchedule() { - while (!scheduleQueue.isEmpty()) { - var data = scheduleQueue.pop(); - assert !isPinnedNode(data); - - assert data.block != null; - assert data.users == 0; - - if (data.node instanceof MemOpNode l && l._isLoad) { - assert isValid(data); - // Handle anti-deps of load nodes. - // At this point all anti-dep nodes are scheduled, - // but they did not refine the placement - // so do that now. - var mem = data.node.in(1); - for(var out : mem._outputs) { - if (out instanceof PhiNode p) { - var r = p.in(0); - for (int i = 1; i < p.nIns(); i++) { - if (p.in(i) == mem) optionalRefinePlacement(data, r.in(i)); - } - } else if (!(out instanceof MemOpNode ld && ld._isLoad)) { - optionalRefinePlacement(data, out); - } - } - } - - assert isValid(data); - data.block.reverseSchedule.add(data.node); - - for(var in:data.node._inputs) { - if (in!=null) update(d(in), data.block); - } - if (data.node instanceof MemOpNode s && !s._isLoad) { - // Store nodes have anti-deps to load nodes. - // So decrease the uses of these loads when the store is placed. - for (var out: ((Node)s).in(1)._outputs) { - if (out instanceof MemOpNode ld && ld._isLoad) od(out).ifPresent(this::decUsers); - } - } - } - - // Now all nodes should be placed and have a block assigned - assert this.data.values().stream().noneMatch(d->d.block==null); - } - - /** - * Decrement the not-yet-placed users of a block. - * @param data The node for which the users should be decremented. - */ - private void decUsers(NodeData data) { - assert data.users > 0; - if (--data.users == 0) { - assert data.block != null; - assert isValid(data); - - // When all users are gone this node can be scheduled. - scheduleQueue.push(data); - } - } - - /** - * Update the placement of a node - * @param data The node to update - * @param block The block before which the node should happen. - */ - private void update(NodeData data, BasicBlock block) { - assert block != null; - if (data.users == 0) { - assert isPinnedNode(data); - return; - } - refinePlacement(data, block); - decUsers(data); - } - - /** - * Checks if a CFG node is ready to be placed. This is used for regions joining branches. - * @param node The CFG node to check - * @return true if all parents of a CFG node are placed. - */ - private boolean isCFGNodeReady(Node node) { - if (node instanceof LoopNode l) { - assert d(l.in(1)).block != null; - } else if (node instanceof RegionNode r) { - for(int i=1; i<r.nIns(); i++) { - if (od(r.in(i)).map(d->d.block==null).orElse(false)) return false; - } - } else if (node instanceof XCtrlNode) { - return false; - } else { - assert d(node.in(0)).block != null; - } - return true; - } - - /** - * Schedule all phi nodes. - * @param phiQueue List of all the phi nodes to schedule. - */ - private void schedulePhis(Stack<NodeData> phiQueue) { - while(!phiQueue.empty()) { - var data = phiQueue.pop(); - var phi = (PhiNode) data.node; - var r = phi.in(0); - data.block = d(r).block; - for(int i=1; i<phi.nIns(); i++) { - var in = phi.in(i); - od(r.in(i)).ifPresent(d->update(d(in), d.block)); - } - } - } - - /** - * Build the control flow graph. - * @param start The start node. - */ - private void doBuildCTF(StartNode start) { - var queue = new Stack<NodeData>(); - var phiQueue = new Stack<NodeData>(); - queue.push(d(start)); - while (!queue.isEmpty()) { - var data = queue.pop(); - var node = data.node; - assert node instanceof CFGNode; - BasicBlock block = switch (node) { - case StartNode s -> new BasicBlock(s, 0); - case LoopNode l -> new BasicBlock(l, d(l.in(1)).block); - case RegionNode r -> { - var prev = new ArrayList<BasicBlock>(); - int depth = 0; - for(int i=1; i<r.nIns(); i++) { - if( !od(r.in(i)).isPresent() ) continue; - BasicBlock bb = d(r.in(i)).block; - prev.add(bb); - depth = Math.max(depth,bb.depth); - } - yield new BasicBlock(r, depth+1, prev.toArray(BasicBlock[]::new)); - } - case IfNode i -> d(i.in(0)).block; - case ReturnNode ret -> d(ret .in(0)).block; - case CallNode call -> throw Utils.TODO(); - default -> new BasicBlock(node, d(node.in(0)).block); - }; - data.block = block; - if (node instanceof RegionNode r) { - // Regions might have phis which need to be scheduled. - // Put them on a list for later scheduling. - for (var out:r._outputs) { - if (out instanceof PhiNode p) { - var d = this.data.get(p); - if (d!=null) { - d.users=0; - phiQueue.push(d); - } - } - } - } - if( node instanceof FunNode fun ) { - // Cannot find CFG user from fun via a naive search, because always find - // Return but Return is not always the one CFG user. - CFGNode n = fun.uctrl(); - assert isCFGNodeReady(n) && d(n).block==null; - queue.push(d(n)); - } else if( !(node instanceof ReturnNode) ) { - for (Node n : node._outputs) - if(n instanceof CFGNode && isCFGNodeReady(n) && d(n).block == null) - queue.push(d(n)); - } - var b = block; - for(var in:data.node._inputs) od(in).ifPresent(d->update(d, b)); - } - - // Schedule all phi nodes. - schedulePhis(phiQueue); - } - - /** - * Mark a node alive and create ancillary data for it. - * @param queue List into which this node should be placed if it needs to be visited. - * @param node Node which should be marked alive. - * @param cfg If this node is a CFG node. - */ - private void markAlive(Stack<NodeData> queue, Node node, boolean cfg) { - var nd = od(node).orElse(null); - if (nd != null) { - // Node was already visited, just increase the users. - if (nd.users>0) nd.users++; - return; - } - assert node instanceof CFGNode == cfg; - assert isNotXCtrl(node); - nd = new NodeData(node); - if (cfg) nd.users=0; - data.put(node, nd); - queue.push(nd); - } - - /** - * Mark all alive nodes. - * First all CFG nodes are marked, then all other nodes. - * @param node THe start node. - */ - private void doMarkAlive(Node node) { - var cfgQueue = new Stack<NodeData>(); - var dataQueue = new Stack<NodeData>(); - var mem = new Stack<NodeData>(); - markAlive(cfgQueue, node, true); - // Mark all CFG nodes. - while (!cfgQueue.isEmpty()) { - var data = cfgQueue.pop(); - node = data.node; - assert node instanceof CFGNode; - if (!(node instanceof ReturnNode) ) { - for (var out : node._outputs) if (out!=null && out instanceof CFGNode && isNotXCtrl(out)) markAlive(cfgQueue, out, true); - } - for (var in : node._inputs) if(in!=null && !(in instanceof CFGNode) && isNotXCtrl(in)) markAlive(dataQueue, in, false); - } - // Mark all other nodes. - while (!dataQueue.isEmpty()) { - var data = dataQueue.pop(); - node = data.node; - assert !(node instanceof CFGNode); - if (node instanceof PhiNode phi) { - var r = phi.in(0); - for (int i=1; i<phi.nIns(); i++) { - if (od(r.in(i)).isPresent()) markAlive(dataQueue, phi.in(i), false); - } - } else { - for (var in : node._inputs) if (in != null && !(in instanceof CFGNode)) markAlive(dataQueue, in, false); - } - if (node instanceof MemOpNode s && !s._isLoad) mem.push(data); - } - // Handle store nodes and increase load with an anti-dep to the store. - while (!mem.isEmpty()) { - var data = mem.pop(); - node = data.node; - for(var out:node.in(1)._outputs) { - if (out instanceof MemOpNode ld && ld._isLoad) od(out).ifPresent(d->d.users++); - } - } - } - - /** - * Helper function to append the nodes of a block to an array of nodes - * @param arr The array the nodes of a block should be appended to. - * @param node A potentially separator node - * @param block The block from which the nodes should be appended. - * @return The combined new array. - */ - private static Node[] appendNodes(Node[] arr, Node node, BasicBlock block) { - var idx = arr.length; - arr = Arrays.copyOf(arr, arr.length+block.reverseSchedule.size()+(node==null?0:1)); - if (node != null) arr[idx++] = node; - for(int i=block.reverseSchedule.size()-1;i>=0;i--) { - arr[idx++] = block.reverseSchedule.get(i); - } - return arr; - } - - /** - * Find the CFG output of a node - * @param node The node - * @return The CFG output of the node. - */ - private static CFGNode findSingleCFGOut(Node node) { - if (node instanceof StartNode) { - for(var n:node._outputs) - if (n instanceof FunNode fun && "main".equals(fun._name) ) - return fun; - return null; - } - if( node instanceof FunNode fun ) return fun.uctrl(); - assert node._outputs.stream().filter(x -> x instanceof CFGNode).limit(2).count()<=1; - for(var n:node._outputs) if(n instanceof CFGNode cfg) return cfg; - return null; - } - - /** - * Build the final data structure after all the scheduling happened. - * @param start The start node - * @return The final schedule. - */ - private Block build(Node start) { - var blocks = new IdentityHashMap<Node, Block>(); - var queue = new Stack<NodeData>(); - queue.push(d(start)); - - // Visit all CFG nodes and create blocks for them. - // This can combine blocks. - while(!queue.isEmpty()) { - var data = queue.pop(); - var first = data.node; - Node prev; - var last = first; - Node[] arr = new Node[0]; - if (first instanceof RegionNode) { - var nodes = new ArrayList<Node>(); - for(var out: first._outputs) { - if (out instanceof PhiNode) { - if(od(out).isPresent()) nodes.add(out); - } - } - arr = nodes.toArray(arr); - } - arr = appendNodes(arr, null, data.block); - assert !(first instanceof IfNode); - for(;;) { - prev = last; - last = findSingleCFGOut(last); - if (last == null) break; - if (last instanceof RegionNode) { - if (blocks.get(last) == null && last != first) - queue.push(d(last)); - break; - } - if (last instanceof ReturnNode) break; - if (last instanceof IfNode if_) { - for(var out:if_._outputs) if (out instanceof CFGNode && blocks.get(out) == null) queue.push(d(out)); - break; - } - data = d(last); - arr = appendNodes(arr, data.node, data.block); - } - blocks.put(first, switch(last) { - case null -> new Block(arr, null, -1, new Block[0]); - case IfNode i -> new Block(arr, i, -1, new Block[2]); - case RegionNode r -> new Block(arr, r, r._inputs.find(prev), new Block[1]); - case ReturnNode r -> new Block(arr, r , -1, new Block[0]); - default -> throw new AssertionError("Unexpected block exit node"); - }); - } - // Update the next pointer of all blocks. - for (var block: blocks.values()) { - switch(block.exit) { - case null: break; - case IfNode i: - for(var out:i._outputs) if (out instanceof CProjNode p) block.next[p._idx] = blocks.get(p); - break; - case RegionNode r: - block.next[0] = blocks.get(r); - break; - default: - assert block.exit instanceof ReturnNode; - } - } - // And return the block for the start node. - return blocks.get(start); - } - - /** - * Create a schedule for the program reachable from start. - * @param start The start node - * @return The final schedule. - */ - public static Block schedule(StartNode start) { - var scheduler = new Scheduler(); - scheduler.doMarkAlive(start); - scheduler.doBuildCTF(start); - scheduler.doSchedule(); - return scheduler.build(start); - } - -} diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java index af4305c0..55c2c040 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java @@ -85,7 +85,7 @@ public class Fuzzer { code1 = FuzzerUtils.parse(script, 123, true); } catch (RuntimeException e1) { try { - FuzzerUtils.parse(script, 456, false); + FuzzerUtils.parse(script, 456, true); } catch (RuntimeException e2) { if (FuzzerUtils.isExceptionFromSameCause(e1, e2)) { if (!valid || e1.getClass() == Parser.ParseException.class) return; @@ -163,26 +163,30 @@ public class Fuzzer { check(sb.toString(), valid, seed); } - /** - * Check that no exceptions happened. - * @return true if no exception happened + * Run one seed as a regression test. Unlike the exploratory fuzzer path, + * this deliberately does not reduce on failure; old bad seeds should fail + * fast with the seed visible in the JUnit assertion. */ - public boolean noExceptions() { - return exceptions.isEmpty(); - } - - /** Run a fixed chapter-local seed without reducing or swallowing failures. */ public void fuzzPeepsRegression(long seed) { var rand = new Random(seed); var sb = new StringBuilder(); var valid = new ScriptGenerator(rand, sb, true).genProgram(); try { runCheck(sb.toString(), valid); - } catch (Throwable e) { - AssertionError ae = new AssertionError("Fuzzer regression seed failed: " + seed); + } catch( Throwable e ) { + AssertionError ae = new AssertionError("Fuzzer regression seed failed: "+seed); ae.initCause(e); throw ae; } } + + + /** + * Check that no exceptions happened. + * @return true if no exception happened + */ + public boolean noExceptions() { + return exceptions.isEmpty(); + } } diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java index 45558541..9fe2e405 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java @@ -94,13 +94,13 @@ class FuzzerUtils { /** * Parse and optimize using the given worklist seed */ - public static CodeGen parse(String script, long workListSeed, boolean reset) { + public static CodeGen parse(String script, long workListSeed, boolean resetTypes ) { var err = System.err; var out = System.out; try { System.setErr(NULL_PRINT_STREAM); System.setOut(NULL_PRINT_STREAM); - var code = new CodeGen(script, TypeInteger.BOT, workListSeed, reset); + var code = new CodeGen(script, workListSeed, resetTypes); return code.driver(CodeGen.Phase.LocalSched); } finally { System.setErr(err); diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java index 20815d5d..c5e94047 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java @@ -83,18 +83,11 @@ public class ScriptGenerator { } } - private static final List<TypeInt> INTTYPES = new ArrayList<>(); - static { - for( var e : Parser.TYPES.entrySet() ) { - if( e.getValue() instanceof TypeInteger ) { - INTTYPES.add(new TypeInt(e.getKey())); - } - } - } - - private static final Type TYPE_INT = INTTYPES.stream().filter(t->t.name.equals("int")).findAny().get(); - private static final Type TYPE_BOOL = INTTYPES.stream().filter(t->t.name.equals("bool")).findAny().get(); + private static final TypeInt TYPE_INT = new TypeInt("int"); + private static final TypeInt TYPE_BOOL = new TypeInt("bool"); + private static final TypeInt TYPE_U8 = new TypeInt("u8"); + private static final List<TypeInt> INTTYPES = new ArrayList<>(){{add(TYPE_INT); add(TYPE_BOOL); add(TYPE_U8); }}; private static class Variable { final String name; @@ -937,4 +930,4 @@ public class ScriptGenerator { else sb.append(v.name); } -}
\ No newline at end of file +} diff --git a/src/test/java/com/seaofnodes/simple/progs/extern_data.c b/src/test/java/com/seaofnodes/simple/progs/extern_data.c new file mode 100644 index 00000000..9325f65e --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/progs/extern_data.c @@ -0,0 +1,22 @@ +#include <stdint.h> +#include <stdio.h> + +int32_t counter=7; +int8_t small=-3; +double fraction=1.25; + +int64_t bump(void) { counter+=5; return counter; } +__attribute__((CALL_CONV)) extern int64_t test_global(void); + +int main(void) { + 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", + (long long)result,counter,small,fraction); + return 1; + } + // A second call must observe C's new values, not a Simple initialization copy. + counter=-2; + result=test_global(); + return result!=-19394 || counter!=6 || small!=-1 || fraction!=2.25; +} diff --git a/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c b/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c new file mode 100644 index 00000000..f65e5c84 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/progs/extern_data_import.c @@ -0,0 +1,14 @@ +#include <stdint.h> +#include <stdio.h> + +int32_t counter=7; +__attribute__((CALL_CONV)) extern int64_t test_global(void); + +int main(void) { + int64_t result=test_global(); + if(result!=710 || counter!=10) { + printf("result=%lld counter=%d\n",(long long)result,counter); + return 1; + } + return 0; +} diff --git a/src/test/java/com/seaofnodes/simple/progs/floatToInteger.c b/src/test/java/com/seaofnodes/simple/progs/floatToInteger.c new file mode 100644 index 00000000..cf9390d8 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/progs/floatToInteger.c @@ -0,0 +1,25 @@ +#include <stdint.h> +#include <math.h> +#include <stdio.h> + +extern int64_t float_to_int(double); +extern int64_t float_to_u8(double); + +int main(void) { + double values[] = {0.0,-0.0,6.92,-6.92,0.99,-0.99,255.75,-257.75, + 0x1p63,0x1.fffffffffffffp62,-0x1p63,-0x1.0000000000001p63, + INFINITY,-INFINITY,NAN,-NAN}; + for( unsigned i=0; i<sizeof(values)/sizeof(values[0]); i++ ) { + double x=values[i]; + int64_t expected=isnan(x) ? 0 : x>=0x1p63 ? INT64_MAX + : x<=-0x1p63 ? INT64_MIN : (int64_t)x; + int64_t actual=float_to_int(x), narrow=float_to_u8(x); + if( actual!=expected || narrow!=(expected&255) ) { + fprintf(stderr,"conversion of %g: got %lld / %lld, expected %lld / %lld\n", + x,(long long)actual,(long long)narrow, + (long long)expected,(long long)(expected&255)); + return 1; + } + } + return 0; +} diff --git a/src/test/java/com/seaofnodes/simple/progs/funptr.c b/src/test/java/com/seaofnodes/simple/progs/funptr.c deleted file mode 100644 index 2abe6e74..00000000 --- a/src/test/java/com/seaofnodes/simple/progs/funptr.c +++ /dev/null @@ -1,15 +0,0 @@ -#include <assert.h> -#include <stdint.h> - -typedef int64_t (*Function)(void); -typedef int64_t (*Printer)(void *, void *); -extern Function factory(int64_t); -extern Printer printer(void); - -int main(void) { - assert(factory(1)() == 42); - assert(factory(0)() == 43); - struct { uint32_t length; char data[2]; } text = {2, {'o','k'}}; - printer()(0, &text); // Unused class receiver, then the byte array. - return 0; -} diff --git a/src/test/java/com/seaofnodes/simple/progs/jig.smp b/src/test/java/com/seaofnodes/simple/progs/jig.smp deleted file mode 100644 index 29987d8e..00000000 --- a/src/test/java/com/seaofnodes/simple/progs/jig.smp +++ /dev/null @@ -1,16 +0,0 @@ -// -*- mode: java; -*- -val _fib = {int n -> - int temp=0; - int f1=1; - int f2=1; - int i=n; - while( i>1 ){ - temp = f1+f2; - f1=f2; - f2=temp; - i=i-1; - } - return f2; -}; - -_fib(10); diff --git a/src/test/java/com/seaofnodes/simple/progs/person21.c b/src/test/java/com/seaofnodes/simple/progs/person21.c deleted file mode 100644 index df95e1ca..00000000 --- a/src/test/java/com/seaofnodes/simple/progs/person21.c +++ /dev/null @@ -1,34 +0,0 @@ -#include <stdio.h> -#include <stdbool.h> -#include <stdlib.h> - -typedef struct Person { - int age; -} Person; - -typedef struct PersonArray { - unsigned int length; - int pad; - Person* data[]; -} PersonArray; - -__attribute__ ((CALL_CONV)) -extern void fcn(PersonArray* people, int index); - -int main() { - Person base[5] = {{25},{1},{18},{45},{60}}; - unsigned char buf[8+8*5]; - PersonArray* people_array = (PersonArray*)buf; - people_array->length=5; - for (int i=0; i<people_array->length; i++) people_array->data[i] = &base[i]; - - fcn(people_array, 1); - fcn(people_array, 1); - fcn(people_array, 1); - fcn(people_array, 1); - fcn(people_array, 1); - - printf("%d\n", people_array->data[1]->age); // expected: 6 - - return 0; -}
\ No newline at end of file diff --git a/src/test/java/com/seaofnodes/simple/spill/BrainFuckTest-testBrainFuck.smp b/src/test/java/com/seaofnodes/simple/spill/BrainFuckTest-testBrainFuck.smp new file mode 100644 index 00000000..71af2b7a --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/BrainFuckTest-testBrainFuck.smp @@ -0,0 +1,46 @@ +// -*- mode: java; -*- +val brain_fuck = { -> + var !program = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."; + int d = 0; + u8[] !output = new u8[0]; + u8[] !data = new u8[100]; + + for( int pc = 0; pc < program#; pc++ ) { + var command = program[pc]; + if (command == 62) { + d++; + } else if (command == 60) { + d--; + } else if (command == 43) { + data[d]++; + } else if (command == 45) { + data[d]--; + } else if (command == 46) { + // Output a byte; increase the output array size + var old = output; + output = new u8[output# + 1]; + for( int i = 0; i < old#; i++ ) + output[i] = old[i]; + output[old#] = data[d]; // Add the extra byte on the end + } else if (command == 44) { + data[d] = 42; + } else if (command == 91) { + if (data[d] == 0) { + for( int d = 1; d > 0; ) { + command = program[++pc]; + if (command == 91) d++; + if (command == 93) d--; + } + } + } else if (command == 93) { + if (data[d]) { + for( int d = 1; d > 0; ) { + command = program[--pc]; + if (command == 93) d++; + if (command == 91) d--; + } + } + } + } + return output; +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc0.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc0.smp new file mode 100644 index 00000000..d85e39e5 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc0.smp @@ -0,0 +1 @@ +return new u8[arg]; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc2.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc2.smp new file mode 100644 index 00000000..864bcdc8 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testAlloc2.smp @@ -0,0 +1 @@ +int[] !xs = new int[3]; xs[arg]=1; return xs[arg&1]; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testArray1.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testArray1.smp new file mode 100644 index 00000000..7f3bd007 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testArray1.smp @@ -0,0 +1,8 @@ +int[] !ary = new int[arg]; +// Fill [0,1,2,3,4,...] +for( int i=0; i<ary#; i++ ) + ary[i] = i; +// Fill [0,1,3,6,10,...] +for( int i=0; i<ary#-1; i++ ) + ary[i+1] += ary[i]; +return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6 diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBasic1.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBasic1.smp new file mode 100644 index 00000000..671e5b74 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBasic1.smp @@ -0,0 +1 @@ +return arg | 2; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBrainfuck.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBrainfuck.smp new file mode 100644 index 00000000..4e5ab81d --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testBrainfuck.smp @@ -0,0 +1,43 @@ +u8[] !program = new u8[106];program[0] = 43;program[1] = 43;program[2] = 43;program[3] = 43;program[4] = 43;program[5] = 43;program[6] = 43;program[7] = 43;program[8] = 91;program[9] = 62;program[10] = 43;program[11] = 43;program[12] = 43;program[13] = 43;program[14] = 91;program[15] = 62;program[16] = 43;program[17] = 43;program[18] = 62;program[19] = 43;program[20] = 43;program[21] = 43;program[22] = 62;program[23] = 43;program[24] = 43;program[25] = 43;program[26] = 62;program[27] = 43;program[28] = 60;program[29] = 60;program[30] = 60;program[31] = 60;program[32] = 45;program[33] = 93;program[34] = 62;program[35] = 43;program[36] = 62;program[37] = 43;program[38] = 62;program[39] = 45;program[40] = 62;program[41] = 62;program[42] = 43;program[43] = 91;program[44] = 60;program[45] = 93;program[46] = 60;program[47] = 45;program[48] = 93;program[49] = 62;program[50] = 62;program[51] = 46;program[52] = 62;program[53] = 45;program[54] = 45;program[55] = 45;program[56] = 46;program[57] = 43;program[58] = 43;program[59] = 43;program[60] = 43;program[61] = 43;program[62] = 43;program[63] = 43;program[64] = 46;program[65] = 46;program[66] = 43;program[67] = 43;program[68] = 43;program[69] = 46;program[70] = 62;program[71] = 62;program[72] = 46;program[73] = 60;program[74] = 45;program[75] = 46;program[76] = 60;program[77] = 46;program[78] = 43;program[79] = 43;program[80] = 43;program[81] = 46;program[82] = 45;program[83] = 45;program[84] = 45;program[85] = 45;program[86] = 45;program[87] = 45;program[88] = 46;program[89] = 45;program[90] = 45;program[91] = 45;program[92] = 45;program[93] = 45;program[94] = 45;program[95] = 45;program[96] = 45;program[97] = 46;program[98] = 62;program[99] = 62;program[100] = 43;program[101] = 46;program[102] = 62;program[103] = 43;program[104] = 43;program[105] = 46; +int d = 0; +u8[] !output = new u8[0]; +u8[] !data = new u8[100]; + +for( int pc = 0; pc < program#; pc++ ) { + var command = program[pc]; + if (command == 62) { + d++; + } else if (command == 60) { + d--; + } else if (command == 43) { + data[d]++; + } else if (command == 45) { + data[d]--; + } else if (command == 46) { + // Output a byte; increase the output array size + var old = output; + output = new u8[output# + 1]; + for( int i = 0; i < old#; i++ ) + output[i] = old[i]; + output[old#] = data[d]; // Add the extra byte on the end + } else if (command == 44) { + data[d] = 42; + } else if (command == 91) { + if (data[d] == 0) { + for( int d = 1; d > 0; ) { + command = program[++pc]; + if (command == 91) d++; + if (command == 93) d--; + } + } + } else if (command == 93) { + if (data[d]) { + for( int d = 1; d > 0; ) { + command = program[--pc]; + if (command == 93) d++; + if (command == 91) d--; + } + } + } +} +return output; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testCast.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testCast.smp new file mode 100644 index 00000000..d1368767 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testCast.smp @@ -0,0 +1 @@ +struct _Bar { int x; }; var b = arg ? new _Bar; return b ? b.x++ + b.x++ : -1; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags1.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags1.smp new file mode 100644 index 00000000..f630e959 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags1.smp @@ -0,0 +1,5 @@ +bool b1 = arg == 1; +bool b2 = arg == 2; +if (b2) if (b1) return 1; +if (b1) return 2; +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags2.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags2.smp new file mode 100644 index 00000000..70bfc226 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFlags2.smp @@ -0,0 +1,6 @@ +bool b1 = arg == 1; +while (arg > 0) { + arg--; + if (b1) arg--; +} +return arg; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFltArg.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFltArg.smp new file mode 100644 index 00000000..caa11f12 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testFltArg.smp @@ -0,0 +1 @@ +return {int i, flt f, int j->return i+f+j;}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testMergeSort.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testMergeSort.smp new file mode 100644 index 00000000..417eba72 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testMergeSort.smp @@ -0,0 +1,46 @@ +// based on the top-down version from https://en.wikipedia.org/wiki/Merge_sort + +val _merge_sort = { int[] a, int[] b, int n -> + _copy_array(a, 0, n, b); + _split_merge(a, 0, n, b); +}; + +val _split_merge = { int[] b, int begin, int end, int[] a -> + if (end - begin <= 1) + return 0; + int middle = (end + begin) / 2; + _split_merge(a, begin, middle, b); + _split_merge(a, middle, end, b); + _merge(b, begin, middle, end, a); + return 0; +}; + +val _merge = { int[] b, int begin, int middle, int end, int[] a -> + int i = begin, j = middle; + + for (int k = begin; k < end; k++) { + // && and || + bool cond = false; + if (i < middle) { + if (j >= end) cond = true; + else if (a[i] <= a[j]) cond = true; + } + if (cond) b[k] = a[i++]; + else b[k] = a[j++]; + } +}; + +val _copy_array = { int[] a, int begin, int end, int[] b -> + for (int k = begin; k < end; k++) + b[k] = a[k]; +}; + +int[] !a = new int[arg]; +int[] !b = new int[a#]; + +for (int i = 0; i < a#; i++) + a[i] = a# - i; + +_merge_sort(a, b, a#); + +return a; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonFloat.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonFloat.smp new file mode 100644 index 00000000..fd76478f --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonFloat.smp @@ -0,0 +1,11 @@ +// Newtons approximation to the square root +val _sqrt = { flt x -> + flt guess = x; + while( 1 ) { + flt next = (x/guess + guess)/2; + if( next == guess ) return guess; + guess = next; + } +}; +flt farg = arg; +return _sqrt(farg) + _sqrt(farg+2.0); diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonInteger.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonInteger.smp new file mode 100644 index 00000000..8912dcd9 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testNewtonInteger.smp @@ -0,0 +1,10 @@ +// Newtons approximation to the square root +val _sqrt = { int x -> + int guess = x; + while( 1 ) { + int next = (x/guess + guess)/2; + if( next == guess ) return guess; + guess = next; + } +}; +return _sqrt(arg) + _sqrt(arg+2); diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp new file mode 100644 index 00000000..951cdc32 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp @@ -0,0 +1,35 @@ +struct _String { + u8[] cs; + new _String={ u8[] b -> cs=b; }; + int _hashCode; +}; + +val _equals = { !_String !self, !_String s -> + if( self == s ) return true; + if( self.cs# != s.cs# ) return false; + for( int i=0; i< self.cs#; i++ ) + if( self.cs[i] != s.cs[i] ) + return false; + return true; +}; + +val _hashCode = { !_String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +val _hashCodeString = { !_String self -> + int hash=0; + if( self.cs ) { + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; + } + if( !hash ) hash = 123456789; + return hash; +}; + +!_String !s = new _String(new u8[17]); +s.cs[0] = 67; // C +s.cs[1] = 108; // l +_hashCode(s); diff --git a/src/test/java/com/seaofnodes/simple/progs/array1.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testArray1.smp index 6b10f92c..6b10f92c 100644 --- a/src/test/java/com/seaofnodes/simple/progs/array1.smp +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testArray1.smp diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testNewtonFloat.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testNewtonFloat.smp new file mode 100644 index 00000000..58449f9c --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testNewtonFloat.smp @@ -0,0 +1,13 @@ +// -*- mode: java; -*- +// Newtons approximation to the square root +val _test_sqrt = { flt x -> + flt epsilon = 1e-15; + flt guess = x; + while( 1 ) { + flt next = (x/guess + guess)/2; + if( guess-epsilon <= next & next <= guess+epsilon ) return guess; + //if( guess==next ) return guess; + guess = next; + } +}; +flt farg = arg; return _test_sqrt(farg) + _test_sqrt(farg+2.0); diff --git a/src/test/java/com/seaofnodes/simple/progs/stringHash21.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testString.smp index 257f79f9..a18a5b88 100644 --- a/src/test/java/com/seaofnodes/simple/progs/stringHash21.smp +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testString.smp @@ -1,11 +1,12 @@ // -*- mode: java; -*- -struct String { +struct _String { u8[] cs; int _hashCode; + new _String = { u8[] data -> cs=data; }; }; // Compare two Strings -val equals = { !String !self, !String s -> +val equals = { !_String !self, !_String s -> if( self == s ) return true; if( self.cs# != s.cs# ) return false; for( int i=0; i< self.cs#; i++ ) @@ -15,13 +16,13 @@ val equals = { !String !self, !String s -> }; // Return the String hashCode (cached, and never 0) -val hashCode = { !String self -> +val hashCode = { !_String self -> self._hashCode ? self._hashCode : (self._hashCode = _hashCodeString(self)); }; -val _hashCodeString = { !String self -> +val _hashCodeString = { !_String self -> int hash=0; if( self.cs ) for( int i=0; i< self.cs#; i++ ) @@ -29,3 +30,6 @@ val _hashCodeString = { !String self -> if( !hash ) hash = 123456789; return hash; }; + +// This fixture exports its API, not the last declaration as a result. +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testAntiDeps1.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testAntiDeps1.smp new file mode 100644 index 00000000..315cc5b6 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testAntiDeps1.smp @@ -0,0 +1,10 @@ +struct _S { int f; }; +var v0 = new _S; +!_S? !v1; +if (arg) v1 = new _S; +if (v1) { + v0.f = v1.f; +} else { + v0.f = 2; +} +return v0; diff --git a/src/test/java/com/seaofnodes/simple/progs/no_stack_arg_count.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount-2.smp index 20a05445..20a05445 100644 --- a/src/test/java/com/seaofnodes/simple/progs/no_stack_arg_count.smp +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount-2.smp diff --git a/src/test/java/com/seaofnodes/simple/progs/arg_count.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount.smp index 6b6bc0ae..6b6bc0ae 100644 --- a/src/test/java/com/seaofnodes/simple/progs/arg_count.smp +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testArgCount.smp diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testFibExport.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testFibExport.smp new file mode 100644 index 00000000..149872a7 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testFibExport.smp @@ -0,0 +1,10 @@ +val fib = {int n -> + int f1=1; + int f2=1; + while( n-- > 1 ){ + int temp = f1+f2; + f1=f2; + f2=temp; + } + return f2; +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testInfinite.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testInfinite.smp new file mode 100644 index 00000000..1804ad59 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testInfinite.smp @@ -0,0 +1 @@ +struct _S { int i; }; !_S !s = new _S; while(1) s.i++; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testLoop2.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testLoop2.smp new file mode 100644 index 00000000..cc54f790 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testLoop2.smp @@ -0,0 +1,9 @@ +int i = 0; +while(true) { + i += 1; + if( i==0 ) continue; + if( i==1 ) continue; + if( i==2 ) break; + continue; +} +return i; diff --git a/src/test/java/com/seaofnodes/simple/progs/newtonFloat.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testNewtonExport.smp index be2dffec..2786558e 100644 --- a/src/test/java/com/seaofnodes/simple/progs/newtonFloat.smp +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testNewtonExport.smp @@ -1,5 +1,3 @@ -// -*- mode: java; -*- -// Newtons approximation to the square root val test_sqrt = { flt x -> flt epsilon = 1e-15; flt guess = x; diff --git a/src/test/java/com/seaofnodes/simple/progs/person21.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testPerson.smp index b5e6ee90..09285034 100644 --- a/src/test/java/com/seaofnodes/simple/progs/person21.smp +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testPerson.smp @@ -6,4 +6,4 @@ struct Person { val fcn = { !Person?[] ps, int x -> if( ps[x] ) ps[x].age++; -};
\ No newline at end of file +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testSieve.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testSieve.smp new file mode 100644 index 00000000..beb0b3ad --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testSieve.smp @@ -0,0 +1,30 @@ +val sieve = { int N -> + // The main Sieve array + bool[] !ary = new bool[N]; + // The primes less than N + u32[] !primes = new u32[N>>1]; + // Number of primes so far, searching at index p + int nprimes = 0, p=2; + // Find primes while p^2 < N + while( p*p < N ) { + // skip marked non-primes + while( ary[p] ) p++; + // p is now a prime + primes[nprimes++] = p; + // Mark out the rest non-primes + for( int i = p + p; i < ary#; i+= p ) + ary[i] = true; + p++; + } + + // Now just collect the remaining primes, no more marking + for( ; p < N; p++ ) + if( !ary[p] ) + primes[nprimes++] = p; + + // Copy/shrink the result array + u32[] !rez = new u32[nprimes]; + for( int j=0; j < nprimes; j++ ) + rez[j] = primes[j]; + return rez; +}; diff --git a/src/test/java/com/seaofnodes/simple/progs/stringHash.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testStringExport.smp index 7344fcdd..ee5a0560 100644 --- a/src/test/java/com/seaofnodes/simple/progs/stringHash.smp +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testStringExport.smp @@ -1,7 +1,7 @@ -// -*- mode: java; -*- struct String { u8[] cs; int _hashCode; + new String = { u8[] data -> cs=data; }; }; // Compare two Strings @@ -28,3 +28,6 @@ val _hashCodeString = { !String self -> if( !hash ) hash = 123456789; return hash; }; + +// This fixture exports its API, not the last declaration as a result. +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testHelloWorld.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testHelloWorld.smp new file mode 100644 index 00000000..1bcac36d --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testHelloWorld.smp @@ -0,0 +1,4 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +_p("Hello, World!"); +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testInfiniteReturn.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testInfiniteReturn.smp new file mode 100644 index 00000000..cc549808 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testInfiniteReturn.smp @@ -0,0 +1 @@ +struct _S { int i; }; !_S !s = new _S; while(1) s.i++; return s.i; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testNarrowCReturns.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testNarrowCReturns.smp new file mode 100644 index 00000000..21293a7a --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testNarrowCReturns.smp @@ -0,0 +1,12 @@ +{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"; +val test_i8={->return c_i8();}; +val test_u8={->return c_u8();}; +val test_i16={->return c_i16();}; +val test_u16={->return c_u16();}; +val test_i32={->return c_i32();}; +val test_u32={->return c_u32();}; +val test_i64={->return c_i64();}; +val test_cmp={->return c_i32()==-1;}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testPerson.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testPerson.smp new file mode 100644 index 00000000..f860091d --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testPerson.smp @@ -0,0 +1,8 @@ +struct Person { + i32 age; +}; + +val fcn = { !Person?[] ps, int x -> + if( ps[x] ) + ps[x].age++; +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail.smp new file mode 100644 index 00000000..ec9c6d50 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail.smp @@ -0,0 +1,4 @@ +struct _Person { i32 age;}; +!_Person !p = new _Person; +p.age = (arg<<17)>>17; +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail2.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail2.smp new file mode 100644 index 00000000..d5264aa2 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextFail2.smp @@ -0,0 +1,5 @@ + + struct _Person { i32 age;}; + !_Person !p = new _Person; + p.age = (arg<<48)>>48; + return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextSuccess.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextSuccess.smp new file mode 100644 index 00000000..c4398a89 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter22Test-testSextSuccess.smp @@ -0,0 +1,5 @@ +// Should fold away sign extend +struct _Person { i8 age;}; +!_Person !p = new _Person; +p.age = (arg<<48)>>48; +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp new file mode 100644 index 00000000..49c80a99 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp @@ -0,0 +1,34 @@ +// -*- mode: java; -*- +struct _String { + u8[] cs; + int _hashCode; + new _String = { u8[] data -> cs=data; }; +}; + +// Compare two Strings +val equals = { !_String !self, !_String s -> + if( self == s ) return true; + if( self.cs# != s.cs# ) return false; + for( int i=0; i< self.cs#; i++ ) + if( self.cs[i] != s.cs[i] ) + return false; + return true; +}; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !_String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + +val _hashCodeString = { !_String self -> + int hash=0; + for( int i=0; i< self.cs#; i++ ) + hash = hash*31 + self.cs[i]; + if( !hash ) hash = 123456789; + return hash; +}; + +// This fixture exports its API, not the last declaration as a result. +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAnd.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAnd.smp new file mode 100644 index 00000000..da85ef0e --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAnd.smp @@ -0,0 +1,11 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int a = 1; +int b = 1; + +if(a && b) { + _p("And"); +} else { + _p("Or"); +} +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAndPtr.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAndPtr.smp new file mode 100644 index 00000000..a1e4e9ab --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testAndPtr.smp @@ -0,0 +1,12 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +struct _S { !_S? !fld; new _S={ !_S? f -> fld=f; }; }; + +val ptr = arg == 1 ? null : new _S(arg==1 ? null : new _S(null)); +if( ptr && ptr.fld ) { + _p("true"); +} else { + _p("false"); +} + +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffAnd.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffAnd.smp new file mode 100644 index 00000000..1a6b5cf3 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffAnd.smp @@ -0,0 +1,17 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int a = 1; +int b = 1; + +int x=1; +int y=1; +int z=0; + +int g = x++ && y++ && z++; + +if(x == 2 && y == 2 && z == 1 && g == 0) { + _p("Effected"); +} else { + _p("Not effected"); +} +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffOr.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffOr.smp new file mode 100644 index 00000000..f2bb16c5 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testCondSideEffOr.smp @@ -0,0 +1,24 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int a = 1; +int b = 1; + +int x = -1; +int y=1; +int z= -1; + +int g = x++ || y++ || z++; + +int switch = 0; +if(x == 4 || y == 4 || z == 4) { + switch = -1; +} else { + switch = 1; +} +int cd = 1; +if((x == 0 && y == 1 && z==-1) && switch && g) { + _p("Effected"); +} else { + _p("Not effected"); +} +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFRefFields.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFRefFields.smp new file mode 100644 index 00000000..eac0a542 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFRefFields.smp @@ -0,0 +1,20 @@ +// A hypothetical scanner class +struct _Scan { + int !x; + u8[~] buf; + new _Scan={ u8[~] b -> buf=b; }; + // Skip whitespace + val skip = { -> + while( buf[x] <= ' ' ) + x++; + }; + // Peek a character; if matched consume it, else false. + val peek = { u8 c -> + skip(); + if( buf[x] != c ) return false; + x++; + return true; + }; +}; +!_Scan !s = new _Scan(" q"); +return s.peek('q'); diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFuncCall.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFuncCall.smp new file mode 100644 index 00000000..8a7b90d9 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testFuncCall.smp @@ -0,0 +1,16 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +// -*- mode: java; -*- +int a = 1; +int b = 1; + +var _sq = { int x -> + x*x; +}; + +if(a && _sq(0)) { + _p("And"); +} else { + _p("Or"); +} +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testMethod.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testMethod.smp new file mode 100644 index 00000000..ced649e3 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testMethod.smp @@ -0,0 +1,17 @@ +// A hypothetical scanner class +struct _Scan { + int !x; + u8[~] buf; + new _Scan={ u8[~] b -> buf=b; }; + // Skip whitespace + val skip = { -> + while( buf[x] <= ' ' ) + x++; + }; + val require = { u8 ch -> + skip(); + buf[x++]==ch; + }; +}; +!_Scan !s = new _Scan(" [1,2]"); +return s.require('['); diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOr.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOr.smp new file mode 100644 index 00000000..dbab1ae5 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOr.smp @@ -0,0 +1,13 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int a = 1; +int b = 0; + +if(a++ || b++ ) { + if(b == 0 && a == 2) { + _p("Or"); + } +} else{ + _p("And"); +} +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopLocal.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopLocal.smp new file mode 100644 index 00000000..c4e4f9b5 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopLocal.smp @@ -0,0 +1,10 @@ +struct _S { int f; }; +int x=0; +int result=arg&1; +if(!result) { + !_S !s=new _S; + while(x<3) x++; + s.f=++x; + result=s.f; +} +return x*10+result; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopMemory.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopMemory.smp new file mode 100644 index 00000000..79478408 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter23Test-testOrRhsLoopMemory.smp @@ -0,0 +1,11 @@ +struct _S { int f; }; +!_S !s=new _S; +int result=arg&1; +if(!result) { + !_S !t=new _S; + while(s.f<3) s.f++; + s.f++; + t.f=s.f; + result=t.f; +} +return s.f*10+result; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp new file mode 100644 index 00000000..f292254e --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp @@ -0,0 +1,217 @@ + + i32 errno = "C"; + // fd buf len -> len + { i32 i64 u32 -> u32 } write = "C"; + // fd buf len -> len + { i32 i64 u32 -> u32 } read = "C"; + // status + { i32 -> i32 } _exit = "C"; + // addr len prot flags fd off -> void* + { i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C"; + // mmap flags + i32 PROT_EXEC = 4; + i32 PROT_READ = 1; + i32 PROT_WRITE= 2; + i32 PROT_NONE = 0; + i32 MAP_PRIVATE = 2; + i32 MAP_ANON = 32; + + val _copy = { u8[] dst, int doff, u8[~] src, int soff, int len -> + for( int i=0; i<len; i++ ) + dst[i+doff] = src[i+soff]; + return dst; + }; + struct _aryu8 { + u32 !len = 0; + u8[] !buf; + new _aryu8={ u8[] b -> buf=b; }; + // Grow as needed + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + u8[] !buf2 = new u8[newlen]; + _copy(buf2,0,buf,0,buf#); + buf = buf2; + self; + }; + + // Add a character to aryu8 + val add = { u8 c -> + _grow(1).buf[len++] = c; + self; + }; + + // Add a plain array to the aryu8 + val addAry = { u8[~] ary -> + _grow(ary#); + _copy(buf,len,ary,0,ary#); + len += ary#; + self; + }; + + // Remove x chars from the tail. + val undo = { int 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; + } + self; + }; + + // Write to fd, returns the number of bytes written (len). + val write = { i32 fd -> i64 ptr = buf; write(fd,ptr,len); }; + }; +struct _Scan { + int !x; + u8[~] buf; + new _Scan={ u8[~] b -> buf=b; }; + + // Skip whitespace + val skip = { -> + while( buf[x] <= ' ' ) + x++; + return self; + }; + + // True if find required character, and skip it; else False. + val match = { u8 c -> + skip(); + if( buf[x] != c ) return false; + x++; + return true; + }; + + // Parse a simple number + val number = { -> + skip(); + bool neg = match('-'); + i64 n = 0; + while( '0' <= buf[x] & buf[x] <= '9' ) + n = n*10 + (buf[x++] - '0'); + return neg ? -n : n; + }; + }; +struct _aryi64 { + u32 !len = 0; + i64[] !buf; + new _aryi64={ i64[] b -> buf=b; }; + // Grow as needed + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + i64[] !buf2 = new i64[newlen]; + for( int i=0; i<buf#; i++ ) + buf2[i] = buf[i]; + buf = buf2; + self; + }; + + // Add a i64 to aryu8 + val add = { i64 c -> + _grow(1).buf[len++] = c; + self; + }; + + // Write to aryu8 + val str = { -> + // TODO: clunky default constructor syntax + var str = new _aryu8(new u8[1]).add('['); + for( int i=0; i<len; i++ ) + str.num(buf[i]).add(',').add(' '); + str.undo(2).add(']'); + }; + }; + val _parseAryI64 = { u8[~] str -> + !_Scan !s = new _Scan(str); + if( !s.match('[') ) return null; + // Array of parsed results + var nums = new _aryi64(new i64[1]); + // Parse a list of numbers + if( !s.match(']') ) { // Handle the empty list + // Parse "123, " until a something else + while( s.x < s.buf# ) { + nums.add(s.number()); + if( !s.match(',') ) + break; + }; + if( !s.match(']') ) return null; // Close the list + } + return nums; + }; + + + // Print string to stdout + val _p = { u8[~] str -> + i64 ptr = str; // cast array base to i64 + return write(1,ptr,str#); + }; + + // Return stdin as a u8[] + val _stdin = { -> + var buf = new u8[8*1024]; + i64 ptr = buf; + int len = 0; + while( true ) { + int rez = read(0,ptr,buf#); + if( rez < 0 ) _exit(errno); + len += rez; + if( rez < buf# ) + break; + _exit(-2); // Need to implement + } + // Compress + var dst = new u8[len]; + return _copy(dst,0,buf,0,len); + }; + // -*- mode: java; -*- + +// An implementation of +// https://sampleprograms.io/projects/bubble-sort/ +// +// Bubble sort, with reading (parsing) and writing (formatting) the data. + +val _sort = { !_aryi64 nums -> + var done = false; + while( !done ) { + done = true; + for( var i = 0; i < nums.len-1; i++ ) + if( nums.buf[i] > nums.buf[i+1] ) { + int tmp = nums.buf[i]; + nums.buf[i] = nums.buf[i+1]; + nums.buf[i+1] = tmp; // Swap + done = false; + } + } +}; + +// Read from stdin a line of the form "[ 4, 3, 2, 1]" and print it sorted. +val main = { -> + // Parse array of numbers from stdin + var nums = _parseAryI64( _stdin() ); + if( !nums ) { + _p("stdin must look like [4, 3, 2, 1]"); + return 1; + } + + // Bubble sort array of integers + _sort(nums); + + // Print a list of numbers to stdout + nums.str().write(1); + return 0; +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testNewtonInteger.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testNewtonInteger.smp new file mode 100644 index 00000000..572e4ad7 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testNewtonInteger.smp @@ -0,0 +1,11 @@ +// Newtons approximation to the square root +val _sqrt = { int x -> + int guess = x; + while( 1 ) { + int next = (x/guess + guess)/2; + if( next == guess ) return guess; + guess = next; + } +}; +int cast_int = arg+2; +return _sqrt(arg) + _sqrt(cast_int); diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testStackArguments.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testStackArguments.smp new file mode 100644 index 00000000..b7d52dc5 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testStackArguments.smp @@ -0,0 +1,5 @@ +val addAll = { int i0, flt f1, int i2, flt f3, int i4, flt f5, int i6, flt f7, int x8, flt f9, int i10, flt f11, int i12, flt f13, int i14, flt f15, int x16, flt f17 int x18, flt f19 -> + return + i0 + f1+ i2+ f3+ i4+ f5+ i6+ f7+ x8 +f9 + + i10+f11+i12+f13+i14+f15+x16+f17+x18+f19 ; +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack1.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack1.smp new file mode 100644 index 00000000..ded92426 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack1.smp @@ -0,0 +1,10 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int something_specific = 11; + +if (10 < something_specific < 20) { + _p("In Range"); +} else { + _p("Out of Range"); +} +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10-2.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10-2.smp new file mode 100644 index 00000000..d9fb38d5 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10-2.smp @@ -0,0 +1 @@ +return (0 < 1) == (1>0); diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10.smp new file mode 100644 index 00000000..f92e5b00 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack10.smp @@ -0,0 +1,3 @@ +var stack10 = { int x -> + (x < 1) == (1>x); +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11-2.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11-2.smp new file mode 100644 index 00000000..b5aac70b --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11-2.smp @@ -0,0 +1 @@ +return 0 < arg < arg+1 < 4; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11.smp new file mode 100644 index 00000000..f50e3512 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack11.smp @@ -0,0 +1,3 @@ +var stack11 = { int x -> + 0 < x < x+1 < 4; +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12-2.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12-2.smp new file mode 100644 index 00000000..575d1b4e --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12-2.smp @@ -0,0 +1 @@ +return 0 != arg != 2; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12.smp new file mode 100644 index 00000000..b4f36c2e --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack12.smp @@ -0,0 +1,3 @@ +var stack12 = { int x -> + 0 != x != 2; +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13-2.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13-2.smp new file mode 100644 index 00000000..9bde9687 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13-2.smp @@ -0,0 +1 @@ +return 0 == arg == 1; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13.smp new file mode 100644 index 00000000..ae7b38a3 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack13.smp @@ -0,0 +1,3 @@ +var stack13 = { int x -> + 0 == x == 1; +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack2.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack2.smp new file mode 100644 index 00000000..5590c42f --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack2.smp @@ -0,0 +1,10 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int score = 75; + +if (60 <= score < 90) { + _p("In Range"); +} else { +_p("Out of range"); +} +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack3.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack3.smp new file mode 100644 index 00000000..4700f8ce --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack3.smp @@ -0,0 +1,11 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +var _sq = { int x -> + x*x; +}; + +int score = 5; + +val str = (_sq(2) <= score < _sq(3)) ? "In Range" : "Out of range"; +_p(str); +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack4.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack4.smp new file mode 100644 index 00000000..47623b30 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack4.smp @@ -0,0 +1,10 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +int something_specific = 9; + +if (10 < something_specific < 20) { + _p("In Range"); +} else { + _p("Out of Range"); +} +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack5.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack5.smp new file mode 100644 index 00000000..3c2c4208 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack5.smp @@ -0,0 +1,15 @@ +{i32 i64 u32 -> u32} write="C"; +val _p={u8[~] str -> i64 ptr=str; return write(1,ptr,str#);}; +var _sq = { int x -> + x*x; +}; + +int score = 5; + +if (_sq(2) <= score < _sq(3)) { +_p("In Range"); +} else { +_p("Out of range"); +} + +return 0; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9-2.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9-2.smp new file mode 100644 index 00000000..f54558f5 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9-2.smp @@ -0,0 +1 @@ +return (0 < 1) == 1; diff --git a/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9.smp b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9.smp new file mode 100644 index 00000000..15979be6 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/Chapter24Test-testStack9.smp @@ -0,0 +1,3 @@ +var stack9 = { int x -> + (x < 1) == 1; +}; diff --git a/src/test/java/com/seaofnodes/simple/spill/MergeSortTest-testMergeSort0.smp b/src/test/java/com/seaofnodes/simple/spill/MergeSortTest-testMergeSort0.smp new file mode 100644 index 00000000..e6478eeb --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/MergeSortTest-testMergeSort0.smp @@ -0,0 +1,35 @@ + val merge_sort = { int[] a -> + int[] !b = new int[a#]; + copy_array (a, 0, a#, b); + split_merge(a, 0, a#, b); + }; + + val split_merge = { int[] b, int begin, int end, int[] a -> + if (end - begin <= 1) + return 0; + int middle = (end + begin) / 2; + split_merge(a, begin, middle, b); + split_merge(a, middle, end, b); + merge(b, begin, middle, end, a); + return 0; + }; + + val merge = { int[] b, int begin, int middle, int end, int[] a -> + int i = begin, j = middle; + + for (int k = begin; k < end; k++) { + // && and || + bool cond = false; + if (i < middle) { + if (j >= end) cond = true; + else if (a[i] <= a[j]) cond = true; + } + if (cond) b[k] = a[i++]; + else b[k] = a[j++]; + } + }; + + val copy_array = { int[] a, int begin, int end, int[] b -> + for (int k = begin; k < end; k++) + b[k] = a[k]; + }; diff --git a/src/test/java/com/seaofnodes/simple/spill/README.ja.md b/src/test/java/com/seaofnodes/simple/spill/README.ja.md new file mode 100644 index 00000000..5ae019dd --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/README.ja.md @@ -0,0 +1,50 @@ +# 過去の割り当てコホート + +[English](README.md) | 日本語 + +この文書は [Historical allocation cohorts](README.md) の日本語訳です。 + +`cohorts.tsv` は、第24章の `SpillStats` で測定した212件のコンパイル項目を固定したものです。 +コホート20〜24の項目数は、それぞれ39、52、24、30、67です。 +ソースファイルは内容で重複を除き、最初に参照したテストの名前を付けています。 +マニフェストには、テストの識別情報、ターゲット、ABI、元の停止フェーズを保持しています。 +`Win64` は、第25章で使う ABI の表記 `win64` に変更しています。 +引数の型は i64、再実行のシードは全項目で123です。 + +これらの入力は、この最終監査で新たな正しさの回帰テストを追加する前に、 +第24章のテストから取得しました。比較のための安定したコーパスです。 +変更する場合は、比較可能性が失われる理由を説明する必要があります。 +人工的なアロケータの回帰テストや命令の回帰テストでの割り当ては、これらのコホートには含めません。 + +第25章では、以前のソースをすべて無変更で解析できるわけではありません。適応のための変更を明記します。 + +- String、Scan、連結された S のコンストラクタでは、`new S { field=...; }` を、 + 宣言されたコンストラクタとその引数に置き換えています。 + レシーバーを明示するスキャナ関数は、操作を保ったままインスタンスメソッドにしています。 +- 保存した `Chapter21Test-testStringExport`、`Chapter21AllocTest-testString`、 + `Chapter23AllocTest-testString` のソースは、必須の非 null フィールド `cs` を初期化するために、 + `new String = { u8[] data -> cs=data; };` を宣言しています。 + これによって10件の再実行項目(4件、3件、3件)を修正していますが、 + 等価性・ハッシュの本体やマニフェストの構成項目は変えていません。 + これらの適応済みソースが新しい比較基準です。以前のコンストラクタなしのフィクスチャと + 同一入力として、その集計値を比較することはできません。 +- 短絡評価の右辺で初期化する2つのループは、同等のガード付きブロックにしています。 + 割り当て、副作用、最終結果を保持しています。 +- 単純な I/O のケースには、以前の `write` の束縛と表示用の補助関数を直接含めています。 + Bubble Sort には、名前空間の接頭辞を平坦化し、コンストラクタを明示し、 + 置換用バッファを可変にした、以前のライブラリの本体を含めています。 + これにより、第25章で大幅に改訂した例に差し替えるのではなく、以前のワークロードを保持しています。 +- 元の Newton の浮動小数点数版2種類を含め、ほかのソース本文は保持しています。 + +すべての項目を **RegAlloc まで**再実行し、スケジューリング済みのレジスタ制約を確認します。 +過去のフェーズの列は来歴情報であり、第25章で以前のネイティブ実行ハーネスを動かすことを約束するものではありません。 +適応済み Bubble Sort は、割り当ての比較可能性のために、過去の `i32 errno="C"` 宣言を保持しています。 +直接のデータ束縛は現在、3つのターゲットすべてでエンコードできますが、 +errno がスレッドローカルまたはアクセサ経由の libc とは、この宣言をリンクできません。 +現在のシステムライブラリのコードは、代わりに `libc.errno()` を使います。 +このコーパスを、過去のランタイムの実行カバレッジとして数えてはいけません。 + +その後 `SpillStats` は、シード456で現在のシステムライブラリをエンコーディングまでコンパイルし、 +既存のシードとネイティブ検査を使って Chapter25Test を実行します。 +これらを合わせてコホート25とします。ライブラリまたはテストが失敗すると、レポートも失敗します。 +部分的な集計値では改善を示せません。現在のコンパイラの実行検証は、通常の章全体のテストスイートが担います。 diff --git a/src/test/java/com/seaofnodes/simple/spill/README.md b/src/test/java/com/seaofnodes/simple/spill/README.md new file mode 100644 index 00000000..b5ab9d80 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/README.md @@ -0,0 +1,78 @@ +# Historical allocation cohorts + +English | [日本語](README.ja.md) + +`cohorts.tsv` freezes 199 compilation entries matching Chapter 24's +`SpillStats`: 39, 52, 26, 24, and 58 entries for cohorts 20-24. The source files +are deduplicated by contents and named for their first referring test. The +manifest preserves test identity, target, ABI, and the original stopping phase. +`Win64` becomes `win64`, the Chapter 25 ABI spelling. Argument type is i64 and +the replay seed is 123 throughout. + +The original 212 entries were captured from the Chapter 24 tests before the +allocator audit's new correctness regressions. The 2026-10-07 refresh adds +Chapter22Test.testNarrowCReturns on x86 SystemV and Win64, matching the two +entries already measured in Chapters 22-24. The source is unchanged; only the +manifest's ABI spelling changes. The [cross-chapter matrix](../../../../../../../../docs/regalloc-spills.md) +checks matching test/CPU/ABI membership, including repeated compilations. +The same refresh removes all 15 Jig entries: six from cohort 23 and nine from +cohort 24. Jig methods are ignored scratch pads for debugging, not regression +tests or allocation benchmarks. Their frozen source copies were removed too. +These inputs are a stable comparison corpus; changing one +requires explaining the resulting break in comparability. Synthetic allocator +and instruction regressions do not contribute allocations to these cohorts. + +Chapter 25 cannot parse all earlier sources unchanged. Adaptations are explicit: + +- String, Scan, and linked S constructors replace `new S { field=...; }` with + declared constructors and arguments. The explicit-receiver scanner functions + become instance methods, preserving their operations. +- The saved `Chapter21Test-testStringExport`, `Chapter21AllocTest-testString`, + and `Chapter23AllocTest-testString` sources declare + `new String = { u8[] data -> cs=data; };` to initialize the required non-null + `cs` field. This repairs ten replay entries (four, three, and three), without + changing their equality/hash bodies or manifest membership. These adapted + sources are a new comparison baseline; their totals cannot be compared as + identical inputs with the earlier constructor-free fixtures. +- The two short-circuit RHS initializer loops become equivalent guarded blocks, + preserving allocation, side effects, and the final result. Their RHS assigns + the field value directly: Simple's `||` returns an operand, not a normalized + boolean. The 2026-10-07 cleanup removes an erroneous `!!` in those adaptations; + both fixtures return 44 for even arguments and 1 for odd arguments. +- Simple I/O cases include the old `write` binding and print helper directly. + Bubble Sort includes the old library bodies, with namespace prefixes flattened, + explicit constructors, and mutable replacement buffers. This retains the old + workload instead of substituting Chapter 25's substantially revised example. +- Local implementation names are private in eleven fixtures: the four Newton + variants that call their helper from the top level, Chapter 20's MergeSort, + String and Cast programs, and Chapter 22's three sign-extension programs and + InfiniteReturn. Their helpers/classes acquire an underscore prefix; operations, + inputs and final results are unchanged. This avoids exporting implementation + functions and allocation factories solely because Chapter 25 treats top-level + declarations as module members. Export/API fixtures, including NewtonExport, + StringExport, Person and the C-return wrappers, retain their public names. + These visibility adaptations change the generated workload and are recorded + separately from the allocator improvement in the cross-chapter comparison. +- The remaining visibility cleanup makes local classes/functions private in + AntiDeps1, Infinite, the Chapter 21/23 allocation String cases, the Chapter 23 + scanner/pointer/short-circuit/function cases, and Stack3/Stack5. Bubble Sort's + copied library implementation is private; its `main` stays public. The three + adapted String API fixtures end in `return 0;`, avoiding an accidental return + of the last declared function. Their equality/hash APIs stay public, and the + named StringExport fixture also keeps its public String factory. No cohort + entries, workload operations, or intentional export APIs are removed. +- Other source texts are retained. + +All entries replay **through RegAlloc**, checking scheduled register constraints. +The historical phase column is provenance, not a promise to run those old native +harnesses in Chapter 25. The adapted Bubble Sort retains its historical +`i32 errno="C"` declaration for allocation comparability. Direct data bindings +now encode on all three targets, but this declaration cannot link to a libc +whose errno is thread-local or accessor-based. Current system-library code uses +`libc.errno()` instead. Do not count this corpus as historical runtime coverage. + +`SpillStats` then compiles the current system library through encoding at seed +456, and runs Chapter25Test with its existing seeds and native checks. Together +these form cohort 25. A library or test failure makes the report fail; partial +totals cannot establish an improvement. The ordinary full chapter suite remains +the execution validation for the current compiler. diff --git a/src/test/java/com/seaofnodes/simple/spill/cohorts.tsv b/src/test/java/com/seaofnodes/simple/spill/cohorts.tsv new file mode 100644 index 00000000..83b19d2a --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/spill/cohorts.tsv @@ -0,0 +1,200 @@ +# cohort test cpu abi original phase source +Chapter20 Chapter20Test.testMergeSort x86_64_v2 SystemV RegAlloc Chapter20Test-testMergeSort.smp +Chapter20 Chapter20Test.testMergeSort riscv SystemV RegAlloc Chapter20Test-testMergeSort.smp +Chapter20 Chapter20Test.testMergeSort arm SystemV RegAlloc Chapter20Test-testMergeSort.smp +Chapter20 Chapter20Test.testNewtonInteger x86_64_v2 SystemV RegAlloc Chapter20Test-testNewtonInteger.smp +Chapter20 Chapter20Test.testNewtonInteger riscv SystemV RegAlloc Chapter20Test-testNewtonInteger.smp +Chapter20 Chapter20Test.testNewtonInteger arm SystemV RegAlloc Chapter20Test-testNewtonInteger.smp +Chapter20 Chapter20Test.testCast x86_64_v2 SystemV RegAlloc Chapter20Test-testCast.smp +Chapter20 Chapter20Test.testCast riscv SystemV RegAlloc Chapter20Test-testCast.smp +Chapter20 Chapter20Test.testCast arm SystemV RegAlloc Chapter20Test-testCast.smp +Chapter20 Chapter20Test.testNewtonFloat x86_64_v2 SystemV RegAlloc Chapter20Test-testNewtonFloat.smp +Chapter20 Chapter20Test.testNewtonFloat riscv SystemV RegAlloc Chapter20Test-testNewtonFloat.smp +Chapter20 Chapter20Test.testNewtonFloat arm SystemV RegAlloc Chapter20Test-testNewtonFloat.smp +Chapter20 Chapter20Test.testBrainfuck x86_64_v2 SystemV RegAlloc Chapter20Test-testBrainfuck.smp +Chapter20 Chapter20Test.testBrainfuck riscv SystemV RegAlloc Chapter20Test-testBrainfuck.smp +Chapter20 Chapter20Test.testBrainfuck arm SystemV RegAlloc Chapter20Test-testBrainfuck.smp +Chapter20 Chapter20Test.testAlloc0 x86_64_v2 SystemV RegAlloc Chapter20Test-testAlloc0.smp +Chapter20 Chapter20Test.testAlloc0 riscv SystemV RegAlloc Chapter20Test-testAlloc0.smp +Chapter20 Chapter20Test.testAlloc0 arm SystemV RegAlloc Chapter20Test-testAlloc0.smp +Chapter20 Chapter20Test.testAlloc2 x86_64_v2 SystemV RegAlloc Chapter20Test-testAlloc2.smp +Chapter20 Chapter20Test.testAlloc2 riscv SystemV RegAlloc Chapter20Test-testAlloc2.smp +Chapter20 Chapter20Test.testAlloc2 arm SystemV RegAlloc Chapter20Test-testAlloc2.smp +Chapter20 Chapter20Test.testArray1 x86_64_v2 SystemV RegAlloc Chapter20Test-testArray1.smp +Chapter20 Chapter20Test.testArray1 riscv SystemV RegAlloc Chapter20Test-testArray1.smp +Chapter20 Chapter20Test.testArray1 arm SystemV RegAlloc Chapter20Test-testArray1.smp +Chapter20 Chapter20Test.testBasic1 x86_64_v2 SystemV RegAlloc Chapter20Test-testBasic1.smp +Chapter20 Chapter20Test.testBasic1 riscv SystemV RegAlloc Chapter20Test-testBasic1.smp +Chapter20 Chapter20Test.testBasic1 arm SystemV RegAlloc Chapter20Test-testBasic1.smp +Chapter20 Chapter20Test.testFlags1 x86_64_v2 SystemV RegAlloc Chapter20Test-testFlags1.smp +Chapter20 Chapter20Test.testFlags1 riscv SystemV RegAlloc Chapter20Test-testFlags1.smp +Chapter20 Chapter20Test.testFlags1 arm SystemV RegAlloc Chapter20Test-testFlags1.smp +Chapter20 Chapter20Test.testFlags2 x86_64_v2 SystemV RegAlloc Chapter20Test-testFlags2.smp +Chapter20 Chapter20Test.testFlags2 riscv SystemV RegAlloc Chapter20Test-testFlags2.smp +Chapter20 Chapter20Test.testFlags2 arm SystemV RegAlloc Chapter20Test-testFlags2.smp +Chapter20 Chapter20Test.testFltArg x86_64_v2 SystemV RegAlloc Chapter20Test-testFltArg.smp +Chapter20 Chapter20Test.testFltArg riscv SystemV RegAlloc Chapter20Test-testFltArg.smp +Chapter20 Chapter20Test.testFltArg arm SystemV RegAlloc Chapter20Test-testFltArg.smp +Chapter20 Chapter20Test.testString x86_64_v2 SystemV RegAlloc Chapter20Test-testString.smp +Chapter20 Chapter20Test.testString riscv SystemV RegAlloc Chapter20Test-testString.smp +Chapter20 Chapter20Test.testString arm SystemV RegAlloc Chapter20Test-testString.smp +Chapter21 Chapter21Test.testFibExport x86_64_v2 win64 Export Chapter21Test-testFibExport.smp +Chapter21 Chapter21Test.testFibExport riscv SystemV Export Chapter21Test-testFibExport.smp +Chapter21 Chapter21Test.testFibExport arm SystemV Export Chapter21Test-testFibExport.smp +Chapter21 Chapter21Test.testAntiDeps1 x86_64_v2 SystemV Encoding Chapter21Test-testAntiDeps1.smp +Chapter21 Chapter21Test.testAntiDeps1 riscv SystemV Encoding Chapter21Test-testAntiDeps1.smp +Chapter21 Chapter21Test.testAntiDeps1 arm SystemV Encoding Chapter21Test-testAntiDeps1.smp +Chapter21 Chapter21Test.testLoop2 x86_64_v2 win64 Encoding Chapter21Test-testLoop2.smp +Chapter21 Chapter21Test.testLoop2 riscv SystemV Encoding Chapter21Test-testLoop2.smp +Chapter21 Chapter21Test.testLoop2 arm SystemV Encoding Chapter21Test-testLoop2.smp +Chapter21 Chapter21Test.testSieve x86_64_v2 win64 Export Chapter21Test-testSieve.smp +Chapter21 Chapter21Test.testSieve riscv SystemV Export Chapter21Test-testSieve.smp +Chapter21 Chapter21Test.testSieve arm SystemV Export Chapter21Test-testSieve.smp +Chapter21 Chapter21Test.testArgCount x86_64_v2 win64 Export Chapter21Test-testArgCount.smp +Chapter21 Chapter21Test.testArgCount riscv SystemV Export Chapter21Test-testArgCount-2.smp +Chapter21 Chapter21Test.testArgCount arm SystemV Export Chapter21Test-testArgCount-2.smp +Chapter21 Chapter21Test.testStringExport x86_64_v2 win64 Export Chapter21Test-testStringExport.smp +Chapter21 Chapter21Test.testInfinite x86_64_v2 SystemV Encoding Chapter21Test-testInfinite.smp +Chapter21 Chapter21Test.testInfinite riscv SystemV Encoding Chapter21Test-testInfinite.smp +Chapter21 Chapter21Test.testInfinite arm SystemV Encoding Chapter21Test-testInfinite.smp +Chapter21 Chapter21Test.testArray1 x86_64_v2 SystemV Encoding Chapter20Test-testArray1.smp +Chapter21 Chapter21Test.testArray1 riscv SystemV Encoding Chapter20Test-testArray1.smp +Chapter21 Chapter21Test.testArray1 arm SystemV Encoding Chapter20Test-testArray1.smp +Chapter21 Chapter21Test.testBasic1 x86_64_v2 SystemV Encoding Chapter20Test-testBasic1.smp +Chapter21 Chapter21Test.testBasic1 riscv SystemV Encoding Chapter20Test-testBasic1.smp +Chapter21 Chapter21Test.testBasic1 arm SystemV Encoding Chapter20Test-testBasic1.smp +Chapter21 Chapter21Test.testNewtonExport x86_64_v2 win64 Export Chapter21Test-testNewtonExport.smp +Chapter21 Chapter21Test.testNewtonExport riscv SystemV Export Chapter21Test-testNewtonExport.smp +Chapter21 Chapter21Test.testNewtonExport arm SystemV Export Chapter21Test-testNewtonExport.smp +Chapter21 Chapter21Test.testPerson x86_64_v2 win64 Export Chapter21Test-testPerson.smp +Chapter21 Chapter21Test.testPerson riscv SystemV Export Chapter21Test-testPerson.smp +Chapter21 Chapter21Test.testPerson arm SystemV Export Chapter21Test-testPerson.smp +Chapter21 Chapter21Test.testString x86_64_v2 SystemV Encoding Chapter21Test-testStringExport.smp +Chapter21 Chapter21Test.testString riscv SystemV Encoding Chapter21Test-testStringExport.smp +Chapter21 Chapter21Test.testString arm SystemV Encoding Chapter21Test-testStringExport.smp +Chapter21 Chapter21AllocTest.testNewtonInteger x86_64_v2 win64 Encoding Chapter20Test-testNewtonInteger.smp +Chapter21 Chapter21AllocTest.testNewtonInteger riscv SystemV Encoding Chapter20Test-testNewtonInteger.smp +Chapter21 Chapter21AllocTest.testNewtonInteger arm SystemV Encoding Chapter20Test-testNewtonInteger.smp +Chapter21 Chapter21AllocTest.testNewtonFloat x86_64_v2 SystemV Encoding Chapter21AllocTest-testNewtonFloat.smp +Chapter21 Chapter21AllocTest.testNewtonFloat riscv SystemV Encoding Chapter21AllocTest-testNewtonFloat.smp +Chapter21 Chapter21AllocTest.testNewtonFloat arm SystemV Encoding Chapter21AllocTest-testNewtonFloat.smp +Chapter21 Chapter21AllocTest.testArray1 x86_64_v2 SystemV Encoding Chapter21AllocTest-testArray1.smp +Chapter21 Chapter21AllocTest.testArray1 riscv SystemV Encoding Chapter21AllocTest-testArray1.smp +Chapter21 Chapter21AllocTest.testArray1 arm SystemV Encoding Chapter21AllocTest-testArray1.smp +Chapter21 Chapter21AllocTest.testString x86_64_v2 SystemV Encoding Chapter21AllocTest-testString.smp +Chapter21 Chapter21AllocTest.testString riscv SystemV Encoding Chapter21AllocTest-testString.smp +Chapter21 Chapter21AllocTest.testString arm SystemV Encoding Chapter21AllocTest-testString.smp +Chapter22 Chapter22Test.testSextFail x86_64_v2 win64 Encoding Chapter22Test-testSextFail.smp +Chapter22 Chapter22Test.testSextFail riscv SystemV Encoding Chapter22Test-testSextFail.smp +Chapter22 Chapter22Test.testSextFail arm SystemV Encoding Chapter22Test-testSextFail.smp +Chapter22 Chapter22Test.testSextFail riscv SystemV Export Chapter22Test-testSextFail.smp +Chapter22 Chapter22Test.testSextFail arm SystemV Export Chapter22Test-testSextFail.smp +Chapter22 Chapter22Test.testSextSuccess riscv SystemV Export Chapter22Test-testSextSuccess.smp +Chapter22 Chapter22Test.testSextSuccess arm SystemV Export Chapter22Test-testSextSuccess.smp +Chapter22 Chapter22Test.testSextSuccess x86_64_v2 win64 Encoding Chapter22Test-testSextSuccess.smp +Chapter22 Chapter22Test.testSextSuccess riscv SystemV Encoding Chapter22Test-testSextSuccess.smp +Chapter22 Chapter22Test.testSextSuccess arm SystemV Encoding Chapter22Test-testSextSuccess.smp +Chapter22 Chapter22Test.testInfiniteReturn x86_64_v2 SystemV Encoding Chapter22Test-testInfiniteReturn.smp +Chapter22 Chapter22Test.testInfiniteReturn riscv SystemV Encoding Chapter22Test-testInfiniteReturn.smp +Chapter22 Chapter22Test.testInfiniteReturn arm SystemV Encoding Chapter22Test-testInfiniteReturn.smp +Chapter22 Chapter22Test.testSextFail2 riscv SystemV Export Chapter22Test-testSextFail2.smp +Chapter22 Chapter22Test.testSextFail2 arm SystemV Export Chapter22Test-testSextFail2.smp +Chapter22 Chapter22Test.testSextFail2 x86_64_v2 win64 Encoding Chapter22Test-testSextFail2.smp +Chapter22 Chapter22Test.testSextFail2 riscv SystemV Encoding Chapter22Test-testSextFail2.smp +Chapter22 Chapter22Test.testSextFail2 arm SystemV Encoding Chapter22Test-testSextFail2.smp +Chapter22 Chapter22Test.testPerson x86_64_v2 win64 Export Chapter22Test-testPerson.smp +Chapter22 Chapter22Test.testPerson riscv SystemV Export Chapter22Test-testPerson.smp +Chapter22 Chapter22Test.testPerson arm SystemV Export Chapter22Test-testPerson.smp +Chapter22 Chapter22Test.testHelloWorld x86_64_v2 win64 Export Chapter22Test-testHelloWorld.smp +Chapter22 Chapter22Test.testHelloWorld riscv SystemV Export Chapter22Test-testHelloWorld.smp +Chapter22 Chapter22Test.testHelloWorld arm SystemV Export Chapter22Test-testHelloWorld.smp +Chapter23 Chapter23Test.testAnd x86_64_v2 win64 Export Chapter23Test-testAnd.smp +Chapter23 Chapter23Test.testAnd riscv SystemV Export Chapter23Test-testAnd.smp +Chapter23 Chapter23Test.testAnd arm SystemV Export Chapter23Test-testAnd.smp +Chapter23 Chapter23Test.testOr x86_64_v2 win64 Export Chapter23Test-testOr.smp +Chapter23 Chapter23Test.testOr riscv SystemV Export Chapter23Test-testOr.smp +Chapter23 Chapter23Test.testOr arm SystemV Export Chapter23Test-testOr.smp +Chapter23 Chapter23Test.testCondSideEffAnd x86_64_v2 win64 Export Chapter23Test-testCondSideEffAnd.smp +Chapter23 Chapter23Test.testCondSideEffAnd riscv SystemV Export Chapter23Test-testCondSideEffAnd.smp +Chapter23 Chapter23Test.testCondSideEffAnd arm SystemV Export Chapter23Test-testCondSideEffAnd.smp +Chapter23 Chapter23Test.testFRefFields x86_64_v2 win64 Encoding Chapter23Test-testFRefFields.smp +Chapter23 Chapter23Test.testAndPtr riscv SystemV Export Chapter23Test-testAndPtr.smp +Chapter23 Chapter23Test.testAndPtr arm SystemV Export Chapter23Test-testAndPtr.smp +Chapter23 Chapter23Test.testOrRhsLoopLocal riscv SystemV Export Chapter23Test-testOrRhsLoopLocal.smp +Chapter23 Chapter23Test.testMethod x86_64_v2 win64 Encoding Chapter23Test-testMethod.smp +Chapter23 Chapter23Test.testOrRhsLoopMemory riscv SystemV Export Chapter23Test-testOrRhsLoopMemory.smp +Chapter23 Chapter23Test.testCondSideEffOr x86_64_v2 win64 Export Chapter23Test-testCondSideEffOr.smp +Chapter23 Chapter23Test.testCondSideEffOr riscv SystemV Export Chapter23Test-testCondSideEffOr.smp +Chapter23 Chapter23Test.testCondSideEffOr arm SystemV Export Chapter23Test-testCondSideEffOr.smp +Chapter23 Chapter23Test.testFuncCall x86_64_v2 win64 Export Chapter23Test-testFuncCall.smp +Chapter23 Chapter23Test.testFuncCall riscv SystemV Export Chapter23Test-testFuncCall.smp +Chapter23 Chapter23Test.testFuncCall arm SystemV Export Chapter23Test-testFuncCall.smp +Chapter23 Chapter23AllocTest.testString x86_64_v2 SystemV Encoding Chapter23AllocTest-testString.smp +Chapter23 Chapter23AllocTest.testString riscv SystemV Encoding Chapter23AllocTest-testString.smp +Chapter23 Chapter23AllocTest.testString arm SystemV Encoding Chapter23AllocTest-testString.smp +Chapter24 Chapter24Test.testAnd x86_64_v2 win64 Export Chapter23Test-testAnd.smp +Chapter24 Chapter24Test.testAnd riscv SystemV Export Chapter23Test-testAnd.smp +Chapter24 Chapter24Test.testAnd arm SystemV Export Chapter23Test-testAnd.smp +Chapter24 Chapter24Test.testOr x86_64_v2 win64 Export Chapter23Test-testOr.smp +Chapter24 Chapter24Test.testOr riscv SystemV Export Chapter23Test-testOr.smp +Chapter24 Chapter24Test.testOr arm SystemV Export Chapter23Test-testOr.smp +Chapter24 Chapter24Test.testCondSideEffAnd x86_64_v2 win64 Export Chapter23Test-testCondSideEffAnd.smp +Chapter24 Chapter24Test.testCondSideEffAnd riscv SystemV Export Chapter23Test-testCondSideEffAnd.smp +Chapter24 Chapter24Test.testCondSideEffAnd arm SystemV Export Chapter23Test-testCondSideEffAnd.smp +Chapter24 Chapter24Test.testFRefFields x86_64_v2 win64 Encoding Chapter23Test-testFRefFields.smp +Chapter24 Chapter24Test.testAndPtr riscv SystemV Export Chapter23Test-testAndPtr.smp +Chapter24 Chapter24Test.testAndPtr arm SystemV Export Chapter23Test-testAndPtr.smp +Chapter24 Chapter24Test.testMethod x86_64_v2 win64 Encoding Chapter23Test-testMethod.smp +Chapter24 Chapter24Test.testStack1 x86_64_v2 win64 Export Chapter24Test-testStack1.smp +Chapter24 Chapter24Test.testStack1 riscv SystemV Export Chapter24Test-testStack1.smp +Chapter24 Chapter24Test.testStack1 arm SystemV Export Chapter24Test-testStack1.smp +Chapter24 Chapter24Test.testStack2 x86_64_v2 win64 Export Chapter24Test-testStack2.smp +Chapter24 Chapter24Test.testStack2 riscv SystemV Export Chapter24Test-testStack2.smp +Chapter24 Chapter24Test.testStack2 arm SystemV Export Chapter24Test-testStack2.smp +Chapter24 Chapter24Test.testStack3 x86_64_v2 win64 Export Chapter24Test-testStack3.smp +Chapter24 Chapter24Test.testStack3 riscv SystemV Export Chapter24Test-testStack3.smp +Chapter24 Chapter24Test.testStack3 arm SystemV Export Chapter24Test-testStack3.smp +Chapter24 Chapter24Test.testStack4 x86_64_v2 win64 Export Chapter24Test-testStack4.smp +Chapter24 Chapter24Test.testStack4 riscv SystemV Export Chapter24Test-testStack4.smp +Chapter24 Chapter24Test.testStack4 arm SystemV Export Chapter24Test-testStack4.smp +Chapter24 Chapter24Test.testStack5 x86_64_v2 win64 Export Chapter24Test-testStack5.smp +Chapter24 Chapter24Test.testStack5 riscv SystemV Export Chapter24Test-testStack5.smp +Chapter24 Chapter24Test.testStack5 arm SystemV Export Chapter24Test-testStack5.smp +Chapter24 Chapter24Test.testStack9 x86_64_v2 win64 Export Chapter24Test-testStack9.smp +Chapter24 Chapter24Test.testStack9 riscv SystemV Export Chapter24Test-testStack9-2.smp +Chapter24 Chapter24Test.testStack9 arm SystemV Export Chapter24Test-testStack9-2.smp +Chapter24 Chapter24Test.testCondSideEffOr x86_64_v2 win64 Export Chapter23Test-testCondSideEffOr.smp +Chapter24 Chapter24Test.testCondSideEffOr riscv SystemV Export Chapter23Test-testCondSideEffOr.smp +Chapter24 Chapter24Test.testCondSideEffOr arm SystemV Export Chapter23Test-testCondSideEffOr.smp +Chapter24 Chapter24Test.testStack10 x86_64_v2 win64 Export Chapter24Test-testStack10.smp +Chapter24 Chapter24Test.testStack10 riscv SystemV Export Chapter24Test-testStack10-2.smp +Chapter24 Chapter24Test.testStack10 arm SystemV Export Chapter24Test-testStack10-2.smp +Chapter24 Chapter24Test.testStack11 x86_64_v2 win64 Export Chapter24Test-testStack11.smp +Chapter24 Chapter24Test.testStack11 riscv SystemV Export Chapter24Test-testStack11-2.smp +Chapter24 Chapter24Test.testStack11 arm SystemV Export Chapter24Test-testStack11-2.smp +Chapter24 Chapter24Test.testStack12 x86_64_v2 win64 Export Chapter24Test-testStack12.smp +Chapter24 Chapter24Test.testStack12 riscv SystemV Export Chapter24Test-testStack12-2.smp +Chapter24 Chapter24Test.testStack12 arm SystemV Export Chapter24Test-testStack12-2.smp +Chapter24 Chapter24Test.testStack13 x86_64_v2 win64 Export Chapter24Test-testStack13.smp +Chapter24 Chapter24Test.testStack13 riscv SystemV Export Chapter24Test-testStack13-2.smp +Chapter24 Chapter24Test.testStack13 arm SystemV Export Chapter24Test-testStack13-2.smp +Chapter24 Chapter24Test.testFuncCall x86_64_v2 win64 Export Chapter23Test-testFuncCall.smp +Chapter24 Chapter24Test.testFuncCall riscv SystemV Export Chapter23Test-testFuncCall.smp +Chapter24 Chapter24Test.testFuncCall arm SystemV Export Chapter23Test-testFuncCall.smp +Chapter24 Chapter24AllocTest.testNewtonInteger x86_64_v2 win64 Encoding Chapter24AllocTest-testNewtonInteger.smp +Chapter24 Chapter24AllocTest.testNewtonInteger riscv SystemV Encoding Chapter24AllocTest-testNewtonInteger.smp +Chapter24 Chapter24AllocTest.testNewtonInteger arm SystemV Encoding Chapter24AllocTest-testNewtonInteger.smp +Chapter24 Chapter24AllocTest.testStackArguments x86_64_v2 win64 Export Chapter24AllocTest-testStackArguments.smp +Chapter24 Chapter24AllocTest.testStackArguments riscv SystemV Export Chapter24AllocTest-testStackArguments.smp +Chapter24 Chapter24AllocTest.testStackArguments arm SystemV Export Chapter24AllocTest-testStackArguments.smp +Chapter24 Chapter24AllocTest.testBubbleSort x86_64_v2 win64 Encoding Chapter24AllocTest-testBubbleSort.smp +Chapter24 Chapter24AllocTest.testBubbleSort riscv SystemV Encoding Chapter24AllocTest-testBubbleSort.smp +Chapter24 Chapter24AllocTest.testBubbleSort arm SystemV Encoding Chapter24AllocTest-testBubbleSort.smp +Chapter21 BrainFuckTest.testBrainFuck x86_64_v2 win64 Export BrainFuckTest-testBrainFuck.smp +Chapter21 BrainFuckTest.testBrainFuck riscv SystemV Export BrainFuckTest-testBrainFuck.smp +Chapter21 BrainFuckTest.testBrainFuck arm SystemV Export BrainFuckTest-testBrainFuck.smp +Chapter21 MergeSortTest.testMergeSort0 x86_64_v2 win64 Export MergeSortTest-testMergeSort0.smp +Chapter21 MergeSortTest.testMergeSort0 riscv SystemV Export MergeSortTest-testMergeSort0.smp +Chapter21 MergeSortTest.testMergeSort0 arm SystemV Encoding MergeSortTest-testMergeSort0.smp +Chapter22 Chapter22Test.testNarrowCReturns x86_64_v2 SystemV Export Chapter22Test-testNarrowCReturns.smp +Chapter22 Chapter22Test.testNarrowCReturns x86_64_v2 win64 Export Chapter22Test-testNarrowCReturns.smp diff --git a/src/test/java/com/seaofnodes/simple/test0/A.smp b/src/test/java/com/seaofnodes/simple/test0/A.smp new file mode 100644 index 00000000..b94fa147 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test0/A.smp @@ -0,0 +1,6 @@ +// -*- mode: java; -*- + +val _fcn = { -> arg ? { int x -> x+1; } : { int x -> x-1; }; }; + +// Refer to child B is OK because last field of A has been initialized +_fcn()(B.x); diff --git a/src/test/java/com/seaofnodes/simple/test0/A/B.smp b/src/test/java/com/seaofnodes/simple/test0/A/B.smp new file mode 100644 index 00000000..82844f74 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test0/A/B.smp @@ -0,0 +1 @@ +val x=5; diff --git a/src/test/java/com/seaofnodes/simple/test5/par.smp b/src/test/java/com/seaofnodes/simple/test5/par.smp new file mode 100644 index 00000000..f19bd57b --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test5/par.smp @@ -0,0 +1,20 @@ +// -*- mode: java; -*- + +// Parent module +val par0 = 3; // Some final field + +// Nested class in parent. +// Makes a field: "val child0 = ..." +struct child0 { + val kid0 = 5; +}; + +// Nested class in parent, comes from disk. +// These are defined in-order +val child1 = child1; + +// Nested class in parent, comes from disk +val child2 = child2; + +// Nested class in parent, comes from disk +val child3 = child3; diff --git a/src/test/java/com/seaofnodes/simple/test5/par/child1.smp b/src/test/java/com/seaofnodes/simple/test5/par/child1.smp new file mode 100644 index 00000000..7bca0cf7 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test5/par/child1.smp @@ -0,0 +1,12 @@ +// -*- mode: java; -*- + +// Child module + +// Same field name as child2 +val kid1 = 7; + +// Can read parent directly +val kid2 = par0; + +// Error to reference a later sibling +// val kid3 = par.child2.kid1; diff --git a/src/test/java/com/seaofnodes/simple/test5/par/child2.smp b/src/test/java/com/seaofnodes/simple/test5/par/child2.smp new file mode 100644 index 00000000..ab5e2ac1 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test5/par/child2.smp @@ -0,0 +1,13 @@ +// -*- mode: java; -*- + +// Child module + +// Same field name as child2 +val kid1 = 11; + +// Can read parent directly. +// Can read defined-in-parent fields. +val kid2 = child0.kid0; + +// OK to reference a past sibling +val kid3 = child1.kid1; diff --git a/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m.smp b/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m.smp new file mode 100644 index 00000000..569f38be --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m.smp @@ -0,0 +1,2 @@ +val a = a; +val b = b; diff --git a/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/a.smp b/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/a.smp new file mode 100644 index 00000000..97113b71 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/a.smp @@ -0,0 +1 @@ +val f = { -> new b(1); }; diff --git a/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/b.smp b/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/b.smp new file mode 100644 index 00000000..43770b60 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test_smp/forward_ctor/m/b.smp @@ -0,0 +1,4 @@ +struct b { + int x; + new b = { int y -> x = y; }; +}; diff --git a/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys.smp b/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys.smp new file mode 100644 index 00000000..1725c57d --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys.smp @@ -0,0 +1,3 @@ +// Reduced multi-compilation-unit reproducer for order-sensitive inlining. +var aryu8 = aryu8; +var aryi64 = aryi64; diff --git a/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/ary.smp b/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/ary.smp new file mode 100644 index 00000000..e3dd6140 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/ary.smp @@ -0,0 +1,6 @@ +// Array utilities, discovered indirectly from aryu8. +val copy = { u8[] dst, int doff, u8[~] src, int soff, int len -> + for( int i=0; i<len; i++ ) + dst[i+doff] = src[i+soff]; + return dst; +}; diff --git a/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryi64.smp b/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryi64.smp new file mode 100644 index 00000000..53c93666 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryi64.smp @@ -0,0 +1,29 @@ +struct aryi64 { + u32 !len = 0; + i64[] !buf; + + new aryi64 = { i64[] b -> buf = b; }; + + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + i64[] !buf2 = new i64[newlen]; + for( int i=0; i<buf#; i++ ) + buf2[i] = buf[i]; + buf = buf2; + self; + }; + + val add = { i64 c -> + _grow(1).buf[len++] = c; + self; + }; + + val str = { -> + var str = new aryu8(new u8[1]).add('['); + for( int i=0; i<len; i++ ) + str.num(buf[i]).add(',').add(' '); + str.undo(2).add(']'); + }; +}; diff --git a/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryu8.smp b/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryu8.smp new file mode 100644 index 00000000..e6b708a4 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/test_smp/seed_repro/sys/aryu8.smp @@ -0,0 +1,50 @@ +struct aryu8 { + u32 !len = 0; + u8[] !buf; + + new aryu8 = { u8[] b -> buf = b; }; + + val _grow = { int len2 -> + if( len+len2 < buf# ) return self; + int newlen = buf# * 2; + while( len+len2 >= newlen ) newlen *= 2; + u8[] !buf2 = new u8[newlen]; + ary.copy(buf2,0,buf,0,buf#); + buf = buf2; + self; + }; + + val clear = { -> len = 0; self; }; + + val add = { u8 c -> + _grow(1).buf[len++] = c; + self; + }; + + val addAry = { u8[~] xs -> + _grow(xs#); + ary.copy(buf,len,xs,0,xs#); + len += xs#; + self; + }; + + val undo = { int x -> + len = len >= x ? len-x : 0; + self; + }; + + 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; + } + self; + }; +}; diff --git a/src/test/smp/lazy/App.smp b/src/test/smp/lazy/App.smp new file mode 100644 index 00000000..caf7b0c1 --- /dev/null +++ b/src/test/smp/lazy/App.smp @@ -0,0 +1,3 @@ +val Main=Main; +val Tables=Tables; +val Stats=Stats; diff --git a/src/test/smp/lazy/App/Main.smp b/src/test/smp/lazy/App/Main.smp new file mode 100644 index 00000000..2444f21b --- /dev/null +++ b/src/test/smp/lazy/App/Main.smp @@ -0,0 +1 @@ +return arg ? Tables.square(3)+Tables.square(4)+100*Stats.builds : Stats.builds; diff --git a/src/test/smp/lazy/App/Stats.smp b/src/test/smp/lazy/App/Stats.smp new file mode 100644 index 00000000..e2d9e02e --- /dev/null +++ b/src/test/smp/lazy/App/Stats.smp @@ -0,0 +1 @@ +int builds=0; diff --git a/src/test/smp/lazy/App/Tables.smp b/src/test/smp/lazy/App/Tables.smp new file mode 100644 index 00000000..0f32a118 --- /dev/null +++ b/src/test/smp/lazy/App/Tables.smp @@ -0,0 +1,4 @@ +Stats.builds++; +int[] _squares=new int[256]; +for(int i=0; i<256; i++) _squares[i]=i*i; +val square={u8 n -> _squares[n];}; |
