Skip to content

Initialize classes lazily per JVMS 5.5#160

Merged
dlunch merged 10 commits into
mainfrom
lazy-clinit
Jun 13, 2026
Merged

Initialize classes lazily per JVMS 5.5#160
dlunch merged 10 commits into
mainfrom
lazy-clinit

Conversation

@dlunch

@dlunch dlunch commented Jun 12, 2026

Copy link
Copy Markdown
Owner

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

  • Class gains a shared InitState machine (NotInitialized / InProgress / Initialized / Erroneous). ensure_initialized runs 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.
  • clinit now runs through 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_internal returns the registry's canonical Class (loader wrappers may build a fresh instance around an already-registered definition, which would otherwise double-run clinit — guarded by the DoubleInit fixture).
  • Failure handling per spec: non-Error exceptions are wrapped in ExceptionInInitializerError (new runtime class); Errors propagate as-is; the class becomes erroneous and later uses throw NoClassDefFoundError.

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::prepare now 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

  • E2E fixtures with expected output generated by running them on a real JVM (OpenJDK 26): 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).
  • Rust-side tests where bytecode can't observe the behavior: test_constant_value (javac inlines constants at use sites) and the existing test_putstatic.
  • Comparing against real-JVM output caught the Class.getName bug.
  • cargo test --workspace green, 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.

dlunch added 7 commits June 13, 2026 08:18
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.
Copilot AI review requested due to automatic review settings June 12, 2026 23:37
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.31707% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.08%. Comparing base (ebd9c03) to head (870a185).

Files with missing lines Patch % Lines
...lasses/java/lang/exception_in_initializer_error.rs 43.47% 13 Missing ⚠️
jvm/src/jvm.rs 78.57% 9 Missing ⚠️
jvm/src/array_class_definition.rs 0.00% 2 Missing ⚠️
java_runtime/src/classes/java/lang/throwable.rs 97.95% 1 Missing ⚠️
jvm_rust/src/class_definition.rs 96.66% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread jvm/src/jvm.rs
Comment thread jvm/src/jvm.rs
dlunch added 2 commits June 13, 2026 08:41
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-Class initialization state and trigger initialization on new, getstatic, putstatic, and invokestatic entrypoints.
  • Materialize ConstantValue attributes into static storage during a new ClassDefinition::prepare phase (including String constants).
  • Fix java.lang.Class.getName() (and related tests) to return dotted names; add ExceptionInInitializerError to 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.

Comment thread jvm/src/jvm.rs
Comment thread jvm/src/jvm.rs
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.
@dlunch dlunch merged commit 07fc404 into main Jun 13, 2026
10 checks passed
@dlunch dlunch deleted the lazy-clinit branch June 13, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants