7.5. Widgets
Suppose a verifier returns a much wider interval than expected. The value you need is already in Lean, but it is buried in a graph record, a vector of node states, and several shape tags. Printing the entire term would answer everything except the question you actually have: at which node did the bound become wide?
That is the job of a widget. It renders an existing Lean object in the Infoview so that its relevant structure is visible: tensor entries, graph parents, inferred shapes, Float32 fields, affine bounds, tape gradients, or metric curves. It does not give that object a second meaning, and a convincing picture is not a theorem.
The widget modules are collected by
NN.Widgets. Import that module when a scratch file needs the inspection tools together; the
broader import NN also brings them into a larger development.
7.5.1. An Inspection Loop
Start with the question, not with the widget name. If the question is “why is this output wrong?”, inspect the graph and then its evaluation trace. If it is “why is verification inconclusive?”, look at interval widths. If training stopped moving, compare the tape, accumulated gradients, and metric log before changing the optimizer.
A productive session has four small moves:
-
Keep the value that matters as a named Lean definition or as an artifact at an explicit path.
-
Run the evaluator or checker whose semantics the eventual claim uses.
-
Put the corresponding widget beside that value and find the first surprising node, field, or step.
-
Change the definition or producer, elaborate again, and let the checker close the argument instead of trusting the picture.
Read-only views such as #tensor_view format a value without changing it. Trace views execute a
specific Lean computation: #ir_exec_trace_view steps through NN.IR.Semantics, while
#tape_trace_view follows the autograd engine's reverse pass. The interpretation is therefore
explicit. A theorem about that same interpretation is still a separate proof.
Here is the short lookup table; the sections below turn each row into a working example.
When the question is about… | Start with | What it exposes |
|---|---|---|
tensor values or representation |
| entries, shape, summaries, or binary32 fields |
graph structure or execution |
| parents, declared/inferred shapes, and intermediate values |
a rewrite |
| source and result graphs side by side |
verifier precision |
| node bounds, affine state, and interval widths |
gradients |
| tape structure, reverse steps, and accumulated gradients |
a completed run |
| metrics, notes, prompts, samples, policies, or transitions |
a PyTorch sketch |
| an editor-assistant translation, not an importer proof |
7.5.2. How Widgets Fit Application Workflows
Some values live only while a file elaborates; others are written by a command and inspected later. The distinction matters. A graph or tape definition can sit immediately above its view. A GPT or PPO run instead writes a named artifact, which a later Lean file reads:
#train_log_file_view "data/model_zoo/cnn_trainlog.json" #gpt2_train_log_file_view "data/model_zoo/gpt2_trainlog.json" #rl_boundary_rollout_file_view "data/rl/cartpole_rollout.json", contract, 12 #pytorch_translate_file "NN/Examples/Quickstart/pytorch_translator_mlp.py"
Keep the producer command beside a file-backed view in a comment or experiment note. The renderer can tell you what the file says, but it cannot recover where the file came from. When the artifact has a checker, check it first and then inspect the accepted object.
7.5.3. Tensor Viewer
Begin with values whose layout is unmistakable. rankThreeGrid writes its three indices into the
hundreds, tens, and units places, so a transposed or flattened axis is visible at a glance. The two
floating-point vectors then show a different problem: decimal printing may look the same even when
the underlying scalar semantics differ.
open Spec
open TorchLean.Floats.IEEE754
def decimalTenth : Float :=
Float.ofBits 0x3fb999999999999a
def oneThirdFloat : Float :=
Float.ofBits 0x3fd5555555555555
def floatVector : Tensor Float (shape![4]) :=
Tensor.dim (fun
| ⟨0, _⟩ => Tensor.scalar (Float.ofNat 1)
| ⟨1, _⟩ => Tensor.scalar (Float.ofNat 2)
| ⟨2, _⟩ => Tensor.scalar decimalTenth
| ⟨_, _⟩ => Tensor.scalar oneThirdFloat)
def ieeeVector : Tensor IEEE32Exec (shape![4]) :=
Tensor.dim (fun
| ⟨0, _⟩ => Tensor.scalar IEEE32Exec.posOne
| ⟨1, _⟩ =>
Tensor.scalar (IEEE32Exec.ofFloat (Float.ofNat 2))
| ⟨2, _⟩ => Tensor.scalar (IEEE32Exec.ofFloat decimalTenth)
| ⟨_, _⟩ => Tensor.scalar (IEEE32Exec.ofFloat oneThirdFloat))
def indexVector : Tensor Nat (shape![5]) :=
Tensor.dim (fun i => Tensor.scalar i.1)
def rankThreeGrid :
Tensor Nat (shape![2, 3, 4]) :=
Tensor.dim (fun i =>
Tensor.dim (fun j =>
Tensor.dim (fun k =>
Tensor.scalar (i.1 * 100 + j.1 * 10 + k.1))))
def sampleMatrix : Tensor Int (shape![2, 4]) :=
Tensor.dim (fun i =>
Tensor.dim (fun j =>
Tensor.scalar (Int.ofNat (i.1 * 10 + j.1))))
#tensor_view indexVector
#tensor_view rankThreeGrid
#tensor_view floatVector
#tensor_view ieeeVector
#tensor_view sampleMatrix
-- Numeric summaries for the small tensors above:
#tensor_stats_view floatVector
7.5.4. IR Graph Viewer
The IR widget family answers the three debugging questions that show up in practice:
-
Structure: which nodes, parents, and shapes are present?
-
Invariants: do declared node shapes match what the ops infer from parent shapes?
-
Semantics: when the graph is evaluated, which node fails first and what are the intermediate values?
open NN.IR
open Spec
def pairTensor (x y : Float) : Tensor Float (shape![2]) :=
Tensor.dim (fun
| ⟨0, _⟩ => Tensor.scalar x
| ⟨_, _⟩ => Tensor.scalar y)
def sampleGraph : Graph :=
{ nodes := #[
{ id := 0, parents := [], kind := .input
outShape := (shape![2]) },
{ id := 1, parents := []
kind := .const (shape![2])
outShape := (shape![2]) },
{ id := 2, parents := [0, 1], kind := .add
outShape := (shape![2]) }
] }
def sampleGraphSub : Graph :=
-- Same as `sampleGraph`, but uses `sub` instead of `add`.
-- (Useful for rewrite/diff examples.)
{ nodes := #[
{ id := 0, parents := [], kind := .input
outShape := (shape![2]) },
{ id := 1, parents := []
kind := .const (shape![2])
outShape := (shape![2]) },
{ id := 2, parents := [0, 1], kind := .sub
outShape := (shape![2]) }
] }
#ir_view sampleGraph
-- 1) Invariant check:
-- declared shape tags vs inferred shapes.
#shape_infer_view sampleGraph
-- 2) Before/after view:
-- handy for compiler/optimizer passes.
#graph_rewrite_view sampleGraph, sampleGraphSub
-- 3) Evaluation trace: run the IR semantics step by step.
-- For `.const` nodes, a small external payload is supplied.
def sampleInput : Runtime.AnyTensor Float :=
{ s := (shape![2]), t := pairTensor 0.60 (-0.20) }
def samplePayload : NN.IR.Payload Float :=
{ const? := fun id =>
if id = 1 then
some { n := 2, v := pairTensor 0.25 0.25 }
else
none }
#ir_exec_trace_view sampleGraph, samplePayload, sampleInput
Read these three views in order. The graph view says node 2 depends on the input and constant. The
shape view checks that all three declared length-two vectors agree with inference. The trace should
then end at [0.85, 0.05], because it adds [0.25, 0.25] to [0.60, -0.20]. If the final value is
wrong, the trace gives you the first intermediate value to compare; if elaboration never reaches
the trace, the shape view gives you the structural failure instead.
7.5.5. Float32 Bit Layout Viewer
namespace Float32Demo def one32 : IEEE32Exec := IEEE32Exec.ofBits (0x3f800000 : UInt32) def qnan32 : IEEE32Exec := IEEE32Exec.ofBits (0x7fc00000 : UInt32) #float32_view one32 #float32_view (1 : IEEE32Exec) #float32_view qnan32 #float32_compare_view one32, qnan32 -- Compare a Float64 input to its Float32 rounding: #float32_round_view decimalTenth #float32_round_view oneThirdFloat end Float32Demo
one32 should show sign 0, exponent field 127, and a zero fraction. The quiet NaN has the
all-ones exponent and a nonzero fraction, so the classification remains visible even though it has
no ordinary real value. The round views answer a separate question: which binary32 bit pattern is
chosen when a binary64 Float such as decimal one tenth crosses the scalar boundary?
7.5.6. Verification (IBP/CROWN State)
TorchLean's verification code includes executable bound propagation engines: IBP boxes and CROWN affine forms. When debugging a verifier, inspect which nodes have bounds and whether shapes and flattened dimensions match the intended layout.
open NN.IR
open Spec
def sampleGraphCROWN : NN.IR.Graph :=
{ nodes := #[
{ id := 0, parents := [], kind := .input
outShape := (shape![2]) },
{ id := 1, parents := []
kind := .const (shape![2])
outShape := (shape![2]) },
{ id := 2, parents := [0, 1], kind := .add
outShape := (shape![2]) }
] }
def samplePropState :
NN.MLTheory.CROWN.Graph.PropState Float :=
let bIn : NN.MLTheory.CROWN.FlatBox Float :=
{ dim := 2
lo := pairTensor (-1.0) (-1.0)
hi := pairTensor (1.0) (1.0) }
let bConst : NN.MLTheory.CROWN.FlatBox Float :=
{ dim := 2
lo := pairTensor (0.25) (0.25)
hi := pairTensor (0.25) (0.25) }
let bOut : NN.MLTheory.CROWN.FlatBox Float :=
{ dim := 2
lo := pairTensor (-0.75) (-0.75)
hi := pairTensor (1.25) (1.25) }
{ inputId := 0
inputDim := 2
states := #[
{ shape := (shape![2])
ibp? := some bIn
aff? := none }
, { shape := (shape![2])
ibp? := some bConst
aff? := none }
, { shape := (shape![2])
ibp? := some bOut
aff? := none }
] }
#crown_view sampleGraphCROWN, samplePropState
-- Interval widths (`hi - lo`) are a fast
-- "where did bounds blow up?" diagnostic.
#bounds_tightness_view sampleGraphCROWN, samplePropState
For this graph, adding the exact constant 0.25 shifts [-1,1] to [-0.75,1.25] without
changing the interval width. The tightness view should therefore report width two at both the input
and output. In a larger network, the first unexpected jump in width is usually more informative
than the final loose bound.
7.5.7. Autograd (Tape + Gradients)
TorchLean's eager autograd engine records a computation graph into a Tape and can run
reverse-mode to accumulate gradients. The widget below shows the recorded tape and the gradients
produced by scalar backprop (like loss.backward() in PyTorch).
open Runtime.Autograd
open Spec
def sampleTape : Tape Float :=
let (t0, aId) :=
Tape.leaf (α := Float) (t := Tape.empty)
(value := Tensor.scalar 2.0) (name := some "a")
let (t1, bId) :=
Tape.leaf (α := Float) (t := t0)
(value := Tensor.scalar 3.0) (name := some "b")
let (t2, abId) :=
match Tape.mul (α := Float) (t := t1)
(s := Shape.scalar) aId bId with
| .ok r => r
| .error _ => (t1, 0)
let (t3, outId) :=
match Tape.add (α := Float) (t := t2)
(s := Shape.scalar) abId bId with
| .ok r => r
| .error _ => (t2, 0)
let _ := outId
t3
#tape_grads_view sampleTape, 3
-- For a step by step explanation of why a grad exists (or is missing),
-- use the step by step reverse pass trace:
#tape_trace_view sampleTape, 3
The recorded scalar is ab+b at a=2 and b=3, so the value is nine and both derivatives are
three. That closed form gives the trace a human-sized oracle: if a gradient is absent or differs,
inspect the first reverse step where the contribution from multiplication or addition failed to
arrive.
7.5.8. Training Dashboards
TorchLean's widget layer is not limited to semantic objects like tensors and tapes. It also includes a small monitoring API for training and evaluation artifacts.
These logs are plain data structures, not a hidden runtime UI:
-
Runtime.Training.TrainLogis a pure record of steps, metric series, and notes, -
Runtime.Training.ConfusionMatrixis a pure table of class counts, -
the widget layer renders them without changing their meaning.
That makes them suitable for pure Lean small runs, runtime/autograd training loops, and imported metrics from external experiments, all rendered through the same viewer.
def sampleTrainLog : _root_.Runtime.Training.TrainLog :=
{ title := "Classifier training run"
steps := #[0, 1, 2, 3, 4]
series := #[
{ name := "loss", values := #[1.20, 0.84, 0.59, 0.41, 0.33], color := "#c44" }
, { name := "val_acc", values := #[0.30, 0.48, 0.61, 0.73, 0.79], color := "#0a7" }
, { name := "lr", values := #[0.05, 0.05, 0.01, 0.01, 0.01], color := "#06c" }
]
notes := #[
"optimizer: SGD"
, "scheduler: StepLR(step_size=2, gamma=0.2)"
, "dataset: synthetic 3-class classifier"
] }
def sampleLabels : Array String := #["cat", "dog", "owl"]
def sampleCM : _root_.Runtime.Training.ConfusionMatrix :=
{ counts := #[
#[8, 1, 0]
, #[2, 6, 1]
, #[0, 1, 7]
] }
#train_log_view sampleTrainLog
#confusion_view sampleLabels, sampleCM
This widget family pairs particularly well with:
-
the CSV loader training example,
-
the NPY loader training example,
-
the CNN and ViT model commands,
-
and the callback/reporting helpers exposed through
Trainerreports.
7.5.9. Runtime Context Viewer
When debugging a failed training step, one of the first questions is often not "what is the graph?" but "which values and gradients are registered now?"
The runtime-context widget answers that question directly.
def anyScalar (x : Float) : Runtime.AnyTensor Float :=
{ s := .scalar, t := Tensor.scalar x }
def sampleCtx : Runtime.RuntimeContext Float :=
{ var_registry := [
("w", anyScalar 3.0)
, ("x", anyScalar 2.0)
, ("wx", anyScalar 6.0)
]
gradients := [
("w", anyScalar 2.0)
, ("x", anyScalar 3.0)
]
next_id := 3 }
#runtime_ctx_view sampleCtx
This view is good for comparing:
-
the training API,
-
the eager autograd tape,
-
and the actual runtime registry that stores values and accumulated gradients.
7.5.10. GPT And Text-Model Logs
GPT-style examples write normal TrainLog artifacts, but prompt/sample notes benefit from a
specialized renderer:
#gpt2_train_log_file_view "data/model_zoo/gpt2_trainlog.json" #gpt2_prompt_view "ROMEO:"
The file view is the one to use in documentation because it renders an artifact that already exists. The prompt view can run a small command from the editor, which is convenient for demos but should be described as execution, not passive inspection.
7.5.11. RL Boundary And Policy Views
RL widgets are about the part of training that scalar reward curves hide:
-
the checked transition boundary for Gymnasium rollouts,
-
GridWorld policies and paths,
-
PPO rollout curves derived from reward/value/advantage data.
The main entry files are:
Use them when the question is "what exactly entered the learner?" or "what policy/path artifact did the command write?" Use the proof layer when the question is a theorem about the MDP or boundary.
7.5.12. PyTorch Translator Widget
The PyTorch translator widget is an editor aid, not the checked importer:
#pytorch_translate_file "NN/Examples/Quickstart/pytorch_translator_mlp.py"
It helps readers see how a simple torch.nn snippet maps onto TorchLean constructors. Checked
interop claims should cite the explicit PyTorch roundtrip/export examples and the artifact bridge,
not the heuristic widget alone.
7.5.13. Work Through The Maintained Example
Open NN/Examples/DeepDives/Widgets.lean in VS Code with the Lean extension enabled. Put the cursor on each widget command and open the Infoview. The file is deliberately small enough to elaborate interactively.
Work through four changes:
-
Change one entry of a tensor and confirm that
#tensor_viewand#tensor_stats_viewupdate from the same Lean value. -
Give an IR node the wrong declared output shape.
#shape_infer_viewshould identify the first disagreement; restore the shape before continuing. -
Replace
sampleGraphbysampleGraphSubin the rewrite view and inspect the changed operation tag rather than comparing raw record syntax. -
Compare
IEEE32Exec.posOnewith a quiet NaN. The bit viewer should expose the exponent, fraction, and classification that ordinary decimal printing hides.
The file can also be elaborated from a terminal:
lake env lean NN/Examples/DeepDives/Widgets.lean
Terminal elaboration checks the commands and definitions, while the VS Code Infoview provides the
interactive rendered panels. If a widget fails to elaborate, first distinguish a malformed Lean
object from a rendering problem: replace the widget command by #check on the same object, then
reintroduce the view after its type is correct.
7.5.13.1. Other Widget Sources
The focused examples are useful when one artifact is the whole subject:
-
Float32 modes pairs
#float32_view,#float32_compare_view, and#float32_round_viewwith executable binary32 values. -
CROWN workflow and IBP workflow provide real verifier states for
#crown_viewand#bounds_tightness_view. -
Autograd basics supplies a tape for
#tape_view,#tape_grads_view, and#tape_trace_view.
7.5.13.2. File-Backed Views
Training and application commands can write artifacts that outlive the Lean process. Use
#train_log_file_view or #gpt2_train_log_file_view for metric and text-generation logs, and
#rl_boundary_rollout_file_view for checked transition records. The
CSV loader example,
GPT widget source,
and RL rollout view
show the corresponding producers.
#runtime_ctx_view is different: it renders the live runtime registry and accumulated gradients.
Use it when a training step failed before writing a log and the question is which variables reached
the runtime context.
File-backed views parse and render the artifact at the named path. A successful visualization does not authenticate its producer or strengthen the artifact's checker claim. When validity matters, run the corresponding checker first and use the widget to inspect the same accepted file.
7.5.14. Keep The Object Close
A widget is most useful when it sits beside the definition that produced its input. Keep the graph view beside the graph, the bounds view beside the verifier state, and the training plot beside the file parser. Then a surprising picture has an immediate Lean object to inspect in the Infoview.