Skip to content
B-Lambda HelpBlock-Based λ-Calculus v1.2.0
05 / Semantics

Reduction you can drive by hand

B-Lambda evaluates every program three times, by three independent methods, and then checks that they agree. This chapter is about the four views that let you watch that happen — and about the one number that legitimately differs between them.

The Semantics dock

Open the bottom panel with Ctrl + J and choose Semantics. Four tabs — and the dock's own tab relabels itself to whichever you are on, so it reads Semantics · Lockstep rather than a bare Semantics:

Tab Shows
Call-by-Structure Substitution reduction under the default strategy, drawn as blocks
Call-by-Value The same, with arguments reduced before they are substituted
CSEK machine An environment machine: control, structure, environment, kontinuation
Lockstep Substitution, the CSEK machine and the register VM, side by side
The trace views need a redex

A reduction trace is always of something. The Call-by-Structure and Call-by-Value tabs stay empty until you pick one, and prompt you: “Right-click an application block and choose Call-by-Structure or Call-by-Value.” The context menu on any application block carries Evaluate – Call-by-Structure, Evaluate – Call-by-Value and Show Type and Value — the last of which opens the Block Info dialog, whose value section is headed Value (Call-by-Structure) so you always know which strategy produced the number you are reading. The CSEK and Lockstep tabs instead have their own Load button and run the whole program.

Call-by-Structure

Call-by-Structure (CbS) is B-Lambda's default evaluation strategy, as it is in MnL. It is the executable form of the λVis calculus, and the single idea behind it is this: a value does not have to arrive stripped of the expression that produced it.

CbS is strict in its argument. It evaluates the argument first, exactly as Call-by-Value does, and exactly once. What differs is what gets bound. CbV binds the bare value. CbS binds a structured value — the computed value paired with the structure it came from, written Vp ⊗ Vv:

(λx. x + x) (3 * 7)

CbS:  evaluate the argument once  →  21, retaining (3 * 7)
      substitute the PAIR         →  (21 ⊗ 3 * 7) + (21 ⊗ 3 * 7)  →  42
      the multiplication happens ONCE; both sites SHOW 3 * 7

CbV:  evaluate the argument once  →  21
      substitute the bare value   →  21 + 21  →  42
      the multiplication happens ONCE; the 3 * 7 is gone

Both reach 42, and both do the same amount of arithmetic. The difference is not work, it is carriage. Under CbS the argument's original structure is still legible at every place the parameter was used, so you can read off where a number came from without re-deriving it. That is what the Copy vs Lookup (CbS ⇄ CbV) example is built to show.

Copies are shown, not recomputed

This is the point most easily misread. Seeing 3 * 7 twice in a CbS trace does not mean the multiplication ran twice. It ran once; both occurrences are the same retained structure being displayed. CbS costs no extra reductions over CbV — it only keeps more of the derivation visible.

An argument that is already a value — a number, a boolean, or a λ — has nothing to retain, so it binds bare with no at all. The pair only appears when the argument actually had to be reduced.

The trace itself renders as a live Blockly workspace, not as text, with the substituted regions labelled — Substituted block for parameter, Function body of CbS — so you can tell which part of the new term came from where. Where a value carries structure, the step's block comment adds a pair: line reading 21 ⊗ (3 * 7) alongside the usual term: and value: lines.

A CbS trace can still get large

Retained structure is real structure, so the trace of a deeply recursive program is genuinely big — the Factorial 5 trace unrolls the whole recursion as blocks. That is the honest picture of what CbS records, but it is awkward to read in a short panel. Maximise the dock, and use Run: Arrange Reduction Steps from the command palette to lay the steps out. For learning the strategy itself, start with a small term like Copy vs Lookup rather than factorial.

The rules CbS fires

The reduction rules carry their λVis names, and the step label under the trace names the one that just fired:

Rule Fires when
E-Beta A function is applied: the parameter is bound to the structured value
E-BetaS The function being applied itself carries a visual, so the call is framed
E-Frame A framed body has produced its value and the visual frame is merged back in — shown as merge the visual frame (E-Frame)
E-FixBeta / E-FixS A letrec binding unfolds, plain and structured respectively
E-UnOp / E-BinOp A primitive operator is applied to its operands
E-Render A structured value is rendered back to the visual surface

Frames are a CbS-only construct: a term of the form (Vv ▷ E) is an expression evaluating underneath a retained visual, and E-Frame is what collapses it once the inside is done. Nothing downstream of the semantics sees them — the compiler pipeline erases frames entirely.

Call-by-Value

Call-by-Value reduces the argument to a value first, then substitutes that value. It is the strategy most real functional languages use, and it is available here for exactly one reason: so you can put it next to CbS and see what the choice keeps and what it throws away.

Since CbS is also strict, the two strategies agree on when the argument is evaluated and on how many times. Put the two tabs side by side on Copy vs Lookup and the traces have the same shape; what differs is that CbV's substituted occurrences are bare numbers where CbS's still carry the expression that produced them.

Everything else about the tab is identical — same block rendering, same controls, same right-click entry point.

No reduction under a binder

Under both strategies, B-Lambda does not reduce inside the body of an abstraction. A λ is a value. Its body is only reduced once an application has substituted something for the parameter.

λx. (2 + 3)     -- stays as it is. Already a value.
(λx. (2 + 3)) 7 -- now the body reduces: → 5

The Normal Form example loads a term that is already in normal form, so you can confirm that the stepper genuinely refuses to go further rather than merely happening not to. This matches MnL's behaviour, and it is why the two languages' reduction traces can be compared at all.

The CSEK machine

The second, independent evaluator. Control, Structure, Environment, Kontinuation.

The S is Structure, not a Store

The extra letter here is not the one in B-MJ's CESK machine, where S is a store and objects live on a heap. B-Lambda has no heap. Its S is the structured value — the machine's image of Vp ⊗ Vv, a computed value paired with the block it was computed from. Two different machines, two different fourth letters.

The CSEK machine tab part-way through the Factorial 5 example, showing the Control column with the expression to evaluate next, the Environment column listing the current bindings, and the Kontinuation column listing the pending frames innermost first.
Fig. 1 The CSEK machine mid-way through Factorial 5. The status line reads step n · rule — the rule that just fired is named next to the step count.

Three columns, in machine order:

  • Control — what the machine will evaluate next.
  • Environment — the current bindings, innermost first, with shadowed names dimmed. A binding that carries structure prints as its value, then , then the block it retained — 21 ⊗ 3 * 7. A letrec binding prints with an hourglass, , because it is the one thing the machine still stores unevaluated.
  • Kontinuation — the pending work, innermost first. Empty reads (empty — top level).

Because CbS is strict, the frames spell out what is being deferred and why:

Frame reads Meaning
apply · evaluating arg strictly, then binding value ⊗ structure The CbS application frame. It says outright that the argument is being evaluated now, and that the pair is what will be bound.
bind x · evaluating strictly, then binding value ⊗ structure The same thing for a let right-hand side.
frame ▷ · merge block into the result A visual frame waiting to be merged — the E-Frame step.
apply · waiting for the argument The plain Call-by-Value application frame.

Controls are Load, Back, Step and Play. If you edit the program while a machine is loaded, the panel says Program changed — load it again to restart rather than silently stepping a stale term. When it finishes it reports Value after n step(s); when it cannot, it says ⨤ stuck after n step(s) and why.

Back is exact time travel

The step function is pure: it returns a new state rather than mutating the current one. So Back does not re-run the program from the beginning and hope to land in the same place — it restores the exact earlier state. This is also what makes the machine safe to compare against the substitution trace.

The environment's structured entry is the machine's counterpart of the substitution trace's retained structure: where the rewriter shows 3 * 7 beside the 21, the machine binds the two together in one entry. Neither recomputes anything, which is why the machine fires the same salient rules in the same order as the substitution trace — the invariant the Lockstep view checks. Note that the CSEK tab has no per-tab strategy switch — it runs the language default, CbS.

Lockstep

The tab that ties everything together. Lockstep runs three evaluators on the same program and pairs them up:

The Lockstep tab running the Factorial 5 example, with the substitution state drawn as a block tree on the left, the CSEK machine state in the middle, the Register VM on the right, and a status line underneath reporting that the three are in sync.
Fig. 2 Lockstep on Factorial 5: substitution on the left, the CSEK machine in the middle, the register VM on the right, and the agreement badge underneath reading In sync — n salient events · same value (v): substitution ≡ CSEK ≡ VM.
  • Left — the substitution state, as a block tree.
  • Middle — the CSEK machine state that has “caught up” to it.
  • Right — the register VM, with its frames and registers.

The status line counts matched salient rules and flags divergence. In sync means the substitution semantics, the abstract machine and the compiled bytecode all agree on this program, at this step, under this strategy — the operational-correspondence claim, executed rather than asserted. If it breaks, the badge names both sides: ⚠ diverged — CSEK reason · VM reason.

The two comparisons are not equally strict

The CSEK machine is checked per event: each rule it fires must be the same rule, in the same order, as the substitution trace. The register VM is checked per count — it has been through nine lowering stages by then, so what is claimed of it is that it performs the same number of salient steps and reaches the same value, not that its instructions line up one-to-one.

Unlike the CSEK tab, Lockstep keeps a CbS / CbV switch, defaulting to CbS, so you can check the correspondence under either strategy. If a trace cannot be built at all the panel says so — Could not build the lockstep trace — rather than looking like a program with no reductions.

Why step counts differ

Watch Fig. 2 for a moment and the two counters drift apart — the machine's is always well ahead of the substitution trace's. Neither is wrong. They are counting different things.

Counts One press advances
CSEK machine Every transition, including the administrative ones: environment lookups, pushing and popping continuation frames, descending into a sub-term, returning a value, merging a visual frame. one machine transition
Substitution trace Only salient reductions — beta, if-true / if-false, and primitive prim … steps. The human-visible redexes. one visible redex

So one reduction frame corresponds to several machine micro-steps, and the machine's number is always the larger. E-Frame is a clear case: merging a visual frame is real work for the machine but is not a redex anyone would point at, so it advances the machine's counter and not the trace's. Lockstep advances the machine in bulk between salient rules — the bookkeeping transitions still increment the machine's count but do not add a reduction frame. Both reach the same normal form, and the salient-event count confirms they agree on the trace that matters.

The same rule of thumb across the workshop

A substitution counter compared against a machine counter is always coarser, because the machine also counts the steps the rewriter never names. Two counters at the same granularity stay equal — which is why B-MJ's A vs B tab shows both its machines finishing in exactly 51 steps.

You can also reach the traces without the dock: More → Call-by-Structure Trace, Call-by-Value Trace and Lockstep Debugger, or the matching Run: commands in the palette. The CSEK machine is the one view with no More entry — reach it from the workspace's Run button, the Semantics tab strip, or Run: CSEK Machine in the palette.

Block Lambda is developed at L-Workshop. This help was written against Block Lambda 1.2.0.