summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-10 11:11:18 -0700
committerCliff Click <cliffc@acm.org>2026-10-24 15:56:44 -0700
commit741d78b6e840c88b7b183010c812ad982b342da1 (patch)
tree84a75eb7ad351bffd20cd5fb152bab032b74271c /docs
parent8d90d2292bbdb23596849c3c3d1cda2f6cad783e (diff)
downloadSimple-linear-chapter28a.tar.gz
Simple-linear-chapter28a.zip
Chapter 28a: Modules and Lazy Class Initializationlinear-chapter28a
Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki <jun784@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
-rw-r--r--docs/chapter28a/README.ja.md94
-rw-r--r--docs/chapter28a/README.md274
2 files changed, 368 insertions, 0 deletions
diff --git a/docs/chapter28a/README.ja.md b/docs/chapter28a/README.ja.md
new file mode 100644
index 00000000..84dcdda2
--- /dev/null
+++ b/docs/chapter28a/README.ja.md
@@ -0,0 +1,94 @@
+# 第28a章: モジュールと遅延クラス初期化
+
+[次の章: 分離コンパイル](../chapter28b/README.ja.md)
+
+[English](README.md) | 日本語
+
+[Previous: Chapter 27](../chapter27/README.ja.md) |
+[Next: Chapter 28b](../chapter28b/README.ja.md)
+
+この章では複数のソースファイルを一度にコンパイルします。各ファイルは
+一つのクラスと共有オブジェクトを定義し、トップレベルの文は隠れた
+`<clinit>` 関数になります。最初の能動的な使用で初期化し、その後は
+同じ結果を使います。クラスへの参照を保持するだけでは初期化しません。
+
+## 必要なときだけ表を作る
+
+実行できる例は [`src/test/smp/lazy`](../../src/test/smp/lazy) にあります。
+`App.smp` は Main、Tables、Stats への参照を公開します。
+`App/Stats.smp` の `builds` は初期化回数を数えます。
+`App/Tables.smp` は次のように表を構築します。
+
+```simple
+Stats.builds++;
+int[] _squares=new int[256];
+for(int i=0; i<256; i++) _squares[i]=i*i;
+val square={u8 n -> _squares[n];};
+```
+
+`App/Main.smp` が入口です。
+
+```simple
+return arg ? Tables.square(3)+Tables.square(4)+100*Stats.builds : Stats.builds;
+```
+
+引数が 0 なら結果は 0 で、Tables は初期化されません。引数が 1 なら
+9 + 16 + 100 = 125 です。二度の呼び出しでも表は一度しか作りません。
+`_squares` は非公開のフィールドで、公開関数を通して読み出します。
+完全なファイル一覧と評価用 Java コードは [英語版](README.md) を参照してください。
+
+## ファイルと解析の境界
+
+`ParseAll` は未解決のクラス名や型から必要なソースを探します。
+ディレクトリ階層が名前空間になり、`App/Main` から `Tables` は兄弟の
+`App/Tables` を指せます。親の初期化は子より先なので、親も解析します。
+相互参照する型はソースの発見後に閉じます。
+
+`CompUnit` はソース、クラス型、関数をまとめます。各単位には
+`StartCU` と `StopCU` があり、全体の Start/Stop の下で解析します。
+公開関数には未知の呼び出し元が存在します。非公開の関数は引き続き
+エスケープ解析によって特殊化できます。フィールド名の先頭の `_` は
+非公開を表します。ただし、現実装はこれを公開インターフェースの選択に使うだけで、
+兄弟クラスからの直接アクセスをまだ拒否しません。このアクセス検査は未実装です。
+
+標準ライブラリ `sys` もソースから解析します。ローカルの `sys` が
+なければ `src/main/smp` を使います。ネイティブの `"C"` 宣言は従来通り
+ネイティブリンカーで解決します。
+
+## 初期化の順序と循環
+
+クラスのフィールドの読み書き、メソッド呼び出し、インスタンス生成は
+初期化を必要とします。パーサーは `CLInit` を挿入し、最適化中は制御と
+メモリ効果を持つ不透明なノードとして残します。実行後は隠れた `$init`
+フラグが真なので、通常のメモリ最適化で重複した検査を除去できます。
+
+型やクラスへの参照の循環は合法です。しかし A の初期化が B を使い、
+B の初期化が A を使う場合、先に完了できるクラスがありません。
+`ClassInitDependencies` は最適化後の呼び出しと `CLInit` をたどり、
+複数のクラス初期化を含む循環をソース位置付きで報告します。
+通常の再帰や同じ初期化関数内の再帰は許されます。
+
+検査後、`Lower` はフラグ検査と未初期化時の呼び出しに展開します。
+親を先に初期化し、フラグは初期化関数の入口で真にします。同じクラスのヘルパーから
+再び使えるようにするためです。他クラスを経由して未完成のクラスへ戻る循環は拒否します。
+初期化関数 A が B の初期化を完了させてから再開することは可能です。
+
+## コード生成とビルド
+
+この章では必要なソースをすべて同時にコンパイルします。`FinishCalls`
+が解析用の呼び出し辺を切り離し、`StaticData` がクラスの共有領域と
+定数フィールドを配置します。ELF は発見したソースのうち生きているコードを含みますが、
+理想 IR のシリアライズ情報は含みません。
+
+リポジトリのルートで次を実行します。
+
+```sh
+make tests CHAPTERS=chapter28a
+```
+
+このディレクトリでは初回に `make lib`、続いて `make tests` を使えます。
+`make view` は共有グラフビューアーを起動します。
+
+次の章ではライブラリのソースがなくてもコンパイルできるように、型と
+理想 IR の保存、インポート時の識別子変換、インポート先を含む初期化検査
+を追加します。ライブラリ構築と最適化の調整は付録として扱います。
diff --git a/docs/chapter28a/README.md b/docs/chapter28a/README.md
new file mode 100644
index 00000000..07f34b3a
--- /dev/null
+++ b/docs/chapter28a/README.md
@@ -0,0 +1,274 @@
+# Chapter 28a: Modules and Lazy Class Initialization
+
+English | [日本語](README.ja.md)
+
+[Previous: Chapter 27](../chapter27/README.md) |
+[Next: Chapter 28b](../chapter28b/README.md)
+
+[Next: Separate Compilation](../chapter28b/README.md)
+
+
+The compiler can now discover several source files and compile them together.
+Each file defines a class with one shared class object; files can nest in
+directories forming a module structure.
+
+The shared class object is a normal object with fields holding function pointers
+or other state. The top-level code in the file forms the `<clinit>`
+function, which runs once only on the first active use of the class - i.e.,
+lazy class initialization. An initializer can trigger another class's
+initialization and wait for it to finish. It cannot observe that other class
+half-initialized. Cycles between class initializers are not allowed.
+
+This chapter adds file and class semantics to the escape analysis we already
+have. All required Simple source is available in one compilation.
+
+
+## Example: Lazy class initialization, or Pay-As-You-Go
+
+Suppose an application sometimes needs a `Table` but not often; many runs may
+never need it. Putting its construction in the application entry would make
+every run pay (the eager-init model). Putting it in a getter would need a test
+and cached storage. A class initializer provides `Table` lazily, on demand,
+and once for all users of the class. This is similar to the Java class init
+model except nothing ever sees a class mid-initialization, except the class
+itself.
+
+The runnable example is in [`src/test/smp/lazy`](../../src/test/smp/lazy).
+Its directory tree also defines the namespace:
+
+```text
+App.smp
+App/Main.smp
+App/Stats.smp
+App/Tables.smp
+```
+
+`App.smp` names its children:
+
+```simple
+val Main=Main;
+val Tables=Tables;
+val Stats=Stats;
+```
+
+These are class references being stored into normal fields, to give easy access
+to nested classes. Merely storing the reference does not trigger the class
+initializer.
+
+`App/Stats.smp` supplies a visible count:
+
+```simple
+int builds=0; // Static global counter
+```
+
+`App/Tables.smp` builds the table and exposes a function that reads it; the
+array is marked private and its public function is the intended interface
+(see the current access-check limitation below):
+
+```simple
+Stats.builds++; // Number of table builds... only ever zero or 1!
+
+// Build a private table of squares
+int[] _squares = new int[256];
+for(int i=0; i<256; i++) _squares[i]=i*i;
+
+// Public accessor to get a square
+val square = { u8 n -> _squares[n];};
+```
+
+`App/Main.smp` is the main entry:
+
+```simple
+return arg ? Tables.square(3)+Tables.square(4)+100*Stats.builds : Stats.builds;
+```
+
+With `arg == 0`, the result is 0 and `Tables` never initializes. With `arg == 1`,
+the result is 125: the two lookups produce 9 and 16, and the table is built once.
+From this chapter directory, the test driver can evaluate it:
+
+```sh
+java -ea -cp "build/classes/main:build/classes/test:lib/*" \
+ com.seaofnodes.simple.Simple --root src/test/smp/lazy --eval App/Main.smp 1
+```
+
+On Windows, use `;` instead of `:` between classpath entries.
+
+The evaluator can also run the example directly after type checking:
+
+```java
+CodeGen code=new CodeGen("src/test/smp/lazy", "build/objs", "App/Main",
+ null, 123, true, TypeInteger.BOT)
+ .driver(CodeGen.Phase.TypeCheck);
+Eval2.eval(code,0); // "0"
+Eval2.eval(code,1); // "125"; a fresh evaluation starts a fresh program
+```
+
+`BoundaryTest.testLazyTables` checks these results. `ClassInitDependenciesTest`
+also runs native code that calls a class repeatedly in one process, checking
+both the cold and already-initialized paths.
+
+### Lazy initialization details
+
+The implementation tracks a hidden `$init` variable in every class. It changes
+from `false` to `true` at initializer entry, allowing the class's own helpers
+to use it while initialization is in progress. The separate cycle check prevents
+another class's initializer from reaching back into that unfinished class.
+A check before each active use calls the initializer if needed. The optimizer
+tracks the flag as a normal alias, distinguishes `false`, `true`, and `bool`,
+and can sometimes remove the test and call.
+
+
+## Name lookup
+
+If a name misses in the file scope, the parser looks for a matching Simple
+source file under the module root. It searches the current class's children,
+then enclosing scopes. For example, in `App.smp`, the declaration
+`val Main=Main;` defines a normal field, but the right-hand `Main` is unresolved.
+The compiler finds `App/Main.smp`, which defines the referenced class.
+
+`ParseAll` starts with the requested file and follows unresolved class names
+and types to source files. From `App/Main`, `Tables` can name the sibling
+`App/Tables`; a longer name walks down the class tree. Long dotted paths are
+also allowed: `App.Tables`. The parser also reads parents, since their
+initialization must precede their children. Cyclic source references are
+allowed: the compiler closes recursive types after discovery.
+
+`CompUnit` ties a source file, class type, and function graph together. They
+form a tree that mirrors the source directories and tracks which files need
+loading and parsing. Each `CompUnit` has a `StartCU` and `StopCU`, beneath
+the whole program's `Start` and `Stop`.
+
+### Private
+
+A leading underscore marks a field or class as private. The intended rule is
+that private fields are visible only inside their declaring class, not to
+siblings or parents. A private enclosing class also keeps its children out of
+the public interface.
+
+The current compiler uses these names to choose the public interface and seed
+escape analysis, but does not yet enforce the source access rule. For example,
+a sibling can currently read `A._secret`. Rejecting that access is still needed;
+the underscore alone is not an enforced boundary in this snapshot.
+
+Like in other languages, private fields allow you to hide implementation
+details. They can be exposed via the normal getter/setter route or any other
+way that makes sense. Escape analysis can track private fields more precisely
+when all uses stay in known code, helping the usual optimizations.
+
+### Other names
+
+The bundled `sys` library uses the same source mechanism. It is a normal module
+in all ways except that lookup falls back to the bundled `src/main/smp` when
+no local `sys` is found. Native `"C"` declarations still name functions
+or data resolved by the native linker.
+
+
+## Active use and initialization order
+
+Reading or writing a class field, calling its method, or constructing an
+instance needs the class initialized; a class reference alone does not. The
+parser inserts `CLInitNode` at these active uses, and uses the usual collection
+of peepholes to optimize these away. E.g. a forward reference *might* be a
+class reference, so a `CLInit` is conservatively inserted. Later the fref
+resolves to a normal instance... so the `CLInit` will peephole away.
+`CLInit` has peeps for removing repeated checks; for telling that the check
+must always be `true` or `false`, etc.
+
+
+Parents initialize before children. In the example, `App` initializes before
+`App.Main` and `App.Tables`. Its passive references to the children do not run
+them while `App` is still being initialized.
+
+The compiler keeps `CLInit` opaque through optimization. It carries control
+and whole-memory effects, plus the fact that this class's hidden `$init` flag
+is `true` afterward. At the `Lower` pass, the `CLInit` explodes into a true
+CFG diamond guarding the `<clinit>` call. At this point we end up with a real
+runtime guard check, i.e. real overhead. Removing redundant checks reduces
+that cost; the first active use may still need a runtime check.
+
+
+## Reject cycles that would observe unfinished classes
+
+An initializer can use a different class after that class finishes initializing.
+For example, `A` can start, initialize `B`, then resume. A cycle would instead
+ask one of them to use the other before it finishes.
+
+Passive references do not create initialization dependencies. These two sibling
+files can refer to each other:
+
+```simple
+// A.smp
+val b = B;
+// B.smp
+val a = A;
+```
+
+Reading each other's fields during initialization does create a cycle:
+
+```simple
+// A.smp
+int value = B.value;
+// B.smp
+int value = A.value;
+```
+
+Parents add another dependency: a parent must finish before its child starts.
+A parent initializer therefore cannot actively use its child. It can keep a
+passive reference, or define a helper that uses the child when called after
+the parent finishes. Calling that helper during parent initialization would
+still create a cycle.
+
+The resulting dependency graph is a DAG, but initialization remains lazy.
+Execution respects the dependencies of each active use; classes that are never
+actively used need not initialize.
+
+
+After optimization and during `TypeCheck`, `ClassInitDependencies` follows
+surviving `CLInit` markers and calls. It checks strongly connected components
+and reports cycles involving more than one class initializer. Ordinary
+recursion and recursive helpers within a single initializer are allowed. Since
+this is after SCCP, eliminated code cannot make a cycle. Surviving calls are
+checked conservatively, even if a more powerful analysis could prove them dead.
+
+After checking, `Lower` expands each remaining marker into a flag test and a
+call on the cold path. Parent initialization comes first. The initializer itself
+sets the flag. Generated calls have conservative memory effects and do not
+reopen SCCP's analysis of the optimized bodies.
+
+
+## Emitting code and data
+
+After cleanup by `FinishCalls`, the live code and data from the discovered
+sources are emitted in one ELF file. Final constant fields are emitted as
+static data in the ELF using the helper `StaticData`; commonly this includes
+static class fields holding function pointers, but any constant data can be
+stored here. These fields then do not need any `<clinit>` code.
+
+Mutable field initialization remains in `<clinit>`, even if the initial value
+is a constant. A final binding whose value is computed at runtime also needs
+initializer code. For example, in `Theme.smp`:
+
+```simple
+{int} read_setting = "C"; // Supplied by the native application
+val scale = read_setting(); // Final binding; value only known at runtime
+```
+
+
+## Build and review
+
+From the repository root:
+
+```sh
+make tests CHAPTERS=chapter28a
+```
+
+Or, in this directory, `make lib` once for JUnit, then `make tests`. The suite
+covers source discovery, initialization cycles and parent order, native cold/hot
+calls, shared class storage, constructor reads and loops, private callbacks,
+and x86/ARM/RISC-V code generation. `make view` starts the shared graph viewer.
+
+The next chapter will keep this language behavior while allowing a client to
+compile without the library's sources. That requires saving types and ideal IR,
+remapping identities on import, and extending the same initialization checks
+across imported bodies. Library construction and optimizer tuning remain
+appendix topics in the [approved split](../chapter-split.md).