-
Notifications
You must be signed in to change notification settings - Fork 8
Adding an import in a later cell nullifies variables defined in earlier cells #119
Description
Summary
Generated by Claude
When a variable is defined in one cell and then new import statements are evaluated in a subsequent cell, the variable's value is reset to null. Any code in later cells that references that variable then fails with a NullPointerException. Combining all imports into a single upfront cell avoids the problem, but that is an unreasonable constraint for interactive notebook use.
Steps to Reproduce
Run the following cells in order:
Cell 1 — initial import
import io.bootique.*;Cell 2 — define a variable
var bq = Bootique.app("-c", "my.yml")
.autoLoadModules()
.createRuntime();Cell 3 — add more imports in a new cell
import io.bootique.config.jackson.*;
import io.bootique.config.*;Cell 4 — use the variable defined in Cell 2
JsonConfigurationFactory cf =
(JsonConfigurationFactory) bq.getInstance(ConfigurationFactory.class);Actual Behavior
Cell 4 throws a NullPointerException because bq is null:
java.lang.RuntimeException: java.lang.NullPointerException, Cannot invoke
"io.bootique.BQRuntime.getInstance(java.lang.Class)" because "REPL.$JShell$30B.bq" is null
| at org.dflib.jjava.kernel.execution.CodeEvaluator.evalSingle(CodeEvaluator.java:145)
| ...
| Caused by: jdk.jshell.EvalException: Cannot invoke
"io.bootique.BQRuntime.getInstance(java.lang.Class)" because "REPL.$JShell$30B.bq" is null
| at .(<Anonymous>#35:1)
The internal REPL class name ($JShell$30B.bq) reveals that JShell has re-created the snippet class for bq's enclosing context, leaving the field uninitialized.
Expected Behavior
Adding imports after a variable has been defined should not affect the value of that variable. The notebook's interactive contract is that each cell runs in the existing state; importing new classes is purely additive and should not invalidate prior bindings.
Workaround
Placing all import statements in a single cell before defining any variables avoids the issue:
// Cell 1 — all imports together
import io.bootique.*;
import io.bootique.config.jackson.*;
import io.bootique.config.*;// Cell 2 — variable definition (safe now)
var bq = Bootique.app("-c", "my.yml")
.autoLoadModules()
.createRuntime();This works, but is impractical for real notebook workflows where imports are naturally discovered and added incrementally.
Root Cause (hypothesis)
JShell re-evaluates all dependent snippets when a new import is added. This re-evaluation replaces the class holding the bq field without re-running the initialization expression, so the field reverts to its default value (null).
JJava may be able to mitigate this by:
- Detecting when a re-import causes variable-holding snippets to be re-created, and automatically re-running their initializers; or
- Issuing a clear warning to the user that earlier variables have been invalidated and need to be re-evaluated.
The JShell API exposes snippet status events (SnippetEvent) that could be used to detect this situation.
Impact
This is a significant usability issue for any notebook that builds up state progressively — which is the primary use case for a Jupyter kernel. The failure is silent (no warning is shown when bq becomes null), making the bug very confusing to diagnose.