fix(golem): asm and run refuse a Source that assembles to zero words#222
Merged
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
andraderaul
added a commit
that referenced
this pull request
Jul 24, 2026
…s, Watchdog, FPU) (#220) * docs(golem): the v2 glossary — Device, Watchdog, FPU, Interrupção, ISR, 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> * test(golem): vendor the unit-2 fixtures, and park unit 3's oracle before 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> * feat(golem): the Step event stream, and int N dispatching end to end (#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> * feat(golem): Terminal readback, and 2_hello_world green end to end (#214) 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> * feat(golem): load <name> puts a reference program in the editor (#215) 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> * feat(golem): isr, reti and the enai macro — 2_interruption green (#216) 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> * feat(golem): the Watchdog — 2_watchdog green (#217) 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> * feat(golem): the FPU — 2_fpu green, and all of unit 2 with it (#218) 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> * feat(golem): the DEVICES panel (#219) 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> * fix(golem): one hardware dispatch per Step — Watchdog first, FPU held 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> * fix(golem): asm and run refuse a Source that assembles to zero words (#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> * fix(golem): address v2 code-review findings (#223) 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> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #220, validating an operator-reported symptom:
runon an empty Source starts a run that never ends.The bug
Blank lines and comments assemble to zero words without error —
assemble('')returnsok: truewith an empty image.build()then reportedassembled 0 words — editor locked, andrunstarted the clock over a machine whose every fetch reads0x00000000: opcode0x00with destinationr0, an implicit nop. Noint 0ever arrives, so the machine never halts and the Console saysrunning at N/sforever.The tab never freezes and
stopalways lands — the clock driver's rAF yield guarantees that by design — but an endless run over nothing has no reading for the operator. (The reference emulator spins the same way: word0is an explicitnopin its main switch.)The fix
build()refuses an image with no words:The editor stays unlocked and no Machine is created. The guard sits in
build(), so one check coversasmandrunboth — and the three-state model is preserved: refusing at the Source → Image boundary means the dead-machine state is unrepresentable rather than handled.Pinned by two app tests:
asmon an empty Source, andrunon a comments-only Source (neither locks the editor, neither starts the clock).Verification
Full pre-commit suite green (
biome check, all workspaces'test:run— 355 in golem now),typecheckclean. One changeset, golem patch.🤖 Generated with Claude Code