Initialize classes lazily per JVMS 5.5#160
Conversation
ensure_initialized guards instantiate/getstatic/putstatic/invokestatic behind an InitState machine with InProgress re-entry, runs clinit through execute_method so it gets a java frame, and canonicalizes resolve results to the registry Class so init state is shared. Registration still initializes eagerly; the lazy flip comes separately.
Registration no longer runs clinit; initialization happens at new, getstatic, putstatic, and invokestatic per JVMS 5.5, superclass first, and interfaces only on access to their own static fields.
Non-Error exceptions from clinit are wrapped per JVMS 5.5; Errors propagate as-is. The failed class becomes erroneous and later uses throw NoClassDefFoundError.
StaticOrder checks that an ldc class literal resolves without initializing, and DoubleInit guards against double clinit through the on-demand class loading path.
javac emits compile-time constants as ConstantValue attributes with no clinit code, so they previously read as zero. ClassDefinition::prepare materializes them into static storage before initialization.
LazyClinit and ClinitFailure cover the trigger, ordering, interface, re-entrancy, and failure scenarios; expected outputs are generated by running the fixtures on a real JVM. This caught Class.getName returning the internal slash form instead of the dotted binary name, now fixed. Constants and StaticFlag stay as Rust-side tests: javac inlines compile-time constants so ConstantValue is unobservable from bytecode, and StaticFlag verifies typed reads from native code.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #160 +/- ##
==========================================
+ Coverage 80.53% 81.08% +0.55%
==========================================
Files 183 184 +1
Lines 8018 8199 +181
==========================================
+ Hits 6457 6648 +191
+ Misses 1561 1551 -10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 99eff87040
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Constants and StaticFlag move to test_data with mains and expected output generated by a real JVM. ConstantsReader is compiled against a non-final Constants so javac emits getstatic instead of inlining, keeping ConstantValue preparation observable from bytecode. The Rust-side putstatic typed-read assertion is dropped with test_data/unit; the narrowing fix itself is still exercised by StaticFlag's clinit.
There was a problem hiding this comment.
Pull request overview
This PR updates the JVM’s class initialization behavior to be spec-aligned (lazy <clinit> on first active use), fixes handling of ConstantValue static finals during preparation, and corrects Class.getName() to return dotted binary names.
Changes:
- Add per-
Classinitialization state and trigger initialization onnew,getstatic,putstatic, andinvokestaticentrypoints. - Materialize
ConstantValueattributes into static storage during a newClassDefinition::preparephase (including String constants). - Fix
java.lang.Class.getName()(and related tests) to return dotted names; addExceptionInInitializerErrorto the runtime.
Reviewed changes
Copilot reviewed 26 out of 46 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_constant_value.rs | Adds a Rust-side unit test asserting ConstantValue statics are visible via get_static_field. |
| test_data/unit/Constants.java | Adds a Java source fixture for constant-value fields. |
| test_data/StaticOrder.txt | Adds expected output fixture for class-literal vs initialization order behavior. |
| test_data/StaticOrder.java | Adds Java fixture verifying Helper.class doesn’t initialize Helper until active use. |
| test_data/LazyClinit.txt | Adds expected output fixture for lazy initialization triggers and ordering. |
| test_data/LazyClinit.java | Adds Java fixture covering the four init triggers, ordering, and self-reference behavior. |
| test_data/DoubleInit.txt | Adds expected output fixture ensuring <clinit> runs once. |
| test_data/DoubleInit.java | Adds Java fixture to catch double-initialization via repeated active use. |
| test_data/ClinitFailure.txt | Adds expected output fixture for initializer failure behavior. |
| test_data/ClinitFailure.java | Adds Java fixture covering ExceptionInInitializerError and subsequent NoClassDefFoundError behavior. |
| jvm/src/jvm.rs | Implements lazy init via ensure_initialized, integrates it into object creation/static field access/static calls, and avoids double init via registry canonicalization. |
| jvm/src/class_loader.rs | Introduces InitState and stores it on Class instances so init state can be shared. |
| jvm/src/class_definition.rs | Extends the ClassDefinition trait with a prepare hook. |
| jvm/src/array_class_definition.rs | Implements a no-op prepare for array class definitions. |
| jvm_rust/src/class_definition.rs | Parses ConstantValue attributes and implements prepare to materialize constants into static storage. |
| java_runtime/tests/classes/java/lang/test_throwable.rs | Updates expectations to match dotted getName() output in Throwable::toString / stack trace headers. |
| java_runtime/src/loader.rs | Registers ExceptionInInitializerError as a runtime-provided class. |
| java_runtime/src/classes/java/lang/exception_in_initializer_error.rs | Adds a minimal ExceptionInInitializerError runtime class and constructors used by the VM. |
| java_runtime/src/classes/java/lang/class.rs | Fixes Class.getName() to return dotted binary names. |
| java_runtime/src/classes/java/lang.rs | Wires the new runtime class module into the java.lang exports. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Throwable gets a cause field, getCause/initCause, and the (Throwable) and (String, Throwable) constructors, mirrored on Exception and RuntimeException. printStackTrace now walks the cause chain printing "Caused by:". ExceptionInInitializerError stores the original exception as its cause (no detail message, matching the JDK) and clinit wrapping wires it through that constructor instead of flattening the cause into the message string.
Summary
Moves class initialization (
<clinit>) from eager (at registration) to the spec-mandated first active use, and fixes two related issues found along the way.Lazy initialization
Classgains a sharedInitStatemachine (NotInitialized / InProgress / Initialized / Erroneous).ensure_initializedruns at the four JVMS §5.5 triggers —new,getstatic,putstatic,invokestatic— initializing the superclass chain first; interfaces initialize only on access to their own static fields. InProgress re-entry returns immediately, so a clinit touching its own statics works.execute_method, so it gets a java frame: class resolution inside clinit uses the initializing class's loader context, clinit shows up in stack traces, and objects it allocates are rooted to its frame.resolve_class_internalreturns the registry's canonicalClass(loader wrappers may build a fresh instance around an already-registered definition, which would otherwise double-run clinit — guarded by the DoubleInit fixture).ExceptionInInitializerError(new runtime class); Errors propagate as-is; the class becomes erroneous and later uses throwNoClassDefFoundError.ConstantValue (static final compile-time constants)
javac emits compile-time constants as ConstantValue attributes with no clinit code, so they previously read as zero/null.
ClassDefinition::preparenow materializes them into static storage during preparation, including String constants.Class.getName
Returned the internal slash form (
java/lang/X); now returns the dotted binary name. Two existing tests had encoded the buggy form and were corrected.Test plan
LazyClinit(all four triggers, superclass-before-subclass ordering, interface non-initialization on implementor instantiation, clinit re-entrancy),ClinitFailure(ExceptionInInitializerError → NoClassDefFoundError, Error unwrapped),StaticOrder(ldc class literal resolves without initializing — verified to fail on the pre-change commit),DoubleInit(no double clinit through on-demand class loading).test_constant_value(javac inlines constants at use sites) and the existingtest_putstatic.Class.getNamebug.cargo test --workspacegreen, fmt clean, clippy no new warnings.Design docs from the forge pipeline (PRD/architecture/review) are kept locally under docs/ and not part of this PR.