Idea
How the calculator’s Why view is generated
A calculator shows a number you did not expect. Is the formula wrong, or did an input you forgot about feed into it?
The Why button on the market calculator answers that. It traces each result back to the expressions, inputs, and source that produced it, then adds the values captured on this run.
Code & run Read and edit both source files
What the author writes
The market calculator has two source files. The calculation builds demand, supply, and taxed-supply lines. The configuration declares sliders, graph curves, intersections, and shaded areas. It does not contain a separately written explanation tree.
What analysis and execution each contribute
- Compile and analyze. Yichus inspects typed definitions, dependencies, and source metadata. Graph marks are compiled into calculations that the explanation system can also inspect.
- Run the calculator. The generated program computes results and captures intermediate values for the current inputs.
- Open Why. The view combines the analyzed dependencies with those values. Follow a dependency to see another calculation and its source.
The structure is derived from the program, rather than inferred from the final number. Execution supplies the values for this run. Changing an input reruns the calculation; changing the model requires recompiling it so the analyzed structure and the calculation stay together.
Try it on the market model
Open the market calculator,
change Per-unit tax, and select Deadweight loss Why?.
Expand area_between to follow the geometry calculation. The
loan calculator provides another
example with numeric output cards.
Available detail and limits
Why can expose expression dependencies and source locations where the compiler retains that information. Some function and external-runtime edges have limited detail and are marked as boundaries. The Math tab is available where the compiler can extract an equation. An explanation shows how a model computed its result; it does not establish that the model’s assumptions are correct.
Supported paths and boundary markers · Calculator compilation pipeline
Earlier UI demo: manually written value trails
The widget below predates the automatic calculator explanations. Its handler writes its trail text explicitly. It illustrates a UI technique, rather than the analysis described above.
Code & run Read the older widget’s source
Manually written trail demo
Widget Dependency Graph and Highlight State
The demo has two source values (source_a, source_b) and
a derived value (derived_sum = source_a + source_b).
This forms a dependency graph:
When you click "bump source_a," the handler changes the colors for source_a and its arrow. This is visual state chosen by the demo handler. It illustrates the declared dependency from both sources to the sum; it does not discover that path. The behavior is exercised by the generated-demo interaction test.
Hand-Composed Trail Handler
This widget writes its own trail text. The bump_a handler below computes
the sum itself and builds the trail text itself, by string
interpolation from the values it just read -- the automatic
version (where the runtime records the derivation) is what the
tax calculator and playground demos show. The code is trimmed
from the demo's shipped source,
demos/ui/explorer-traceability.bosatsu.
(Reading it: x <- e.flat_map() is Bosatsu
do-notation -- "run e, bind its result to
x"; the continuation lambda is supplied by the
notation, so flat_map() appears with no written
argument. capture(name, value) is an IO primitive
that tags a value with a label and hands it back.)
bump_a: IO[Unit] = (
# 1. Read the current sources
sa <- source_a.flat_map()
sb <- source_b.flat_map()
total <- derived_sum.flat_map()
current_a <- sa.read().flat_map()
current_b <- sb.read().flat_map()
next_a = current_a.add(1)
# 2. Tag the values at capture points
captured_a <- capture("source_a", next_a).flat_map()
captured_b <- capture("source_b", current_b).flat_map()
pair = SourcePair(captured_a, captured_b)
new_sum = compute_sum(pair) # adds the pair's two fields
tagged_sum <- capture_formula("sum",
update_formula(captured_a, captured_b, new_sum), new_sum).flat_map()
# 3. Update values and visual highlights
_ <- sa.write(captured_a).flat_map()
_ <- total.write(tagged_sum).flat_map()
# (color constants: lit_color = "#fbbf24" gold,
# sum_lit = "#22c55e" green)
_ <- set_highlight(lit_color, purple_box, sum_lit, lit_color, dim_color).flat_map()
# 4. Build the trail line -- the handler interpolates it itself.
# (The formula and Why? writes that follow are trimmed here; they
# use the same interpolation pattern.)
tt <- trail_text.flat_map()
tt.write("source_a changed: ${int_to_String(current_a)} -> ${int_to_String(captured_a)} | sum recomputed: ${int_to_String(current_a.add(current_b))} -> ${int_to_String(tagged_sum)}")
)
Takeaway: this handler reads values, computes the next sum, and writes both display values and its latest trail, formula, and Why? text. The generated runtime routes states through direct bindings or a declared render route; a missing route fails loudly. Inspect the runtime for that dispatch contract and the demo source for the writes this handler performs.
Trail Text Produced by the Handler
Below the graph, a dark panel shows the latest provenance update: what changed, what was the old value, what is the new value, and which derived value was recomputed.
After clicking bump source_a twice, the panel retains only the second click's interpolated string:
"source_a changed: 11 -> 12 | sum recomputed: 14 -> 15"
The "Why?" panel explains the current value in terms of its sources -- this text is also composed by the handler, in the same hand-wired way:
"sum = 15 because source_a(12) + source_b(3) -- triggered by bump_a"
Takeaway: the latest line makes the widget's update easy to follow,
but its completeness comes from the author of this handler.
To restore the initial values and prompt, reload this page; that
also reloads the embedded demo. The widget has no reset control.
For an automatically generated derivation, open an output's
Why? panel in the
tax calculator;
its implementation lives in
WhyExplainer.scala.
Style Bindings for Dependency Highlights
The graph boxes use style-background bindings.
Each box has its own State[String] holding its
background color; when bump_a fires, it writes
"#fbbf24" (gold) to the source_a box's state and "#22c55e"
(green) to the sum box's. Note the types below:
source_box takes a plain String. The
view reads the state and passes the current string; because
that argument was read from a state cell, the compiler binds
the style-background property to that cell, so
later writes hit the DOM directly.
box_a_bg: IO[State[String]] = state("#3b82f6") # one cell per box
# in main: bind the cell, read it, pass the current value
# (a_val is source_a's value, read the same way)
ba <- box_a_bg.flat_map()
a_bg <- ba.read().flat_map()
# ... source_box("source_a", int_to_String(a_val), a_bg)
def source_box(label: String, value_str: String, bg: String) -> VNode:
h("div", [
("style-background", bg) # compiler binds this to box_a_bg
], [text(value_str)])
Takeaway: each background cell is a scalar style binding, so its generated route targets the corresponding DOM property. The binding analyzer extracts that route, and the runtime applies it.
Style-binding dispatch scope
Static Connectivity and Automatic Why? Surfaces
| Surface | Reported technical content |
|---|---|
| Explorer trace | Typed dependency and influence edges upstream from a selected binding |
| Automatic Why? | Compiler-derived output dependencies plus runtime capture values available to the simulation |
| Input-ignore checks | Structural signals such as unused parameters and literal-only result ancestry |
| What-if controls | Recomputed simulation outputs after a declared input changes |
| Generated UI diagnostics | Binding-map hits and misses on the browser runtime path |
The Explorer's static connectivity reports and the simulation's automatic Why? panels are separate surfaces with separate evidence. The fact tools explain the static facts. To inspect automatic Why?, click any output in the tax calculator, or run the playground and inspect a result's Why? trace.