diff options
51 files changed, 839 insertions, 433 deletions
diff --git a/chapter28b/README.ja.md b/chapter28b/README.ja.md index 4feaee93..d04e9ccb 100644 --- a/chapter28b/README.ja.md +++ b/chapter28b/README.ja.md @@ -1,110 +1,108 @@ -# 第25章: モジュール、分割コンパイル、SSA 構築 +# 第28b章: 分離コンパイル [English](README.md) | 日本語 -この文書は [原文](README.md) の日本語訳です。 +[Previous: Chapter 28a](../chapter28a/README.ja.md) -[前の章: 第24章](../chapter24/README.ja.md) | -[章の一覧](../README.ja.md#各章) +前の章では、見つかったソースを一度にコンパイルしました。この章ではライブラリと +クライアントを別々にコンパイルします。クライアントにはライブラリのオブジェクト +ファイルが必要ですが、そのソースは不要です。 -この章では Simple のソースファイルを再利用可能なオブジェクトファイルへコンパイルし、その型と理想 IR を後のコンパイルに読み込み、別に構築したシステムライブラリとネイティブプログラムをリンクします。また、不完全な情報を扱うため、パーサー、型、メモリ SSA を大幅に再設計します。 +## オブジェクトに理想 IR を保存する -この番号付きスナップショットは比較用に残しています。章の分割は進行中で、型未確定時の SSA 構築、オブジェクトの構築、エスケープ解析、モジュールを名前付きの章に分けました。[分割計画](../docs/chapter-split.md) と [バックポート待ち一覧](../docs/chapter-backports.md) を参照してください。 +通常の ELF オブジェクトには機械語、データ、シンボル、再配置情報があります。 +Simple はさらに `.simple` セクションへ型と理想 IR を保存します。クライアントは +宣言を読み込んで型を検査し、関数本体も読み込んで SCCP やインライン化に使います。 +残った呼び出しは、ネイティブリンクでライブラリの機械語につなぎます。 -第21〜24章のメモリ処理はここにも引き継ぎます。Load 検索は読み取りをループ外へ移す前にマージの全枝を証明し、Phi の因数分解はメモリ破壊を確認し、スケジューリングはメモリ集合体を通じてエイリアスをたどります。検索は `EscapeNode` から非公開または公開メモリへ進み、未解決の Store エイリアスを待ちます。配列長の読み取りには要素の境界検査がなく、配列要素の読み取りは制御を保持します。整数値の Phi は、既存の型付き変換機構で、ループの拡大を通じてもロード幅を保持します。New は引き続き非公開のコンストラクタメモリを生成し、公開は Escape が扱います。以前の章の割り当て入力設計は、これを置き換えません。 +宣言型と解析中の型は別々に保存します。ある時点でフィールドの値が 0 でも、 +その宣言を定数 0 に変えてはいけません。引数の型の上限、数値演算のモード、 +Store の幅、元のソース位置も保存します。入れ子の構造体には、関数本体だけでなく +割り当て関数とコンストラクタの宣言も必要です。 -[直線的な履歴](https://github.com/SeaOfNodes/Simple/tree/linear) で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter25) を読み、[第24章と比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter24...linear-chapter25) することもできます。 +別々のコンパイルで同じ番号になったエイリアスや関数は、同じものとは限りません。 +`GlobalBits` は定義元のファイルとファイル内の番号を保存し、インポート時にその +組み合わせを今回の番号へ写します。型とノードに同じ写像を使い、複数ファイルを +またぐ再帰型も復元します。 -この章の[完全な言語文法](docs/25-grammar.ja.md)はこちらです。 +読み込みの単位は現在オブジェクトです。無関係なオブジェクトは読み込まずに済みますが、選ばれた +オブジェクトの保存済み IR は全体を復元し、記録された依存先も読み込みます。`sys.io.p` だけを +呼ぶ場合でも、`sys.o` にまとめられた十個の単位をすべて読み込み、`aryu8` と `aryi64` も含みます。 +必要なメンバーだけの読み込みは今後の目標です。IR の読み込みだけでクラスが初期化されたり、 +その本体がクライアントの出力にコピーされたりするわけではありません。元のソースの解析は省けますが、 +インポートしたグラフも新しい呼び出し元とともに最適化し、型検査します。 -## コンパイル単位と名前 +## 実行できる例 -`CompUnit` はソースまたはオブジェクトファイルを表します。`ParseAll` は依存関係を発見し、必要なソースを解析し、以前コンパイルした単位を読み込みます。ソースツリーとビルドツリーは対応する相対パスを使い、クラス名はドット区切りのパスを使います。たとえば `a/b/foo.smp` は名前空間 `a.b.foo` を供給します。 +英語版の例は次の二つのソースルートにあります。 -各単位は Start/Stop ノードを持ちます。関数がコンパイル単位に所有されることと、未知の外部呼び出し側を表す辺とは別です。出力のためにコードを保持することが、任意の呼び出し側から到達できることを意味してはいけません。 - -ドライバは入力パスからプロジェクトルートを推定し、出力の `-o` と外部 Simple オブジェクト検索パスの `-L` を受け付けます。検索パスにはオブジェクトファイルやディレクトリを指定できます。ヘルプには `--root` もありますが、この番号付きスナップショットでは未実装です。名前付きのモジュール章では使用できます。コンパイラはネイティブリンク用の外部 C シンボルも解決します。`_` で始まる名前と外側の名前は、未知の呼び出し元に公開するインターフェースを決めます。 - -明示的な数値型の C 宣言は、可変のネイティブ記憶領域へ直接束縛します。 - -```java -i32 counter = "C"; -counter += 1; -return counter; +```text +src/test/smp/separate/library/Counters.smp +src/test/smp/separate/client/Main.smp ``` -ネイティブリンカが `counter` を解決します。読み書きはその C 変数にアクセスし、宣言はコピーも初期化もしません。繰り返した宣言とモジュールインポートは同じ記憶領域エイリアスを共有します。整数と浮動小数点データの束縛をサポートし、ポインタと集合体のデータ束縛はまだサポートしません。外部フィールドは記憶領域フラグを持ち、宣言元クラス内でバイトを占めません。配置はフィールドオフセットを `ExternOffset` シンボルへ解決し、命令選択はクラス先頭+オフセットをネイティブシンボルアドレスへ置き換えます。ネイティブオブジェクト出力は既存の AMD64 ELF ライタを使い、ARM と RISC-V はメモリ内リンカを通じてこれらのアドレスをサポートします。スレッドローカル変数と C マクロにはアクセサ関数が必要です。システムライブラリは小さなネイティブランタイムアクセサを通じ、`libc.errno()` を公開します。 - -## オブジェクトファイルは理想 IR を含む - -ELF ライタは、ネイティブコードと、正規化された型、理想グラフノード、依存関係を含む `.simple` セクションを保存します。リーダはそのグラフを再構築し、インポートした関数をインライン化などの最適化に参加させます。インライン化せず残った呼び出しは、ネイティブリンクで解決します。 - -`GlobalBits` はファイルの同一性とファイルごとのインデックスを、エイリアス、関数対象、戻り位置の局所的な密なインデックスへ対応付けます。独立したコンパイルの等しい局所番号を、等しい同一性と取り違えてはいけません。読み込み後に型を閉じて更新し、単位をまたぐ循環参照も扱います。 - -シリアライズは、宣言型を精密化されたフロー型とは別に保持します。意味を持つすべてのノード/型のフィールドには、対応する読み書きサポートが必要です。ソースにある高コストなシリアライズの全単射検査は現在無効で、より大きな増分再ビルドテスト `testModule0` は無視されています。どちらも有効な検証保証ではありません。 - -外部フィールドのフラグと記号的オフセットは、シリアライズされた配置を変更します。マジックは `C0DE` のままで、独立した形式バージョンはまだありません。このコンパイラへインポートする前に、すべての Simple オブジェクトファイルを再ビルドしてください。 - -## 型が完成する前の SSA 構築 +ライブラリは、ループで final フィールドを計算する `Counter` と共有カウンタを +公開します。クライアントの `run(3)` は最初に 107 を返します。同じプロセスで +続けて `run(5)` を呼ぶと 231 になります。インスタンスは別々ですが、クラスの +カウンタは共有されます。`run(0)` はライブラリを使わずに戻ります。 -Simple は引き続き AST を介さず直接 SSA に解析します。前方参照により、解析時点では式の最終的な型が利用できない場合があります。そのため構文と解決済みの字句的束縛でトポロジーを選び、後から型でグラフを精密化、最適化、検証します。 +このディレクトリでライブラリをコンパイルします。 -- 算術ノードは整数モードか浮動小数点モードかの選択を延期し、SCCP 後の再帰的グラフ成分を通じた解決も扱います。 -- Call は呼び出し先の種類が決まる前に、構文上のレシーバスロットを保持します。記号的なフィールド情報は配置依存の判断を延期します。 -- Load と通常の Phi は、パーサーが供給した下限型を持たなくなります。 -- `TypeScalar` は整数、浮動小数点、メモリポインタ、関数ポインタ値を、大域的な制御/メモリのボトムと区別して表します。 -- `TypeStruct._open` は不完全なフィールド集合を記述し、`_fref` は権威ある定義の欠如を記録します。フィールドを発見したことは、型を定義したことではありません。 -- ガードは入力の種類がわかる前に、分岐が証明したゼロ/非ゼロの事実を運びます。`FunPtrNode` は関数の戻りグラフへの明示的な辺を保持します。 - -ドライバはシリアライズとマシン選択の前に、解析、悲観的反復、SCCP、最終型検査を行います。単純でないインライン化は、常に即時のピープホール最適化として行うのではなく、反復と調整します。 - -## メモリと構築 - -パーサーは通常のバルクメモリを保持します。最適化器は `MemMerge`、`MemPhi`、`BulkMemPhi` で正確なエイリアスを復元します。バルク Phi は、並列の正確な Phi で表す除外分以外のすべてのエイリアスを覆います。各スライスはちょうど1回だけ覆われなければなりません。 - -ユーザーコンストラクタは次のように宣言し、呼び出します。 - -```java -struct Box { - i64[] values; - new Box = { i64[] initial -> values = initial; }; -}; -return new Box(new i64[1]); +```sh +make lib +make build/classes/test/.ttag +java -ea -cp "build/classes/main:build/classes/test:lib/*" \ + com.seaofnodes.simple.Simple --norun -o build/separate-example/lib/ \ + src/test/smp/separate/library/Counters.smp ``` -宣言で割り当て補助関数を生成します。これは割り当てを行い、隠れた `<init>` とユーザーコンストラクタを順に呼び、`EscapeNode` を通じて初期化済みの非公開メモリを公開します。パーサーの `Var` メタデータは、早すぎるフィールド読み取りやコンストラクタ出口での未初期化を検出します。Store の幅は保存する値ではなく、対象の宣言から得ます。 +次にオブジェクトを使ってクライアントを評価します。 -ファイルレベルのコードは、インスタンス初期化とは異なるクラス初期化子(`<clinit>`)で表します。遅延初期化と循環検査については[英語版の説明](README.md#lazy-class-initialization)を参照してください。今後の計画は[英語版ロードマップ](ROADMAP.md#class-initialization-and-compiler-follow-ups)にあります。 - -## システムライブラリと例 - -以前の単一の `sys.smp` ライブラリは、libc 束縛、I/O、文字、スキャナ、配列、伸長可能なバッファ、ビット集合の単位に整理されました。ネイティブドライバは実際のプログラム引数をサポートします。`docs/examples` の例には Bubble Sort、Capitalize、Dijkstra、FileIO があります。 +```sh +java -ea -cp "build/classes/main:build/classes/test:lib/*" \ + com.seaofnodes.simple.Simple --eval \ + -L build/separate-example/lib/Counters.o \ + src/test/smp/separate/client/Main.smp 3 +``` -`Chapter25Test` は、意図的にインライン化せず残す Hello World 呼び出しを含め、ライブラリ読み込みとネイティブリンクを検証します。そのテストは `sys.o` とリンクする前に、クライアントの外部シンボルを確認します。ほかのテストは前方コンストラクタ、呼び出し経由のフィールド更新、リテラルのエスケープ、縮小したコンパイラ失敗を検証します。独立した回帰テストは以前の章へ移す候補です。 +結果は `107` です。Windows のクラスパス区切りには `;` を使います。クライアントの +ソースルートには `Counters.smp` がありません。`SeparateCompilationTest` はさらに +二つのオブジェクトを C ドライバとリンクし、繰り返し呼び出す動作を検査します。 -## ビルドと確認 +## 解析とコードの所有者 -このディレクトリで `make lib` は Java テスト依存関係を用意し、`make tests` は章の生のスイート、独立スイート、システムテスト、ファザー回帰テストを実行します。`make release` はコンパイラ jar とネイティブライブラリ成果物を構築し、`make tags` はエディタ用タグを構築します。ネイティブツールと選択した CPU/ABI は環境と一致する必要があります。Makefile は現在、既定で x86-64/win64 を使います。 +インポートした本体は解析に参加しても、クライアントの出力には属しません。 +`Serialize` は `Lower` がクラス初期化を展開する前に理想 IR を保存します。 +`Unlink` は機械語生成に向けてインポートした本体と解析用の呼び出し辺を切り離します。 +インライン化でクライアントへコピーされたコードは、クライアント自身の本体として +出力されます。 -シリアライズ IR は `C0DE` ヘッダを使います。ポインタ型は独立した読み取り専用アクセスビットを含みます。形式にはまだバージョン番号がないため、形式を変更したらオブジェクトファイルを再ビルドしてください。現在、後方互換性は保証していません。 +初期化規則は前の章と同じです。型のインポートだけでは初期化しません。能動的な使用は +`CLInit` を通して初期化を要求し、ライブラリが持つクラスオブジェクトとフラグを使います。 +依存関係の検査にはインポートした本体も含めます。ネイティブコードへ渡した関数の +未知の呼び出し元も保持します。 -コンパイラ変更は `sys.o` のシリアライズ IR とネイティブコードの両方を無効にする場合があります。リンクしたプログラムのテスト結果を解釈する前に再ビルドしてください。ソースだけの部分集合は、この章の完全な `make tests` と同等ではありません。 +コンストラクタのフィールド更新はローカル値と Phi のままで、ヘルパーの出口に Store を +置きます。未完成のオブジェクトでも初期化済みフィールドは読めます。 +アンダースコアは公開インターフェースの選択に使いますが、ソースのアクセス制限は +[レビュー待ち](../docs/chapter-backports.md)です。 -## RegAlloc の改良: 生存区間の面積と分割コスト +## 検証と今後 -第24章はループ Phi の高頻度辺を分割する前に、低頻度のコピーを試しました。この章ではさらに、スピルがスケジュール済みコードのどれだけをレジスタ圧力から解放するかを推定し、新しいコピーの推定コストで割ります。callee-save 区間は関数全体を覆います。複製可能な定数は使用の近くで再構築できます。ループ内のコピーは高コストなので、`8^loopDepth` で重み付けします。特に多数の使用を持つ値については、低コストな近似です。 +リポジトリのルートから次を実行します。 -複数の固定レジスタ使用を直接分割する規則や、一部の複数定義区間をより深く分割する規則など、第25章のほかの衝突処理はここに残します。サイズのしきい値はヒューリスティックであり、最初の割り当て器へバックポートすべき一般的不変条件ではありません。共有される合法性修正は、引き続き最も早い該当章から始めます。最終監査は第20〜25章の固定隣接ノードの色バイアス、第20〜21章の狭いストアマスク、第21〜25章の ARM レジスタバンク/浮動小数点メモリエンコーディングを修正しました。 +```sh +make tests CHAPTERS=chapter28b +``` -第21章の隣接コピー転送も引き継ぎます。次の命令だけで使うコピーは、オペランドがコピー元レジスタを受け入れ、命令の結果に結び付けられていなければ除去します。 +保存したコンストラクタ、インポートをまたぐ初期化循環、共有データ、ネイティブ関数の +サンク、ライブラリとクライアントの実行を検査します。Make はテスト用の `sys` も +オブジェクトにします。`make release` は jar と標準ライブラリを作り、`make view` は +共通のグラフビューアーを起動します。 -現在の比較は、固定したテスト群を行、コンパイラの章を列にした -[英語版の測定表](README.md#regalloc-improvements-live-range-area-and-split-cost) を参照してください。 -各セルは ARM/RISC-V の実行命令数、そのうち割り当て器が生成したコピーなどの命令数、 -x86 のループ重み付き移動数を示します。これらは異なる指標です。 -対象、入力、除外項目は [測定方法](../docs/regalloc-spills.md) にあります。 -古い測定群の集計値と混ぜて比較しないでください。 +保存形式は `C0DE` 以外のバージョン番号をまだ持ちません。コンパイラや形式を変えたら +オブジェクトを再構築してください。高コストの往復一致検査は無効のままで、古い増分 +ビルドの実験 `testModule0` も実行対象外です。 -クラス初期化、インライン化、エスケープ解析、ライブラリの変更もグラフを変えます。 -章をまたぐ差を割り当て器だけの品質差と解釈せず、同じ入力とコンパイラで -ヒューリスティックを比較してください。 +[分割計画](../docs/chapter-split.md)の主要な七つのスナップショットはこれで揃います。 +ネイティブライブラリの構築と最適化の調整は[付録](../docs/appendices/README.md)にまとめています。 diff --git a/chapter28b/README.md b/chapter28b/README.md index d6ab5666..189f2b7b 100644 --- a/chapter28b/README.md +++ b/chapter28b/README.md @@ -1,267 +1,177 @@ -# Chapter 25: Modules, Separate Compilation, and SSA Construction +# Chapter 28b: Separate Compilation English | [日本語](README.ja.md) -[Previous: Chapter 24](../chapter24/README.md) | -[Chapter index](../README.md#chapters) +[Previous: Chapter 28a](../chapter28a/README.md) -This chapter compiles Simple source files into reusable object files, loads -their types and ideal IR into later compilations, and links native programs -against a separately built system library. It also contains a substantial -parser, type, and memory-SSA redesign to support incomplete information. +The previous chapter compiled every discovered source file together. This +chapter lets a library and other code compile separately, and have full type +information. The client needs the library's *object* file, but not its source. +In spirit this is similar to Java's *class* file, with its full execution and +type semantics stored in the file; we happen to be using an ELF file for the +same reason. The ahead-of-time compiled code is *also* available for the +linking step. -You can also read [this chapter](https://github.com/SeaOfNodes/Simple/tree/linear-chapter25) -in the [linear history](https://github.com/SeaOfNodes/Simple/tree/linear) and -[compare it with Chapter 24](https://github.com/SeaOfNodes/Simple/compare/linear-chapter24...linear-chapter25). +An ordinary native object contains machine code, data, symbols, and +relocations. Simple adds a `.simple` section with types and the ideal IR. +Importing `.simple` lets the client check types and optimize calls into the +library. Calls that remain out of line use the library's machine code at +native link time. -Here is the [complete language grammar](docs/25-grammar.md) for this chapter. +## A library and a client -## Compilation units and names +The runnable example has two separate source roots: -`CompUnit` represents a source or object file. `ParseAll` discovers dependencies, -parses needed sources, and loads previously compiled units. Source and build -trees use corresponding relative paths; class names use dotted paths. For -example, `a/b/foo.smp` supplies the namespace `a.b.foo`. - -Each unit has Start/Stop nodes. A function's ownership by a compilation unit is -distinct from an edge representing unknown external callers: retaining code -for emission must not imply that arbitrary callers can reach it. +```text +src/test/smp/separate/library/Counters.smp +src/test/smp/separate/client/Main.smp +``` -The driver infers the project root from the input path and accepts `-o` for -output and `-L` for external Simple object search paths, including an object -file or a directory. Its help advertises `--root`, but this numbered snapshot -does not yet parse that option; the named modules snapshot does. -The compiler also resolves external C symbols for native linking. Names beginning -with `_` control the public interface; enclosing names also affect which -entries are exposed to unknown callers. +`Counters.smp` declares shared state and an instance constructor: -Explicit numeric C declarations bind directly to mutable native storage: +```simple +int calls=0; +val next_noInline = { -> calls++; return calls; }; -```java -i32 counter = "C"; -counter += 1; -return counter; +struct Counter { + val value=1; + new Counter = { int n -> + value=0; + for( int i=0; i<n; i++ ) value=value*2+1; + }; + val get = { -> value; }; +}; ``` -The native linker resolves `counter`. Reads and writes access that C variable; -the declaration neither copies nor initializes it. Repeated declarations and -module imports share the same storage alias. Integer and floating-point data -bindings are supported; pointer and aggregate data bindings are not yet supported. -External fields carry a storage flag and occupy no bytes in their declaring -class. Layout resolves their field offsets to `ExternOffset` symbols; instruction -selection replaces class-base-plus-offset with the native symbol address. -Native object output uses the existing AMD64 ELF writer; ARM and RISC-V support -these addresses through the in-memory linker. Thread-local variables and C macros -need accessor functions: the system library exposes `libc.errno()` through a small -native runtime accessor. - -Native function pointers still occupy four bytes. A native library can live -above that address range, so the AMD64 ELF writer gives each address-taken -native function a small entry in Simple's low-address code. The entry tail-jumps -through the native symbol's PLT entry; arguments and the return address pass -through unchanged. Direct calls still use the native symbol directly. - -Static class fields and addresses computed in code both refer to that entry. -Its stable weak symbol, `__simple_native$<name>`, lets separately compiled -objects agree on one pointer when linked into an executable. The class image -uses a four-byte relocation to this defined entry, instead of a four-byte -relocation to a dynamic native symbol in writable data, which GNU ld rejects. -Constant field Stores can still fold away. The entry is a Simple function -identity; it need not equal the raw address obtained by native code. - -`StaticDataTest.testNativeFunctionIdentity` reads two separately compiled class -images before their initializers run, compares their stored and computed -addresses, and calls the native function. It checks both object-link orders. - -## Object files contain ideal IR - -The ELF writer stores native code and a `.simple` section containing canonical -types, ideal graph nodes, and dependencies. The reader reconstructs that graph -so imported functions can participate in optimization, including inlining. -Calls that remain out of line are resolved by native linking. - -`GlobalBits` maps file identities and per-file indices to local dense indices -for aliases, function targets, and return points. Equal local numbers from -independent compilations must not be mistaken for equal identities. Types are -closed and upgraded after loading, including cross-unit cyclic references. - -Serialization preserves declaration types separately from sharpened flow types. -Every semantic node/type field needs matching read/write support. The source's -expensive serialization bijection check is currently disabled, and the larger -incremental-rebuild test `testModule0` is ignored. Neither is an active -validation guarantee. - -The external-field flag and symbolic offsets change the serialized layout. -The magic remains `C0DE`; there is no separate format version yet. Rebuild all -Simple object files before importing them with this compiler. - -## Constructing SSA before types are complete - -Simple still parses directly to SSA without an AST. Forward references mean an -expression's eventual type may be unavailable while parsing it. Syntax and -resolved lexical binding therefore choose topology; types subsequently refine, -optimize, and validate the graph. - -- Arithmetic nodes defer integer versus floating-point mode selection, including - resolution through recursive graph components after SCCP. -- Calls preserve their syntactic receiver slot before the callee kind is settled. - Symbolic field information defers layout-dependent decisions. -- Loads and ordinary Phis no longer carry parser-supplied lower-bound types. -- `TypeScalar` represents integer, float, memory-pointer, and function-pointer - values separately from global control/memory bottom. -- `TypeStruct._open` describes an incomplete field set; `_fref` records the - absence of an authoritative definition. Discovering fields does not define a type. -- Guards carry branch-proven zero/nonzero facts before the input family is known. - `FunPtrNode` retains an explicit edge to its function's return graph. - -The driver separates optimization from lowering: +`Main.smp` uses both parts of the library: -```text -Parse -> Iter -> Opto -> IterPostOpto -> FinalizeIR -> TypeCheck - -> Serialize (when producing an object) -> Lower -> LoopTree - -> Unlink -> Select -> Schedule -> LocalSched -> RegAlloc -> Encoding +```simple +val run = { int n -> + if( !n ) return 0; + val c = new Counters.Counter(n); + return c.get()+100*Counters.next_noInline(); +}; +return run(arg); ``` -`Opto` runs SCCP and resolves numeric modes. `IterPostOpto` applies peepholes -and inlining using those results; `FinalizeIR` settles field layouts and offsets. -Type checking sees the optimized ideal graph. Serialization saves that graph -before lowering introduces implementation details such as initialization guards. -For machine compilation, `Unlink` detaches imported function bodies before -instruction selection; calls retain the symbols needed for linking. Ideal-IR -evaluation keeps those bodies available and skips machine selection. +At runtime, `run(0)` returns without reaching or initializing `Counters`. -## Memory and construction +For `run(3)`, `Counters` initializes, the constructor computes 7 and +`calls`==1, so we get `(7+100*1)` or 107. -The parser carries ordinary bulk memory. The optimizer recovers precise aliases -using `MemMerge`, `MemPhi`, and `BulkMemPhi`. A bulk Phi covers all aliases -except its exclusions, represented by parallel precise Phis. Every slice must -be covered exactly once. +Calling `run(3)`, then `run(5)`, in one process increments the shared `calls` +twice. The first call gives `(7+100*1)`, or 107; the second gives +`(31+100*2)`, or 231. -User constructors are declared and called as follows: +(The `_noInline` suffix prevents inlining, so the example exercises native +linking as well as imported IR.) -```java -struct Box { - i64[] values; - new Box = { i64[] initial -> values = initial; }; -}; -return new Box(new i64[1]); +From this chapter directory, build the compiler and compile the library: + +```sh +make lib +make build/classes/test/.ttag +java -ea -cp "build/classes/main:build/classes/test:lib/*" \ + com.seaofnodes.simple.Simple --norun -o build/separate-example/lib/ \ + src/test/smp/separate/library/Counters.smp +``` + +Then compile and evaluate the client using that object: + +```sh +java -ea -cp "build/classes/main:build/classes/test:lib/*" \ + com.seaofnodes.simple.Simple --eval \ + -L build/separate-example/lib/Counters.o \ + src/test/smp/separate/client/Main.smp 3 ``` -An allocation helper is generated at the declaration. It allocates, calls the -hidden `<init>` and then the user constructor, and publishes initialized private -memory through `EscapeNode`s. Parser `Var` metadata detects early field reads -and missing initialization on constructor exits. Store widths come from target -declarations, not from the values being stored. -## Lazy class initialization +## What crosses the separate compilation boundary -File-level code is represented by a class initializer (`<clinit>`), distinct -from instance initialization. An active class-field access ensures that the -class is initialized. Taking a class reference is passive: it identifies the -class without running its body. Initialization runs once, on the path that -first needs it, and a parent initializes before its child. +The optimized IR owned by the object is compactly encoded in the `.simple` +section and fully reconstructed when read back in. The saved state includes +canonical declarations as well as optimized flow types. E.g. `int x=7` has +declared type `int` and initial value `7`. Struct field types (both declared +and optimized), function argument types (both declared and optimized), numeric +modes, store widths, and source locations also survive serialization. -For example, if `B.smp` computes `value`, then this code initializes B only -when `take` is true; the second access reuses the initialized class: +Each loaded `.simple` section restores its units into the shared `CompUnit` +tree, mirroring the original code layout. A dependency shared by several +objects uses the same `CompUnit`. Loaded aliases and function indices are +renumbered to avoid collisions; all the dense indices end up in the same +namespace. `GlobalBits` has the mapping from the imported file indices to the +local compilation indices; this remapping extends everywhere through the loaded +code, including recursive types that span several files. Importing skips +parsing the original source, but it still analysed and type checked with its +new callers. -```java -return take ? B.value + B.value : 0; -``` +`ParseAll` is the workhorse that discovers source and object dependencies. It +tracks missing symbol references, looking them up in the search path one by one, +and loading the matching files. + +The end result combines locally parsed and imported IR in one graph, while +keeping their separate `CompUnit`s. This combined compilation can take +advantage of e.g. constants in the library, or inline library code using the +normal inlining heuristics. The combined graph goes through the normal +compilation phases, including peepholes, Opto and TypeCheck. + +Dependencies between `CompUnit`s are recorded. An explicitly requested source +file is always compiled. Other source files are recompiled if their object is +missing, their source is newer, or a recorded dependency's object in the build +directory is newer. These timestamp checks are not yet a complete incremental +build system, there is some modest future work needed to finish this. + +Loading currently works at the object granularity. Unrelated objects can stay +unloaded, but selecting an object loads its entire saved graph and follows its +recorded dependencies. Future work will look at loading sub-parts more +incrementally. + + + +## Analysis, ownership, and initialization + +An imported function can participate in analysis without belonging to the +client's output. Loading the `io` library, with its `p` print function, means +that `p` is part of the analysis - but when writing out the `helloWorld` app, +`p` will not end up in the `helloWorld.o` file directly. It might be inlined +fully, or called out of line, but there won't be a stand-alone copy of it. + +After optimization, `Serialize` saves ideal IR before `Lower` expands class +initialization. `Unlink` then detaches imported bodies for machine compilation +and removes analysis call edges. The client emits its own bodies and any code +copied into them by inlining; the +remaining calls refer to symbols supplied by the library object. + +Class initialization follows the previous chapter's rules. Importing a type +does not initialize its class. Active uses retain `CLInit` checks until +lowering (unless optimization clears them out). The library supplies its +shared class object and flag. Initialization checks appear at all the active-touch +locations, whether local or remote; including source locations for error +reporting. -The compiler represents the obligation with an opaque `CLInit` node. It carries -control, whole-memory effects, the class pointer, and a load of the hidden -`$init` flag. Its results include a memory slice where that flag is known true. -Ordinary memory SSA and load simplification can therefore remove repeated -checks, including after a merge when every incoming path has initialized the -class. Calls conservatively account for arbitrary memory effects. - -After optimization, the compiler builds a graph containing function calls, -class touches, and parent requirements. A strongly connected component with -more than one class initializer is an error. Ordinary recursion and helper -cycles involving just one initializer are allowed. Checking after optimization -means eliminated branches and call targets do not create false dependencies. -Diagnostics show the cyclic edges and their source locations, including calls -through helpers and imported code. - -Object files preserve opaque initialization nodes and their source locations. -Imported ideal bodies participate in the combined cycle check before lowering. -Lower then expands each remaining obligation into a flag test, a conditional -initializer call, and control/memory merges. The initializer owns the flag store; -the store is at body entry to support permitted same-class reentry. Cross-class -cycles have already been rejected. This is a single-threaded protocol. - -The selected entry class receives the current integer `arg`; other initializers -receive zero. Planned argument arrays and further initialization optimizations -are described in the [roadmap](ROADMAP.md). - -## System library and examples - -The former single `sys.smp` library is organized into units for libc bindings, -I/O, characters, scanners, arrays, growable buffers, and bitsets. The native -driver supports real program arguments. Examples in `docs/examples` include -Bubble Sort, Capitalize, Dijkstra, and FileIO. - -`Chapter25Test` exercises library loading and native linking, including a Hello -World call deliberately kept out of line. That test checks the client's external -symbol before linking against `sys.o`. Other tests cover forward constructors, -field updates through calls, literal escapes, and reduced compiler failures. -Independent regressions are candidates for earlier chapters. ## Building and checking -From this directory, `make lib` supplies Java test dependencies and `make tests` -runs the raw chapter suites, standalone suites, system tests, and fuzzer -regressions. `make release` builds the compiler jar and native library artifacts; -`make tags` builds editor tags. Native tools and the selected CPU/ABI must match -the environment; the Makefile currently defaults to x86-64/win64. - -Serialized IR uses the `C0DE` header; pointer types include an independent -read-only access bit. The format is not versioned yet, so rebuild object files -after format changes. Backward compatibility is not currently supported. - -Compiler changes can invalidate both serialized IR and native code in `sys.o`. -Rebuild it before interpreting linked-program test results. A source-only subset -is not equivalent to this chapter's full `make tests`. - -## RegAlloc improvements: live-range area and split cost - -Chapter 24 tried cold copies before splitting a loop Phi's hot edges. This -chapter also estimates how much scheduled code a spill frees from register -pressure, divided by the estimated cost of the new copies. Callee-save ranges -cover a whole function. Cloneable constants can be rebuilt nearer their uses. -Copies inside loops cost more: the estimate weights them by `8^loopDepth`. -These are inexpensive approximations, especially for values with many uses. - -A single-use function address in the same block gets priority for -rematerialization when it is separated from its use. Moving that address -calculation next to the use adds no instruction and avoids preserving a -callee-save register merely to hold the constant. Adjacent uses and multi-use -addresses retain the area/cost ranking. - -Rows are fixed test cohorts; columns are compiler chapters. Each cell gives -**Ops / RA / X**: executed ARM+RISC-V instructions, the subset emitted for -allocator copies/rematerializations, and x86's loop-weighted split-move estimate. -**Cases D / X** counts measured dynamic program/target cases and x86 compilations; -membership and inputs stay fixed across columns. Native-library work is excluded. -Chapter 20 has no encoder, hence no dynamic counts. Lower is better within each -metric; see the [measurement details](../docs/regalloc-spills.md) for coverage. - -<!-- spill-matrix:start --> -| Test cohort | Cases D / X | Ch 20: Ops / RA / X | Ch 21: Ops / RA / X | Ch 22: Ops / RA / X | Ch 23: Ops / RA / X | Ch 24: Ops / RA / X | Ch 25: Ops / RA / X | -|---|---:|---:|---:|---:|---:|---:|---:| -| Ch 20 | 22 / 13 | — / — / 161 | 55,173 / 5,407 / 174 | 58,751 / 9,099 / 175 | 58,736 / 9,084 / 184 | 58,720 / 9,068 / 183 | 58,965 / 9,076 / 208 | -| Ch 21 | 20 / 18 | | 6,207 / 261 / 461 | 6,324 / 379 / 475 | 6,326 / 381 / 475 | 6,325 / 380 / 470 | 6,196 / 219 / 534 | -| Ch 22 | 14 / 8 | | | 198 / 60 / 7 | 194 / 56 / 7 | 194 / 56 / 7 | 252 / 92 / 11 | -| Ch 23 | 14 / 8 | | | | 630 / 160 / 36 | 630 / 160 / 36 | 864 / 358 / 36 | -| Ch 24 | 36 / 20 | | | | | 4,923 / 1,341 / 790 | 4,646 / 1,335 / 609 | -| Ch 25 | 2 / 12 | | | | | | 20 / 0 / 1,856 | -<!-- spill-matrix:end --> - - -Chapter 25 introduces several features, which when run on the older tests -generate a lot more code, which in turn generates more spills. - -Class initialization, inlining, escape analysis, and library adaptations -all change the graphs. To help with this, we adjust the inlining -heuristics to try to inline and optimize the boilerplate. +From the repository root: + +```sh +make tests CHAPTERS=chapter28b +``` +The suite includes the preceding constructor and escape regressions, imported +constructors, initialization cycles across objects, shared static data, native +function thunks, and native library/client execution. Make also builds the +modular `sys` object used by the library tests. Clients can import that object +with `-L`; they do not need to parse the standard library's source again. +`make release` builds the launcher jar and standard library; `make view` +starts the shared graph viewer. + +The serialized format is internal and currently unversioned beyond its `C0DE` +magic. Rebuild Simple objects when the compiler or layout changes. The expensive +serialization bijection check remains disabled, and `testModule0`, the older +incremental-rebuild experiment, remains ignored. They are not validation +guarantees for this snapshot. + +This completes the seven main snapshots in the [approved split](../docs/chapter-split.md). +See the appendices on [native libraries](../docs/appendices/native-libraries.md) +and [optimizer/backend tuning](../docs/appendices/optimizer-tuning.md). diff --git a/chapter28b/ainotes.txt b/chapter28b/ainotes.txt index 625acddc..c0b03a4d 100644 --- a/chapter28b/ainotes.txt +++ b/chapter28b/ainotes.txt @@ -1,4 +1,8 @@ +Historical design notes from the former combined Chapter 25, now Chapter 28b. +Current status and remaining work are in ../docs/stage-audit.md and +../docs/chapter-backports.md. The original intent below is preserved. + Meta-info: I intend to keep extending and re-editing this "ainotes.txt" file so I can reprompt the you (the AI) between AI restarts, As such this file will contain a mix of high-level design decisions, goals, intents and wisdom, and diff --git a/chapter28b/docs/25-grammar.ja.md b/chapter28b/docs/28b-grammar.ja.md index 9e9fb820..65944fe2 100644 --- a/chapter28b/docs/25-grammar.ja.md +++ b/chapter28b/docs/28b-grammar.ja.md @@ -1,6 +1,6 @@ -# 第25章の文法 +# 第28b章の文法 -[English](25-grammar.md) | 日本語 +[English](28b-grammar.md) | 日本語 ```antlrv4 grammar SimpleLanguage; diff --git a/chapter28b/docs/25-grammar.md b/chapter28b/docs/28b-grammar.md index 065cf051..c7a279fa 100644 --- a/chapter28b/docs/25-grammar.md +++ b/chapter28b/docs/28b-grammar.md @@ -1,6 +1,6 @@ -# Grammar for Chapter 25 +# Grammar for Chapter 28b -English | [日本語](25-grammar.ja.md) +English | [日本語](28b-grammar.ja.md) ```antlrv4 grammar SimpleLanguage; diff --git a/chapter28b/docs/Makefile b/chapter28b/docs/Makefile deleted file mode 100644 index 12659b8f..00000000 --- a/chapter28b/docs/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -GV := $(wildcard *.gv) -SVG := $(patsubst %.gv,%.svg,$(GV)) - -.PHONY: all -all: $(SVG) - -%.svg: %.gv - dot -Tsvg $< > $@ - -.PHONY: clean -clean: - @rm -f $(SVG) diff --git a/chapter28b/helloWorld.exe.stackdump b/chapter28b/helloWorld.exe.stackdump deleted file mode 100644 index 369cf738..00000000 --- a/chapter28b/helloWorld.exe.stackdump +++ /dev/null @@ -1,16 +0,0 @@ -Exception: STATUS_ACCESS_VIOLATION at rip=001004010BB -rax=0000000100405000 rbx=0000000000000001 rcx=00000001004010D0 -rdx=00000001004010A6 rsi=0000000000000015 rdi=0000000800049E60 -r8 =00000000FFFFC568 r9 =000000010040500C r10=0000000100000000 -r11=00000001004010A6 r12=0000000800051F10 r13=00000000FFFFCC40 -r14=00000000FFFFFFFF r15=00000000FFFFCC79 -rbp=0000000800051EF0 rsp=00000000FFFFCB50 -program=C:\Users\cliffc\Desktop\Simple\chapter25\build\objs\helloWorld.exe, pid 1066, thread main -cs=0033 ds=002B es=002B fs=0053 gs=002B ss=002B -Stack trace: -Frame Function Args -00800051EF0 001004010BB (0000000000D, 00000000000, 00000000015, 0010040181E) -00800051EF0 00000000001 (00000000000, 00000000015, 0010040181E, 00000000015) -00800051EF0 0010040500C (00000000015, 0010040181E, 00000000015, 00800049E60) -00800051EF0 0000000000D (00000000015, 0010040181E, 00000000015, 00800049E60) -End of stack trace diff --git a/chapter28b/pom.xml b/chapter28b/pom.xml index c60ec7a2..cad8a591 100644 --- a/chapter28b/pom.xml +++ b/chapter28b/pom.xml @@ -8,9 +8,9 @@ <artifactId>simple</artifactId> <version>1.0</version> </parent> - <artifactId>chapter25</artifactId> + <artifactId>chapter28b</artifactId> <packaging>jar</packaging> - <name>Chapter 25</name> + <name>Separate Compilation</name> <properties> <simple.graph.directory>${project.basedir}/../graph</simple.graph.directory> <simple.print.directory>${project.basedir}/../print</simple.print.directory> diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/Parser.java b/chapter28b/src/main/java/com/seaofnodes/simple/Parser.java index 5ceda6a9..87a3570a 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/Parser.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/Parser.java @@ -213,7 +213,6 @@ public class Parser { } - /** * Parses a function body, assuming the header is parsed. */ @@ -255,12 +254,14 @@ public class Parser { } 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 - _scope.push(new Kind.Func(funName)); + _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; @@ -290,6 +291,20 @@ public class Parser { _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); + } + } + // The initializer owns its run-once flag store. Parent initialization // is a structural requirement, executed before this body by CLInit lowering. if( fun.isClz() ) { @@ -316,6 +331,9 @@ public class Parser { nvar = updateSelfAsFieldsDiscovered( typeName, nvar ); } + if( isInit ) finishConstructor(); + _pendingCtor=outerCtor; + ParmNode self = fun.parm(2); // Last expression is the return if( ctrl()._type==Type.CONTROL ) { @@ -456,6 +474,10 @@ public class Parser { 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)); @@ -597,6 +619,9 @@ public class Parser { } // Constructor declaration: new TypeName = { args -> body }; + private record Constructor(String name, Lexer body) {} + private Constructor _pendingCtor; + private Node parseConstructorDeclaration() { TypeStruct self = _scope.constructorSelf(); assert self != null; @@ -605,6 +630,43 @@ public class Parser { if( name != expect ) throw error("Constructor name '" + name + "' must match type name '" + expect + "'"); require("="); + 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); @@ -614,7 +676,7 @@ public class Parser { 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)); - return require(ctor,";"); + require(ctor,";"); } /** @@ -1006,9 +1068,9 @@ public class Parser { if( _scope.in(def._idx)._type!=Type.TOP && def._final && // Inside an allocation, final assign is OK, outside nope. // The alloc() call added the allocation scope - !(_scope.inConstructor() && def._idx >= _scope.klast()._lexSize) && - !inExplicitConstructor() ) + !_scope.canInit(def) ) throw error("Cannot reassign final '"+name+"'"); + checkSharedFunction(def); // Parse assignment expression Node expr = parseAsgn(); @@ -1082,6 +1144,15 @@ public class Parser { }; } + 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. @@ -1163,6 +1234,11 @@ public class Parser { throw error("a not-null/non-zero expression"); t = expr._type; 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 @@ -1214,7 +1290,6 @@ public class Parser { } - /** * Parse a struct declaration, and return the following statement. * Structs cannot be redefined, but can be nested. @@ -1744,7 +1819,6 @@ public class Parser { } - /** * Parse an eq/ne expression */ @@ -1789,8 +1863,8 @@ public class Parser { return cmp.unkeep(); } - // rhs is keeped() and becomes lhs - // cmp is NOT keeped() and is the last test + // Keep the middle operand separately from the boolean result. + // The next test compares adjacent operands, not that boolean. Node ifNode = new IfNode(ctrl(), cmp.unkeep()).peephole(); Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole(); @@ -1806,9 +1880,9 @@ public class Parser { throw error("Mixing relational directions in a chained relational test"); // True side parses next arm of test ctrl(ifT.unkeep()); - lhs = rhs; // lhs is keeped() and is old RHS + lhs = rhs; // Reuse the previous RHS exactly once rhs = parseShift().keep(); - cmp = makeCompBool(dir,lhs.unkeep(),rhs); // Convert to a bool + cmp = makeCompBool(dir0,lhs.unkeep(),rhs); // Convert to a bool ifNode = new IfNode(ctrl(), cmp).peephole(); ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep(); ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole(); @@ -1822,7 +1896,7 @@ public class Parser { ctrl(ifT.unkeep()); RegionNode r = fail.mergeScopes(_scope, loc()).init(); _scope = fail; - return new PhiNode("", r,_code.ZERO,con(1)).peephole(); + return new PhiNode("", r,con(0),con(1)).peephole(); } private int parseCompDir() { @@ -1833,12 +1907,11 @@ public class Parser { return 0; } - private Node makeCompBool( int dir0, Node lhs, Node rhs ) { - // Convert to a bool - if( Math.abs(dir0)==2 ) // Swap direction - { Node tmp = lhs; lhs = rhs; rhs = tmp; } - lhs = peep(dir0 < 0 ? new BoolNode.LE(lhs,rhs) : new BoolNode.LT(lhs,rhs)); - return peep(lhs); + private Node makeCompBool( int dir, Node lhs, Node rhs ) { + if( Math.abs(dir)==2 ) + { Node tmp=lhs; lhs=rhs; rhs=tmp; } + Node cmp = dir < 0 ? new BoolNode.LE(lhs,rhs) : new BoolNode.LT(lhs,rhs); + return peep(cmp); } /** @@ -1927,9 +2000,10 @@ public class Parser { if( n != null && !(n.type() instanceof TypeMemPtr) ) { if( n._extern!=0 ) return parsePostfixName(_scope.in(n),name,delta); - if( n._final ) + if( n._final && !_scope.canInit(n) ) throw error("Cannot reassign final '"+n._name+"'"); - Node expr = zsMask(peep(new AddNode(_scope.in(n),con(delta))),n.type()); + checkSharedFunction(n); + Node expr = convertExpr(peep(new AddNode(_scope.in(n),con(delta))),n.type()); _scope.update(n,expr); return expr; } @@ -2015,12 +2089,10 @@ public class Parser { return parsePostfixName(rvalue,id,0); if( var._fref ) return parsePostfix(rvalue); - // Required constructor fields begin as their declared pointer type - // widened to include null. Inside the explicit constructor, any read - // before all paths replace that value observes a partially initialized - // object. Ordinary methods run only after construction is complete; - // their enclosing declaration scope still carries the same Var marker. - if( inExplicitConstructor() && var._uninit && rvalue._type instanceof TypeNil tn && tn.nullable() ) + // 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 @@ -2029,13 +2101,13 @@ public class Parser { 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 ) + 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 && // Out of scope + 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"); @@ -2062,8 +2134,9 @@ public class Parser { return parsePostfixMethod(rvalue,_scope.in(_scope.update(_scope.var(selfx),null))); } // Assign-update direct into Scope - if( var._final ) + if( var._final && !_scope.canInit(var) ) throw error("Cannot reassign final '"+var._name+"'"); + checkSharedFunction(var); Node op = opAssign(ch,rvalue, var.type() ); _scope.update(var,op); return postfix(ch) ? rvalue.unkeep() : op; @@ -2261,6 +2334,58 @@ public class Parser { 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)); @@ -2283,6 +2408,7 @@ public class Parser { } 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(); @@ -2782,6 +2908,7 @@ public class Parser { _position = pos; } + public static void gather(Lexer loc, HashMap<String,Integer> strs) { if( loc==null || loc._cu==null ) return; Serialize.gather(strs,loc._cu._cname); diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java index 47ec0112..8302ede6 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -318,7 +318,7 @@ public class CodeGen { } public String entryClinitName() { - return Parser.addClzPrefix(_srcName==null ? "Test" : _srcName)+".<clinit>"; + return Parser.addClzPrefix(_srcName==null ? "Test" : _srcName.replace('/','.'))+".<clinit>"; } // Verbose printing during compilation @@ -463,6 +463,8 @@ public class CodeGen { // --------------------------- // Parser object + public final Ary<CallNode> _calls = new Ary<>(CallNode.class); + public final Parser P; // Parse ASCII text into Sea-of-Nodes IR diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java index 128c4136..71126795 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java @@ -57,7 +57,8 @@ public class CompUnit { _obj = new File( CODE._buildDir + "/" + fname + ".o" ); boolean mustCompile = _smp.exists() && // Has source to compile - ( !_obj.exists() || // No object file + ( fname.equals(CODE._srcName) || // Explicit input is a compilation request + !_obj.exists() || // No object file _smp.lastModified() > _obj.lastModified() || // Out-of-date with source? checkDependentObjs() ); // Out-of-date with other objs? _src = mustCompile ? new String(Files.readAllBytes(_smp.toPath())) : null; diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Opto.java b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Opto.java index ef222f60..3b3f9f19 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Opto.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Opto.java @@ -145,8 +145,7 @@ abstract public class Opto { private static void linkStart( CodeGen code, FunNode fun ) { assert !fun.isDead(); StartCUNode start = fun._compunit._start; - if( fun.nIns() < 2 ) { - assert fun.nIns() < 2 || fun.in(1) != start; + if( !fun.unknownCallers() ) { assert !start.isDead(); code.keepInCompUnit(fun); // Function is reachable by any *remote* caller who gets the pointer! diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java index 7ce727fd..c55f9a90 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java @@ -81,11 +81,16 @@ public abstract class ParseAll { } } + private static void enqueue(CompUnit cu) { + if( cu._par!=null && (cu._par._src!=null || (cu._par._obj!=null && cu._par._obj.exists())) ) 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 NEEDS_LOAD.clear(); // Prime the pump with the first CompUnit - WORK.add(cunit); + enqueue(cunit); // Work through all unparsed compilation units and parse while( !WORK.isEmpty() ) { CompUnit cu = WORK.remove(0); @@ -245,7 +250,7 @@ public abstract class ParseAll { if( sub==null ) sub=makeCUnit(code,null,fname,depObj(code,cunit,fname)); // If not seen before or out of date, parse the new nested cunit if( sub._clz == null && WORK.find(sub)==-1 && NEEDS_LOAD.find(sub)==-1 ) - WORK.add(sub); + enqueue(sub); } } catch( IOException ioe ) { throw new RuntimeException(ioe); @@ -370,7 +375,7 @@ public abstract class ParseAll { 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 ) - WORK.add(sub); + enqueue(sub); return sub; } @@ -405,7 +410,7 @@ public abstract class ParseAll { Parser.TYPES.put(clzName,clz = TypeStruct.make(clzName,true)); cunit._clz = clz; if( WORK.find(cunit) == -1 && NEEDS_LOAD.find(cunit) == -1 ) - WORK.add(cunit); + enqueue(cunit); return cunit; } } diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java index 6a23252d..5a74ded2 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java @@ -110,9 +110,12 @@ abstract public class Serialize { stopType.gather(types); startType.gather(types); // A defined class remains importable even when its initializer body dies. + // Nested structs also need their class declaration: <new> and <ctor> + // can be absent from graph types even while their function bodies live. + // Keep the nearest file owner, so an imported child is not re-exported + // merely because its parent was compiled here. for( TypeStruct ts : pubTypes ) - if( Parser.startsClzPrefix(ts._name) && - Arrays.binarySearch(units,ts._name.substring(Parser.CLZ.length()).replace('.','/'))>=0 ) + if( ownedClass(ts,units) ) ts.gather(types); // Only record canonical declarations already reachable from this @@ -262,6 +265,18 @@ abstract public class Serialize { } // Returned array of *public* strings only. + private static boolean ownedClass(TypeStruct ts, String[] units) { + if( !Parser.startsClzPrefix(ts._name) ) return false; + String name=ts._name.substring(Parser.CLZ.length()).replace('.','/'); + while( true ) { + if( CodeGen.CODE._compunits.containsKey(name) ) + return Arrays.binarySearch(units,name)>=0; + int slash=name.lastIndexOf('/'); + if( slash<0 ) return false; + name=name.substring(0,slash); + } + } + static void read_public_strs(ElfReader elf) { BAOS bais = elf._bais; Type.TAGOFFS(); diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index 36a012e1..fc6effdc 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -127,7 +127,7 @@ public class CallEndNode extends CFGNode implements MultiNode { // A linked function for every concrete function TypeTuple state = hasExtern - ? TypeTuple.make(Type.CONTROL,TypeMem.BOT,tfp.ret()) + ? 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); diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/CallNode.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/CallNode.java index 71f6d146..bcb756fa 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/node/CallNode.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/CallNode.java @@ -22,7 +22,7 @@ public class CallNode extends CFGNode { // Source location for late reported errors public final Parser.Lexer _loc; - public CallNode(Parser.Lexer loc, Node... nodes) { super(nodes); _loc=loc; } + 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 Tag serialTag() { return Tag.Call; } public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) { @@ -67,7 +67,25 @@ public class CallNode extends CFGNode { public TypeFunPtr tfp() { return (TypeFunPtr)fptr()._type; } // Call is to an externally supplied code - public boolean external() { return false; } + public boolean external() { + 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(fidx)!=null ) return true; + return false; + } + + // 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()); + if( _type!=Type.CONTROL || !external() ) return TypeMem.START; + 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 TypeMem.make(1,Type.BOTTOM,false,false,false,escaped._escFs,escaped._escAs); + } // Find the Call End from the Call diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/Node.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/Node.java index 77b43ba8..8718f351 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/node/Node.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/Node.java @@ -715,6 +715,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; } diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java index 21d60d53..694d5dcb 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -39,7 +39,12 @@ public class ScopeNode extends MemMergeNode { public final TypeStruct _self; } // Function scope - public static class Func extends Kind { public Func(String name) { _name=name;} public final String _name; } + public static class Func extends Kind { + public final String _name; + public final boolean _constructor; + public Func(String name) { this(name,false); } + public Func(String name, boolean constructor) { _name=name; _constructor=constructor; } + } public int _lexSize; // Number of Vars in this scope } @@ -184,10 +189,7 @@ public class ScopeNode extends MemMergeNode { } // Which lexical scope contains this Var? - public Kind kind( Var v ) { - int kx = kindx(v); - return kx == -1 ? null : _kinds.at(kx); - } + public int kindFcnx( Var v ) { for( int i=depth()-1; i>=0; i-- ) @@ -202,6 +204,21 @@ public class ScopeNode extends MemMergeNode { return kx == -1 ? null : _kinds.at(kx); } + // 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 // typically hits many dozens of variables. @@ -346,7 +363,7 @@ public class ScopeNode extends MemMergeNode { 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 && !var(i)._final ? this : in(i)); + dup.addDef(loop && loopVar(var(i)) ? this : in(i)); return dup; } @@ -431,7 +448,7 @@ public class ScopeNode extends MemMergeNode { // Fill in the backedge of any inserted Phis void _endLoop( ScopeNode scope, Node back, Node exit ) { for( int i=1; i<nIns(); i++ ) { - if( var(i)._final ) continue; // Final vars did not get modified in the loop + if( !loopVar(var(i)) ) continue; 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(); diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java index d3a356ed..43acf631 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/StopCUNode.java @@ -79,6 +79,11 @@ public class StopCUNode extends StopNode { // 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; @@ -88,6 +93,10 @@ public class StopCUNode extends StopNode { 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); } diff --git a/chapter28b/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/chapter28b/src/main/java/com/seaofnodes/simple/node/StoreNode.java index cc958348..5fa0b99f 100644 --- a/chapter28b/src/main/java/com/seaofnodes/simple/node/StoreNode.java +++ b/chapter28b/src/main/java/com/seaofnodes/simple/node/StoreNode.java @@ -184,8 +184,12 @@ public class StoreNode extends MemOpNode { 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 - if( _size!=0 && val() instanceof AndNode and && and.in(2)._type.isConstant() ) { + 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(); @@ -198,7 +202,7 @@ public class StoreNode extends MemOpNode { } // Store will chop high order bits off; math to change those bits can be dropped. - if( _size!=0 && 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() ) { @@ -253,7 +257,7 @@ public class StoreNode extends MemOpNode { return switch( use ) { case MemOpNode mem -> mem._alias; case MemPhiNode phi -> phi._alias; - case EscapeNode esc -> throw Utils.TODO("Untested"); // esc.pub()==this ? esc.fld()._alias : 0; + case EscapeNode esc -> esc.pub()==this ? esc.fld()._alias : 0; default -> 0; }; } diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/BoundaryTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/BoundaryTest.java new file mode 100644 index 00000000..b736ccb3 --- /dev/null +++ b/chapter28b/src/test/java/com/seaofnodes/simple/BoundaryTest.java @@ -0,0 +1,86 @@ +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 testLazyTables() { + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen("src/test/smp/lazy","build/objs",null,"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/chapter28b/src/test/java/com/seaofnodes/simple/ChainedComparisonTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/ChainedComparisonTest.java new file mode 100644 index 00000000..8db9c024 --- /dev/null +++ b/chapter28b/src/test/java/com/seaofnodes/simple/ChainedComparisonTest.java @@ -0,0 +1,37 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.TypeInteger; +import org.junit.Test; +import static org.junit.Assert.*; + +public class ChainedComparisonTest { + @Test public void testAdjacentOperandsAndOperators() { + String[] expressions = {"10 < arg < 20", "0 < arg <= 3", "3 >= arg > 0", + "0 <= arg < 3", "0 < arg <= 3 < 4"}; + String[][] expected = {{"0","0","1","0"}, {"0","1","0","0"}, + {"0","1","0","0"}, {"1","0","0","0"}, + {"0","1","0","0"}}; + int[] args = {0,3,15,30}; + for(int i=0;i<expressions.length;i++) { + CodeGen code = new CodeGen("return "+expressions[i]+";").driver(CodeGen.Phase.TypeCheck); + for(int j=0;j<args.length;j++) assertEquals(expressions[i],expected[i][j],Eval2.eval(code,args[j])); + } + } + + @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;",seed,true) + .driver(CodeGen.Phase.TypeCheck); + assertEquals("100",Eval2.eval(code,0)); + assertEquals("441",Eval2.eval(code,3)); + assertEquals("530",Eval2.eval(code,4)); + } + try { + new CodeGen("return 0 < arg > 1;").parse(); + fail("mixed directions"); + } catch(RuntimeException e) { + assertEquals("Mixing relational directions in a chained relational test",e.getMessage()); + } + } +} diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java index 08029c4b..4019fd38 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java @@ -300,7 +300,7 @@ A x = new A; 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; };", // Argument shadows field + "!B !b; new A = { !B !b -> b = new B; };", // Argument shadows field "new A = { -> }; !B !b;", // Field declared later } ) { CodeGen code = new CodeGen("struct B {}; struct A { "+fields+" }; return 0;"); diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter18Test.java b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter18Test.java index ae1dca5f..632b9539 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter18Test.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter18Test.java @@ -436,7 +436,7 @@ return s.x; catch( Exception e ) { assertEquals("Accessing unknown field 'x' from '*Test.S'",e.getMessage()); } } - @Test @Ignore + @Test public void testErr5() { CodeGen code = new CodeGen( """ @@ -450,8 +450,8 @@ return g(); } - // Mutual recursion. Fails without SCCP to lift the recursive return types. - @Ignore @Test + // Mutual recursion: check results, independent of how much gets inlined. + @Test public void testFcnMutRec() { CodeGen code = new CodeGen( """ @@ -460,7 +460,6 @@ val is_odd = { int x -> x ? is_even(x-1) : false; }; return is_even(arg); """); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("Stop[ return #2; return Phi(Region,0,1,#2); ]", code.print()); assertEquals("1", Eval2.eval(code, 0)); assertEquals("0", Eval2.eval(code, 1)); assertEquals("1", Eval2.eval(code, 2)); diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter20Test.java index 0458941e..cdfd16a7 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter20Test.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter20Test.java @@ -202,7 +202,6 @@ flt farg = arg; return _test_sqrt_noInline(farg) + _test_sqrt_noInline(farg+2.0) } - @Test public void testArray1() { String src = @@ -257,9 +256,9 @@ val _hashCodeString = { !String self -> return hash; }; """; - testCPU(src,"x86_64_v2", "SystemV",21,null); - testCPU(src,"riscv" , "SystemV",8,null); - testCPU(src,"arm" , "SystemV",8,null); + testCPU(src,"x86_64_v2", "SystemV",12,null); + testCPU(src,"riscv" , "SystemV",6,null); + testCPU(src,"arm" , "SystemV",6,null); } @Test diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter21Test.java index 07ae42eb..2fcf65e9 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -383,9 +383,9 @@ val _hashCodeString = { !String self -> return hash; }; """; - testCPU(src,"x86_64_v2", "SystemV",21,null); - testCPU(src,"riscv" , "SystemV",8,null); - testCPU(src,"arm" , "SystemV",8,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 { @@ -422,7 +422,7 @@ val _hashCodeString = { !String self -> return hash; }; """; - TestC.runC(src, "stringHash", "",21); + TestC.runC(src, "stringHash", "",12); } @Test public void testLoop2() { diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter25Test.java index 275be96a..31a616b5 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter25Test.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter25Test.java @@ -412,7 +412,7 @@ public class Chapter25Test { @Test public void testHelloWorldDriver() throws Exception { String out = "build/objs/helloWorldDriver/"; - Simple.main(new String[]{"-L",SYS_BLDDIR,"-o",out,"--norun","docs/examples/A_helloWorld.smp"}); + Simple.main(new String[]{"-L",SYS_BLDDIR,"-o",out,"--norun","src/test/smp/examples/A_helloWorld.smp"}); assertTrue(Files.isRegularFile(Path.of(out,"A_helloWorld.o"))); } @@ -423,7 +423,7 @@ public class Chapter25Test { try( PrintStream capture = new PrintStream(output,true,StandardCharsets.UTF_8) ) { System.setOut(capture); Simple.main(new String[]{"-L",SYS_FILE.toString(),"-o","build/objs/helloWorldDriverLibFile/", - "docs/examples/A_helloWorld.smp"}); + "src/test/smp/examples/A_helloWorld.smp"}); } finally { System.setOut(saved); } @@ -471,7 +471,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2); @Test public void testBubbles() throws IOException { - String src = Files.readString( Path.of("docs/examples/BubbleSort.smp")); + String src = Files.readString( Path.of("src/test/smp/examples/BubbleSort.smp")); String exe = TestC.compile(src,"BubbleSort",new Ary<>(new String[]{SYS_BLDDIR}), TestC.CALL_CONVENTION,null,null,-1); assertEquals("[-17, 2, 3, 999]\n", @@ -492,7 +492,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2); @Test public void testCapitalize() throws IOException { - String src = Files.readString(Path.of("docs/examples/Capitalize.smp")); + String src = Files.readString(Path.of("src/test/smp/examples/Capitalize.smp")); String exe = TestC.compile(src,"Capitalize",new Ary<>(new String[]{SYS_BLDDIR}), TestC.CALL_CONVENTION,null,null,-1); assertEquals("Hello world\n",TestC.exec(exe,"hello world")); @@ -505,7 +505,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2); @Test public void testDijkstra() throws IOException { - String src = Files.readString(Path.of("docs/examples/Dijkstra.smp")); + String src = Files.readString(Path.of("src/test/smp/examples/Dijkstra.smp")); String matrix = "[0, 2, 0, 6, 0, 2, 0, 3, 8, 5, 0, 3, 0, 0, 7, 6, 8, 0, 0, 9, 0, 5, 7, 9, 0]"; String exe = TestC.compile(src,"Dijkstra",new Ary<>(new String[]{SYS_BLDDIR}), TestC.CALL_CONVENTION,null,null,-1); @@ -520,7 +520,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2); @Test public void testFileIO() throws IOException { - String src = Files.readString(Path.of("docs/examples/FileIO.smp")); + String src = Files.readString(Path.of("src/test/smp/examples/FileIO.smp")); String exe = TestC.compile(src,"FileIO",new Ary<>(new String[]{SYS_BLDDIR}), TestC.CALL_CONVENTION,null,null,-1); assertEquals("File I/O succeeded\n",TestC.exec(exe)); diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java new file mode 100644 index 00000000..f583e2d2 --- /dev/null +++ b/chapter28b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java @@ -0,0 +1,65 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.StoreNode; +import org.junit.Test; +import static org.junit.Assert.*; + +public class FinalConstructionTest { + private static final String LOOP = """ + val step={int x -> x*2+1;}; + struct C { + val x=1; + new C={int n -> + x=0; x=1; + for(int i=0; i<n; i++) x=step(x); + }; + val get={->x;}; + }; + return new C(arg).get(); + """; + + @Test public void testFinalConstructorLocals() throws java.io.IOException { + var code=new CodeGen(LOOP).driver(CodeGen.Phase.LocalSched); + assertEquals("1",Eval2.eval(code,0)); + assertEquals("15",Eval2.eval(code,3)); + assertEquals("63",Eval2.eval(code,5)); + code._stop.walk(n -> { + if( n instanceof StoreNode st && st._name.equals("x") ) + assertEquals("Final field writes belong after the loop",0,st.cfg0().loopDepth()); + return null; + }); + String[] sources={ + "struct C { val x=1; new C={->x=1; x=2;}; }; return new C().x;", + "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i<n;i++) self.x*=2;}; }; return new C(arg).x;", + "struct C { val x=1; new C={int n -> for(int i=0;i<n;i++) ++x;}; }; return new C(arg).x;", + "struct C { val x=1.5; new C={->x=2.5;}; }; return new C().x;", + "struct C { val x=\"a\"; new C={int n -> for(int i=0;i<n;i++) x=\"b\";}; }; return new C(arg).x[0];", + "struct C { val x=new int[1]; new C={->x=new int[2]; x=new int[3];}; }; return new C().x#;" + }; + String[] results={"2","8","4","2.5","98","3"}; + for( int i=0; i<sources.length; i++ ) + assertEquals(results[i],Eval2.eval(new CodeGen(sources[i]).driver(CodeGen.Phase.TypeCheck),3)); + + String[] rejected={ + "struct C { val x=1; }; val c=new C(); c.x=2; return c.x;", + "struct C { new C={->val x=1; x=2;}; }; return new C();", + "struct C { val f={->1;}; new C={->f={->2;};}; }; return new C().f();" + }; + for( String src : rejected ) { + try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(src); } + catch( Parser.ParseException e ) { assertTrue(e.getMessage(),e.getMessage().startsWith("Cannot")); } + } + + var dir=java.nio.file.Path.of("build/objs"); + java.nio.file.Files.createDirectories(dir); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.smp"),"val buildFinal={int arg ->"+LOOP+"};"); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.c"),""" + extern long long buildFinal(long long); + int main(void) { return buildFinal(0)!=1 || buildFinal(3)!=15 || buildFinal(5)!=63; } + """); + 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/chapter28b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java new file mode 100644 index 00000000..4576bc94 --- /dev/null +++ b/chapter28b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java @@ -0,0 +1,90 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; + +public class SeparateCompilationTest { + @Test public void testImportedNativeExposure() throws Exception { + Path build=Path.of("build/objs/separate"); + Files.createDirectories(build); + Path lib=Files.createTempDirectory(build,"callbacks-"); + 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"; + retain(power); + return power(2,n); + }; + """; + new CodeGen(null,lib.toString(),null,"Callbacks",src,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + CodeGen code=new CodeGen(null,null,new Ary<>(new String[]{lib.toString()}), + "Client","return Callbacks.run(arg);",123,true,TypeInteger.BOT) + .driver(CodeGen.Phase.Opto); + assertNull(code._compunits.get("Callbacks")._src); + ParmNode base=code._start.walk(n -> n instanceof ParmNode p && p._label.equals("base") ? p : null); + assertNotNull(base); + assertSame("Native code may call the saved callback with any base",TypeInteger.BOT,base._type); + code.driver(CodeGen.Phase.TypeCheck); + } + + // The client root contains no library source. The saved constructor must + // keep local loop values, and retained calls must use the library's storage. + @Test public void testImportedConstructorAndState() throws Exception { + Path fixtures=Path.of("src/test/smp/separate"); + Path build=Path.of("build/objs/separate"); + Files.createDirectories(build); + Path lib=Files.createTempDirectory(build,"library-"); + Path client=Files.createTempDirectory(build,"client-"); + new CodeGen(fixtures.resolve("library").toString(),lib.toString(),null, + "Counters",null,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + assertTrue(Files.isRegularFile(lib.resolve("Counters.o"))); + assertFalse(Files.exists(fixtures.resolve("client/Counters.smp"))); + var paths=new Ary<>(new String[]{lib.toString()}); + for( long seed : new long[]{2,13,123} ) { + CodeGen code=new CodeGen(fixtures.resolve("client").toString(),client.toString(),paths, + "Main",null,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + CompUnit imported=code._compunits.get("Counters"); + assertNotNull(imported); + assertNull("Library source must not be parsed",imported._src); + assertEquals(lib.resolve("Counters.o").toRealPath(),imported._obj.toPath().toRealPath()); + assertEquals("0",Eval2.eval(code,0)); + assertEquals("107",Eval2.eval(code,3)); + assertEquals("131",Eval2.eval(code,5)); + } + new CodeGen(fixtures.resolve("client").toString(),client.toString(),paths, + "Main",null,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + Path driver=client.resolve("driver.c"); + Files.writeString(driver,""" + #include <stdio.h> + __attribute__((CALL_CONV)) extern long long run(long long); + int main(void) { + long long args[]={0,3,5,0,3}, expected[]={0,107,231,0,307}; + for(int i=0;i<5;i++) { + long long actual=run(args[i]); + if(actual!=expected[i]) { + printf("call %d: got %lld, expected %lld\\n",i,actual,expected[i]); + return 1; + } + } + return 0; + } + """); + String exe=client.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); + TestC.linkExe(client.resolve("Main.o").toString(), + TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi",driver.toString(), + new Ary<>(new String[]{lib.resolve("Counters.o").toString()}),exe); + assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); + } +} diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Simple.java b/chapter28b/src/test/java/com/seaofnodes/simple/Simple.java index 2d688bbe..7cfdf57d 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/Simple.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/Simple.java @@ -13,7 +13,6 @@ import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.stream.Collectors; public class Simple { static void usage() { @@ -83,7 +82,11 @@ Options: static String getInput() { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); - return reader.lines().collect(Collectors.joining("\n")); + 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 { @@ -140,6 +143,9 @@ Options: 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 "-L": if (i + 1 >= args.length || args[i + 1].charAt(0) == '-') bad_usage(); libPaths.add(args[++i]); break; @@ -175,7 +181,8 @@ Options: if( modPath == null ) modPath = Path.of("."); } else { - throw Utils.TODO(); + modPath=Path.of(root); + inPath=modPath.resolve(inPath); } // Compute the module-relative paths for all sources @@ -184,7 +191,8 @@ Options: Path relPath = n==1 ? null : srcRelPath.subpath(0,n-1); // Compute source/class name String srcNameExt = srcRelPath.getName(n-1).toString(); - String srcName = srcNameExt.substring(0,srcNameExt.length()-4); + String srcName = srcRelPath.toString().replace('\\','/'); + srcName=srcName.substring(0,srcName.length()-4); // Compute the build path. Path outPath; @@ -219,15 +227,14 @@ Options: if( simpleHome != null && !simpleHome.isEmpty() ) libPaths.add(Path.of(simpleHome,cpu+"_"+abi).toString()); - // 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); } + // Keep file-backed units so discovery sees parents and siblings. + src = input_filename.isEmpty() ? getInput() : null; // Compilation pipeline Ary<String> externPaths = libPaths.isEmpty() ? null : new Ary<>(libPaths.toArray(new String[0])); CodeGen code = new CodeGen(modPath.toString(), outPath.toString(), externPaths, srcName, src, 456, true, TypeInteger.BOT ); - code.driver(Phase.LastPhase,cpu,abi, false, emit_main || do_run || do_eval, dump); + code.driver(do_eval && !do_codegen ? Phase.TypeCheck : Phase.LastPhase, + cpu,abi, false, emit_main || do_run || do_eval, dump); if( do_codegen && print_asm ) System.out.println(code.asm()); diff --git a/chapter28b/docs/examples/A_helloWorld.smp b/chapter28b/src/test/smp/examples/A_helloWorld.smp index 4ed6abdf..4ed6abdf 100644 --- a/chapter28b/docs/examples/A_helloWorld.smp +++ b/chapter28b/src/test/smp/examples/A_helloWorld.smp diff --git a/chapter28b/docs/examples/BubbleSort.smp b/chapter28b/src/test/smp/examples/BubbleSort.smp index ca6906b5..ca6906b5 100644 --- a/chapter28b/docs/examples/BubbleSort.smp +++ b/chapter28b/src/test/smp/examples/BubbleSort.smp diff --git a/chapter28b/docs/examples/Capitalize.smp b/chapter28b/src/test/smp/examples/Capitalize.smp index 8e31c18f..8e31c18f 100644 --- a/chapter28b/docs/examples/Capitalize.smp +++ b/chapter28b/src/test/smp/examples/Capitalize.smp diff --git a/chapter28b/docs/examples/Dijkstra.smp b/chapter28b/src/test/smp/examples/Dijkstra.smp index fd595eb5..fd595eb5 100644 --- a/chapter28b/docs/examples/Dijkstra.smp +++ b/chapter28b/src/test/smp/examples/Dijkstra.smp diff --git a/chapter28b/docs/examples/FileIO.smp b/chapter28b/src/test/smp/examples/FileIO.smp index e48d435b..e48d435b 100644 --- a/chapter28b/docs/examples/FileIO.smp +++ b/chapter28b/src/test/smp/examples/FileIO.smp diff --git a/chapter28b/src/test/smp/lazy/App.smp b/chapter28b/src/test/smp/lazy/App.smp new file mode 100644 index 00000000..caf7b0c1 --- /dev/null +++ b/chapter28b/src/test/smp/lazy/App.smp @@ -0,0 +1,3 @@ +val Main=Main; +val Tables=Tables; +val Stats=Stats; diff --git a/chapter28b/src/test/smp/lazy/App/Main.smp b/chapter28b/src/test/smp/lazy/App/Main.smp new file mode 100644 index 00000000..2444f21b --- /dev/null +++ b/chapter28b/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/chapter28b/src/test/smp/lazy/App/Stats.smp b/chapter28b/src/test/smp/lazy/App/Stats.smp new file mode 100644 index 00000000..e2d9e02e --- /dev/null +++ b/chapter28b/src/test/smp/lazy/App/Stats.smp @@ -0,0 +1 @@ +int builds=0; diff --git a/chapter28b/src/test/smp/lazy/App/Tables.smp b/chapter28b/src/test/smp/lazy/App/Tables.smp new file mode 100644 index 00000000..0f32a118 --- /dev/null +++ b/chapter28b/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];}; diff --git a/chapter28b/src/test/smp/separate/client/Main.smp b/chapter28b/src/test/smp/separate/client/Main.smp new file mode 100644 index 00000000..077f6575 --- /dev/null +++ b/chapter28b/src/test/smp/separate/client/Main.smp @@ -0,0 +1,6 @@ +val run={int n -> + if(!n) return 0; + val c=new Counters.Counter(n); + return c.get()+100*Counters.next_noInline(); +}; +return run(arg); diff --git a/chapter28b/src/test/smp/separate/library/Counters.smp b/chapter28b/src/test/smp/separate/library/Counters.smp new file mode 100644 index 00000000..a3e36562 --- /dev/null +++ b/chapter28b/src/test/smp/separate/library/Counters.smp @@ -0,0 +1,12 @@ +// A library can keep shared state and publish instance constructors. +int calls=0; +val next_noInline={ -> calls++; return calls; }; + +struct Counter { + val value=1; + new Counter={int n -> + value=0; + for(int i=0; i<n; i++) value=value*2+1; + }; + val get={ -> value; }; +}; diff --git a/chapter28b/docs/font-awesome-4.7.0.zip b/docs/appendices/reference-assets/font-awesome-4.7.0.zip Binary files differindex b6bdb4bd..b6bdb4bd 100644 --- a/chapter28b/docs/font-awesome-4.7.0.zip +++ b/docs/appendices/reference-assets/font-awesome-4.7.0.zip diff --git a/chapter28b/docs/ifg_trim.pdf b/docs/appendices/reference-assets/ifg_trim.pdf Binary files differindex d952fd4f..d952fd4f 100644 --- a/chapter28b/docs/ifg_trim.pdf +++ b/docs/appendices/reference-assets/ifg_trim.pdf diff --git a/chapter28b/docs/lattice.gv b/docs/appendices/reference-assets/lattice.gv index 8661082d..8661082d 100644 --- a/chapter28b/docs/lattice.gv +++ b/docs/appendices/reference-assets/lattice.gv diff --git a/chapter28b/docs/lattice.svg b/docs/appendices/reference-assets/lattice.svg index e72b473b..e72b473b 100644 --- a/chapter28b/docs/lattice.svg +++ b/docs/appendices/reference-assets/lattice.svg diff --git a/chapter28b/docs/lattice2.gv b/docs/appendices/reference-assets/lattice2.gv index 6aa37fa0..6aa37fa0 100644 --- a/chapter28b/docs/lattice2.gv +++ b/docs/appendices/reference-assets/lattice2.gv diff --git a/chapter28b/docs/lattice2.svg b/docs/appendices/reference-assets/lattice2.svg index 8b682aa7..8b682aa7 100644 --- a/chapter28b/docs/lattice2.svg +++ b/docs/appendices/reference-assets/lattice2.svg diff --git a/chapter28b/docs/pardoe.docx b/docs/appendices/reference-assets/pardoe.docx Binary files differindex 24fde921..24fde921 100644 --- a/chapter28b/docs/pardoe.docx +++ b/docs/appendices/reference-assets/pardoe.docx diff --git a/chapter28b/docs/webtcp.min.js b/docs/appendices/reference-assets/webtcp.min.js index bf23ee5a..bf23ee5a 100644 --- a/chapter28b/docs/webtcp.min.js +++ b/docs/appendices/reference-assets/webtcp.min.js diff --git a/chapter28b/ROADMAP.ja.md b/docs/roadmap.ja.md index 222f228a..0af228a5 100644 --- a/chapter28b/ROADMAP.ja.md +++ b/docs/roadmap.ja.md @@ -1,8 +1,12 @@ # ロードマップ -[English](ROADMAP.md) | 日本語 +この設計ノートは旧第25章から移したものです。初期の節には、現在の第25〜28b章で +実装済みの機能について検討した案も含みます。すべてが未実装という意味ではありません。 +現状と残る課題は[監査記録](stage-audit.md)、増分ビルドは[設計メモ](incremental-builds.md)を参照してください。 -この文書は [ROADMAP.md](ROADMAP.md) の日本語訳です。原文の設計案と未解決の問いを、そのまま設計案として記しています。 +[English](roadmap.md) | 日本語 + +この文書は [ROADMAP.md](roadmap.md) の日本語訳です。原文の設計案と未解決の問いを、そのまま設計案として記しています。 ## 分割コンパイル diff --git a/chapter28b/ROADMAP.md b/docs/roadmap.md index b5647587..aad2d9ca 100644 --- a/chapter28b/ROADMAP.md +++ b/docs/roadmap.md @@ -1,6 +1,16 @@ +# Compiler roadmap and design notebook + +This notebook preserves the design discussion from the former combined Chapter +25. Early sections include alternatives considered while implementing features +now taught in Chapters 25–28b; they are not all pending tasks. The +[stage audit](stage-audit.md) distinguishes current behavior from remaining gaps. +The [incremental-build design](incremental-builds.md) is a separate future item. +The final section records the outstanding class-initialization and compiler +follow-ups. Keep speculative language/library designs as proposals. + ## Separate Compilation -English | [日本語](ROADMAP.ja.md) +English | [日本語](roadmap.ja.md) How to handle unknown forward ref? |
