Skip to content

Adding an import in a later cell nullifies variables defined in earlier cells #119

@andrus

Description

@andrus

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:

  1. Detecting when a re-import causes variable-holding snippets to be re-created, and automatically re-running their initializers; or
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions