Skip to content

feat(golem): GOLEM//Console v2 — the whole of unit 2 (interrupts, ISRs, Watchdog, FPU)#220

Merged
andraderaul merged 12 commits into
mainfrom
feat/golem-v2-unit-2
Jul 24, 2026
Merged

feat(golem): GOLEM//Console v2 — the whole of unit 2 (interrupts, ISRs, Watchdog, FPU)#220
andraderaul merged 12 commits into
mainfrom
feat/golem-v2-unit-2

Conversation

@andraderaul

Copy link
Copy Markdown
Owner

Implements the GOLEM//Console v2 spec (#203) end to end — the whole of Poxim's unit 2. Closes #204, #205, #206, #207, #208, #209, #210, #211.

The v1 machine was a closed loop: nothing ever happened to a program. v2 points GOLEM at the machinery it couldn't show — interrupts with user-written ISRs, a memory-mapped Watchdog and FPU that tick on their own, and a Console that narrates every dispatch so the operator watches the golem's own inscription shut it down rather than deducing it from a changed PC.

All four unit-2 reference programs are green on both oracles.hex word-for-word and .out trace-for-trace. The unit-1 fixtures stay green untouched.

What landed, slice by slice

Merged into this branch as eight reviewed PRs, one per issue, tracer-bullet order:

PR Issue Slice
#212 #204 Vendor the unit-2 fixtures; park unit 3's oracle before it decays
#213 #205 step(machine) → { machine, events }; int N dispatching end to end
#214 #206 Terminal readback → 2_hello_world green
#215 #207 load <name> puts a reference program in the editor
#216 #208 isr, reti, and the enai macro → 2_interruption green
#217 #209 The Watchdog → 2_watchdog green
#218 #210 The FPU → 2_fpu green
#219 #211 The DEVICES panel

The one interface change

step(machine) now returns { machine, events }. It is forced, not chosen: int 1 and a division by zero leave byte-identical machines behind — same cause in CR, same vector in PC — so a dispatch is not derivable from a state diff. The trace formatter and the Console narration read that one event stream. step stays pure. This is also half the v3 boundary; the other half is that devices are now intercepted on the single memory-access path, where the cache will attach as a layer.

Decisions worth a reviewer's eye

  • load prevents data loss rather than just warning about it. The issue asked for a warning that it replaces the current Source; a warning printed after the overwrite doesn't satisfy "so I don't lose work". So it refuses once and confirms on repeat — but the starter example and an already-loaded program are replaced freely, so load watchdog + run stays two commands from a cold start. More than the issue literally asked; easy to reduce.
  • The FPU replicates the reference's broken exponent mask on purpose. Unlike the broken ble (which GOLEM fixes), this only decides how many cycles an operation is said to cost — didactic fiction with no ground truth — and the fixture's timings only come out under the broken mask. A test asserts it is not the real exponent, so nobody "fixes" it later.
  • FPU register reads are value-dependent (integer vs IEEE-754 bits), recovered from the reference C, not the trace. Documented in ISA.md as the quirk it is.
  • 2_blt/2_bnzgen_blt/gen_bnz: their 2_ meant "generated with poxim2", which became a trap once real unit-2 programs landed beside them.

Every deliberate divergence from the reference is written down in apps/golem/docs/ISA.md.

Note on CI

The three Vercel deploy checks fail on the constituent PRs with build-rate-limit ("retry in 24 hours") — an account limit, not the code. Build and Test passed on every PR.

Verification

npm run test:run green across all workspaces (351 in golem), npm run check, npm run typecheck and npm run build all clean. Seven changesets included — golem minor, no other app bumped (ADR 0012).

🤖 Generated with Claude Code

andraderaul and others added 9 commits July 19, 2026 14:16
…R, Macro

Groundwork for GOLEM//Console v2 (#203). Sharpens Step (devices tick in
lockstep) and Alias (a macro is not an alias), and names the unit-2 vocabulary
the slices will use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ore it decays (#212)

Closes #204.

The four unit-2 reference sets (2_hello_world, 2_interruption, 2_watchdog,
2_fpu — each .s/.hex/.out) join the unit-1 fixtures under the same provenance
statute as ADR 0019. No product code: this is the oracle every later v2 slice
diffs against, landing first so each slice can go green on its own.

Unit 3's traces are parked with no consumer, and that is the point. The
reference emulators are C from 2017; the oracle is perishable and the window is
open today. The _cache.out traces came pre-generated with the material, so
nothing needed to compile — recorded as such in PROVENANCE rather than claimed
as a run of ours.

Renames the fabricated 2_blt/2_bnz to gen_blt/gen_bnz: their '2_' meant
'generated with poxim2', which stops being readable the moment real unit-2
programs sit beside them.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…213)

Closes #205.

step(machine) now returns { machine, events } — the one interface change v2
makes to an existing seam, and it is forced rather than chosen: `int 1` and a
division by zero leave byte-identical machines behind, same cause in CR, same
vector in PC. A dispatch is not derivable from a state diff, so it is reported.

The trace formatter and the Console narration consume that one stream. The
formatter emits the reference's [SOFTWARE INTERRUPTION] marker after the two
lines of the instruction that raised it, which is the order a diff against the
reference .out expects. The narration is one Console line per dispatch — never
the Terminal, which is the program's own output.

int N with IE clear is decided as a no-op, not a half-dispatch that writes CR
without jumping; no fixture pins it, and ISA.md records the reasoning.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
)

Closes #206.

Unit 2's smallest behavior: a byte stored at the Terminal address keeps the
byte readable there. The write no longer returns early — it echoes the
character and then falls through to the ordinary byte write, which is what
gives the readback. The Terminal is a device *and* an address.

The trace formatter gained the reference's [TERMINAL] section, emitted only
when the program printed something, which is why no unit-1 fixture carries one.

2_hello_world now passes both oracles untouched: word-for-word against its .hex
and trace-for-trace against its .out. The v1 assembler needed no change for it.

UNIT_2_RUNNABLE grows by one entry per slice, so the fixture suite is never red
on work not yet done.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Closes #207.

The one new Console command of v2. `load` with no argument lists the four
programs with a one-line summary each; an unknown name gets a did-you-mean off
the same edit-distance threshold commands use, generalized out of
`nearestCommand` rather than reimplemented beside it.

The three-state model holds by construction: with a Machine alive the editor is
locked, so there is nothing to load into and `load` says to reset first.

Losing work to a demo is prevented rather than merely warned about. A Source the
operator wrote earns one refusal, and re-issuing the same `load` confirms it —
no new grammar, no dialog. The starter example and an already-loaded program are
not work, so `load watchdog` then `run` stays two commands from a cold start.

The programs reach the bundle through Vite `?raw` imports of the fixture .s
files, deliberately not through the fixture loader: that one goes via node:fs to
keep .hex and .out oracles out of the bundle, and only the sources ship.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Closes #208.

ISRs become the operator's own code. `isr` gains its target operand and
*jumps*: it captures IPC and CR and then lands on the handler body, which is
what lets an interrupt vector fit in one word — the vector word is the preamble
and the branch at once. `reti` reads as `ret` does; they differ in intent, not
in what they do to the machine.

`enai` is the assembler's first Macro, and the only one. Its expansion was
pinned before a line of it was written: the predicted pair
(addi rX, r0, 64; or fr, fr, rX) appears verbatim at consecutive offsets in all
three unit-2 .hex fixtures. The operand register is scratch, clobbered on the
way through, exactly as the reference assembler does it.

Division by zero (cause 0x01) and invalid instruction (cause 0x2A) now dispatch
when IE is set. An invalid instruction prints no disassembly — there is no
instruction — so the trace entry is the marker lines alone, matching the
reference.

One test-harness fix worth noting: machine.fixtures counted [UFS] effect lines
as Steps, an assumption unit 2 breaks, since an invalid instruction costs a Step
and prints no effects line. It now counts the marker too.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Closes #209.

The first Device with a clock. Its register *is* memory word 0x2020 rather than
a field beside it, which is what makes it readable by the program and dumpable
by `mem` for free — a memory-mapped device whose state lived outside memory
would be a lie.

Ticking happens in `step`, after the PC settles. That ordering is not
incidental: a hardware interrupt saves the address the program would have run
next, which for a taken branch is the branch target. Software interrupts still
save PC + 4, since they are raised mid-instruction. `divert` names that as the
one thing the two kinds disagree about.

The reference pinned the timing exactly — 100 `bun` instructions between the
arming store and the dispatch — which only comes out right because the arming
Step ticks too. That off-by-one is now a documented, tested property rather than
an accident.

The trace needed one subtlety: the effects line of the instruction a hardware
interrupt preempts must report the PC that instruction produced, not the vector
the device then jumped to. The event carries that address, so the formatter
reports the instruction's own result.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Closes #210.

The second Device, and the first whose registers do not round-trip: a write
converts an integer to a float, a read converts back by a different rule. So
unlike the Watchdog — whose register simply *is* a memory word — the FPU keeps
its own state on the Machine, which is also exactly what the DEVICES panel
needs.

Devices are now intercepted in readWord/writeWord, the single memory-access
path, so ldw/stw/push/pop agree about them without four copies of the check.

Three behaviours were recovered by reading the reference emulator's source, not
guessed:

- Writes convert numerically (74 becomes 74.0f), which is why 74/8 yields 9.25
  rather than a denormal.
- Reads are **value-dependent**: a whole number comes back as an integer, a
  fractional one as its IEEE-754 bits. z = 9.25 reads 0x41140000, z = 19 reads
  0x13. Indefensible as design, and exactly what the fixture pins in both
  directions.
- The exponent mask is one hex digit short (0x7F80000 for 0x7F800000), so it
  reads mantissa bits. Replicated deliberately: unlike the broken `ble`, this
  only decides how many cycles an operation is said to cost. The fixture's
  divide-costs-4 and multiply-costs-3 only come out under the broken mask.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Closes #211, and completes GOLEM//Console v2.

One panel, read-only, where the machinery v1 could not show becomes watchable:
the Watchdog's counter descending a tick per Step, and the FPU's x, y and z as
decoded floats beside the raw words they really are, with the in-flight
operation and its remaining cycles.

The reading model lives in `devicesOf`, a pure function, so "9.25 reads as 9.25
and not as 0x41140000" is a unit test rather than something only a DOM assertion
could reach — the same seam `flagsOf` and `formatMemoryDump` already use.

The IE flag joins the named-flags display. Without it a suppressed dispatch
looks like the machine ignoring the program.

Placed under Flags and sized to its content rather than sharing the stretch, so
it does not squeeze Memory and the Terminal; on a phone the column scrolls and
the countdown still reads.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ascii-art-converter Error Error Jul 24, 2026 12:31am
cyberdeck-golem Error Error Jul 24, 2026 12:31am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
glitch-studio Skipped Skipped Jul 24, 2026 12:31am

… pending (#221)

* fix(golem): one hardware dispatch per Step — the Watchdog takes it, the FPU waits

When the Watchdog and the FPU ran out on the same Step, both dispatched
and the second divert overwrote CR/IPC with the hardware-1 vector as the
return address — the Watchdog's interrupt lost to a clobber. The
reference emulator does the same, but no fixture exercises the
coincidence, which puts it in the broken-ble category: an accident, not
a semantic, and GOLEM does not replicate it.

Now a Step carries at most one hardware dispatch. The Watchdog (line 1)
takes it; the FPU's completion holds at zero cycles and dispatches whole
on the Step after, with its own cause and a real resume address. With IE
clear the FPU still completes silently, as ISA.md already decided.

dispatchHardware and softwareInterrupt also collapse the three
duplicated divert call-site shapes the review flagged.

Pinned by a hand-written test; documented in ISA.md under "Divergências
deliberadas".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(golem): the DEVICES panel shows the raw word a program reads, plus review cleanups

The panel showed x/y/z raw words as IEEE-754 bits unconditionally, while
a read of the same register yields the integer for whole values — so the
panel said 0x41980000 where a ldw got 0x13. The raw word now uses the
machine's own value-dependent encoding, via the exported fpuWord.

Also from the review:

- UNIT_2_RUNNABLE had finished growing into UNIT_2_PROGRAMS; collapsed.
- nearestCommand was a one-line delegate with one caller; inlined.
- devices.tsx names DeviceReading[] instead of walking
  ReturnType<typeof devicesOf>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…222)

Blank lines and comments assemble to an empty image without error, and
run over it started a run that could never halt: every fetch of zeroed
memory decodes as an implicit nop, and no int 0 ever arrives. The
reference emulator spins the same way — word 0 is an explicit nop in its
main switch — but an endless run over nothing has no reading for the
operator.

The guard lives in build(), so one check covers asm and run both. The
Console answers "nothing to assemble — the Source has no instructions"
and the editor stays unlocked.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Standards and spec findings from the review of #220, none of them
behaviour changes (355 golem tests pass unchanged):

- machine.ts: drop the stray "(ADR 0020 groundwork)" from the
  memory-access-path comment — ADR 0020 is the Control Strip decision,
  not the v3 cache; there is no ADR to cite for future cache work.
- isa.ts: the IE JSDoc claimed only *software* interrupts are gated on
  it, but tickWatchdog and tickFpu gate hardware dispatch on IE too.
- machine.ts: fold the mirrored read/write FPU-register cascades into one
  address→field map (FPU_FIELD); CONTROL stays a special case on both
  paths since it is a command, not a value.
- inspect.ts: the three identical x/y/z device rows become one map.
- PROVENANCE.md: #204 banked the unit-3 cache traces by copying them; the
  spec's "the emulator still compiles" bet went unexercised. Verified it:
  poxim3 compiles clean under clang today, and rerunning it on
  3_memory_access.hex reproduces the committed 3_memory_access_cache.out
  byte-for-byte (bar the trailing blank line already documented). The
  window is confirmed open, not assumed.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@vercel
vercel Bot temporarily deployed to Preview – glitch-studio July 24, 2026 00:31 Inactive
@andraderaul
andraderaul merged commit 620bb3e into main Jul 24, 2026
4 of 6 checks passed
@andraderaul
andraderaul deleted the feat/golem-v2-unit-2 branch July 24, 2026 00:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vendor the unit-2 fixtures, and fabricate the unit-3 cache traces while the reference emulator still compiles

1 participant