This document defines the coding standards and architectural patterns for the Loop 2.1 simulator. Every Claude instance working on this project must read this before writing or modifying code.
These standards describe the architecture that exists now (v.307, ~15,500 lines, 574 functions). The restructuring in v.267–v.288 established these patterns. New features must fit into this structure, not beside it.
Loop 2.1 is a single HTML file. No build step, no framework, no npm, no external dependencies beyond Google STUN servers for WebRTC. This constraint is permanent.
All simulation state lives in const L21 = { ... }. If a value participates in simulation — loop bits, register values, bus configurations, counter values, PM settings, TG thresholds, logger state, statistics — it is a property of L21. No exceptions.
| Sub-Object | Contents |
|---|---|
L21.loops | Four loop state objects: bits arrays, pause/gate, flash, word tracking |
L21.buses | Nine bus state objects (a–i): bits, active, src, dst, flash, transfer tracking |
L21.alu | Registers A–D, result, operand selection, operation, capture route, writeback, flags |
L21.cmp | Comparator flags, packed word, writeback buffer |
L21.ctr | Five counters (working/alu/memory/big/global): values, triggers, output buffers |
L21.wscr | Working scratch: four slots, capture state, writeback buffers |
L21.mem | 16 memory slots, write/auto-inc/addr-read, batch queue, file load queue |
L21.inj | Inject channel: buffer, pending, flash, active, writeback position |
L21.clock | Running, tickCount, hz, timing, accuracy samples |
L21.switchBits | 16-element array: operator input switch state |
L21.ext | External Bus E buffers: outBuf, inQueue |
L21.pm / pm2 | Pattern matcher: mask, pattern, rewrite, cascade, bridge, match count |
L21.tg1 / tg2 | Threshold gate: threshold, mode, clamp, cascade, bridge, match count |
L21.logger | Session log: active, lines, tick count, start time |
L21.stats | Session statistics: operator actions, transfers, ALU ops, etc. |
Each L21 sub-object has a const alias declared near the code that uses it:
const loops = L21.loops; const alu = L21.alu; const mem = L21.mem; const clock = L21.clock;
The alias is the same object reference (not a copy). Mutating alu.result mutates L21.alu.result. Declare the alias once, near the top of the relevant code section. When adding a new L21 sub-object, create the alias in the same pattern.
UI state — DOM references, canvas contexts, resize observers, drag state, dirty-check caches, geometry caches — is deliberately not in L21. It lives in separate objects (EL, SK, loopGeometryCache, etc.).
The test: "Would a replay need this value to reproduce the machine's behavior?" If yes → L21. If no → outside L21.
Machine constants declared at top of JS, never change: BPW = 17, BUS_N = 24, INJ_N = 17.
Config tables drive behavior generically:
BUS_CONFIGS— central table for all nine buses: id, label, type, DOM IDs, CSS classes, flash properties, canvas colors.LOOP_DEFS(aliasedDEFS) — four loops: id, word capacity, color, RGB values.COUNTER_TRIGGERS— events each loop counter can fire on.UNARY_OPS,CARRY_OPS— Sets for hot-path ALU branching.
if (busId === 'a') ... else if (busId === 'b') ..., stop. Add an entry to a config table and write one generic function that reads it.camelCase, whole English words, reads like prose. No abbreviations except universal ones (ALU, PM, TG, P2P, CBX, DOM).
| Pattern | Examples | Used For |
|---|---|---|
verbNoun | togglePause, injectValue, clearAllInputSwitches | Operator-triggered actions |
tickNoun | tickPreamble, tickReadPhase, tickRotatePhase | Tick engine phases |
renderNoun | renderLoop, renderAllComponents | Canvas/display updates |
refreshNounDisplay | refreshMemoryDisplay, refreshWorkingScratchDisplay | DOM-only display refresh |
handleNoun | handleSpeedChange, handleChallengeAnswerWord | Event responses |
buildNoun | buildInputSwitchLevers, buildNetBusPeerOptions | DOM construction at init |
cbxVerb | cbxPingNetwork, cbxReceiveMessage | CBX protocol |
p2pVerb | p2pFlushWordBufferToQueue | P2P data channel |
challVerb | challSetStatus, challUpdateLiveStats | Challenge module |
netmonVerb | netmonCapture, netmonRefreshStream | Network monitor |
Legacy one-liner delegates (function toggleBus() { toggleBusGeneric('a'); }) are kept for backward compatibility. New code calls the generic function directly. Do not create new delegates.
camelCase, whole words. Private-ish properties: leading underscore (_autoWbPending, _batchRunning). Booleans read as assertions: active, running, destructive (not isActive). Arrays: plural nouns (bits, slots, lines). Buffers end in Buf: capBuf, wbBuf, outBuf.
Element IDs: kebab-case (btn-run, alu-bits-a). CSS classes: kebab-case (.bus-strip). DOM elements cached in EL at init — never call getElementById in a per-tick function. Buttons use data-action attributes. No inline onclick.
function executeOneTick() {
tickPreamble(); // counter rotation, accuracy sampling
const sources = tickSampleBusSources(); // read bus source bits BEFORE anything moves
const injExitBit = tickReadPhase(); // sample read heads, advance captures, triggers
tickGatePhase(); // gate destruction
tickRotatePhase(); // shift all loop bit arrays by one
tickWritePhase(sources, injExitBit); // deliver bus/inject bits to destinations
}
Order matters. Sources sampled before rotation. Writes after rotation. New per-tick logic goes into the appropriate phase, not as a new top-level call unless it is genuinely a new phase.
mainLoop() (rAF callback): flash decay, catch-up tick loop (up to 8/frame), accuracy display, conditional renderAllComponents().
Inside executeOneTick or called by it:
- No DOM access. All DOM updates in render functions only.
- No string allocation. No template literals, no concat. Exception: log writes when
logger.active. - No array allocation. No spread, slice, filter, map, reduce. Manual for loops on pre-allocated buffers.
- No string comparison chains. Use pre-built Sets (
UNARY_OPS,CARRY_OPS). - Use
ctrFireBulkfor bulk counter increments.
Single delegated click listener on document. Buttons declare actions via data-action="functionName" with optional data-arg, data-arg2.
<button data-action="togglePause" data-arg="working">⏸</button>
ELEMENT_RECEIVER_ACTIONS Set: functions that receive the DOM element as argument instead of parsed data attributes.
To add a button: add data-action in HTML, write the function. No listener registration needed.
onclick="...". All clicks through delegation. Only exception: skin selector buttons in header (legacy).renderAllComponents() is the master render, called once per frame when something changed. Render functions read from L21, write to DOM/canvas. They never modify L21. One-way flow: tick mutates state → render displays.
Dirty flags (alu.displayNeedsRefresh, mem.displayNeedsRefresh) skip expensive updates when state hasn't changed.
Bus rendering: renderBusA() calls renderUnidirectionalBus('a'). Generic functions read BUS_CONFIGS.
The bus system is the canonical example of multi-instance component structure. Study it before adding anything new:
1. Config table entry. BUS_CONFIGS[busId] — all metadata in one place.
2. Generic functions. selBusSrc(busId, id), toggleBusGeneric(busId), tickUnidirectionalBus(busId), renderUnidirectionalBus(busId). Each takes bus ID, looks up config.
3. Legacy delegates. function selCSrc(id) { selBusSrc('c', id); } — for data-action compat. Don't create new ones.
4. State in L21. L21.buses.a through L21.buses.i. Shared shape.
For any new multi-instance component: config table → generic functions → L21 state with shared shape.
- State. New sub-object in L21. All properties, types, defaults. Const alias.
- Config table. If multi-instance or has metadata, create/extend a table.
- Tick logic. Add to appropriate phase. Follow hot-path rules.
- Operator actions. Named functions per conventions. Modify L21 state.
- HTML.
data-actionon buttons. No inline onclick. - Rendering. Read L21, write DOM/canvas. Add to
renderAllComponents(). Dirty flags if expensive. - Snapshot. Add to both
captureFullSnapshot()andrestoreFromSnapshot(). - Session log. Dual-format entries (
MACHINE.CODE || Human description). OP. prefix for operator actions. - Tests. Initialization, key behaviors, edge cases.
captureFullSnapshot() serializes L21 to a plain object. restoreFromSnapshot(snap) writes it back. Both must be updated when L21 state changes. Values must be primitives or plain objects/arrays — no DOM refs, functions, or class instances. Exclude non-serializable values and re-derive on restore.
Dual-format: machine code left of ||, human description right.
writeLogLine('OP.BUS.A.SRC=ALU', 'Bus A source set to ALU');
writeLogLine('BUS.A.XFER w=31400', 'Bus A delivered word 31400');
Operator actions: OP. prefix. Machine events: no prefix. Log writes conditional on logger.active, guard at call site.
Major sections: // ================================================================. Sub-blocks: // ── description ──────.
Section order (approximate): Skins → Constants → L21 namespace → UI state → Net bus/P2P → Op count/loop defs/bus configs → Switch/inject → Clock/lookups → Bus sidebar/generics → Speed/clock → CBX → Challenges → Network monitor → Networking → Files → Working scratch → Tick engine → Main loop → Bus delegates → ALU → Memory → Logger → Snapshots → Sound → PM/TG → Counters → Render → Init → Event delegation.
New code goes in the matching section. If none fits, create a new section with a header comment in a logical position.
Version lives in exactly one place: const APP_VERSION = '307';. Title and UI read from it dynamically. No other version string in the file.
- APP_VERSION updated. Search for
const APP_VERSION. Change string. Confirm one instance. - Syntax check passes.
sed -n '/<script>/,/<\/script>/p' file.html | sed '1d' | sed 's/<\/script>//' > /tmp/check.js && node --check /tmp/check.js
- Test suite passes. All tests green.
- Filename matches.
loop2-stage2 (NNN).htmlwhere NNN = APP_VERSION. - No unrelated changes. No drive-by refactoring, no bonus features.
| Anti-Pattern | Why | Instead |
|---|---|---|
| Top-level let/var for sim state | Invisible to snapshots | Add to L21 |
| getElementById in hot path | DOM lookup every tick | Cache in EL at init |
| onclick="..." in HTML | Bypasses delegation | data-action attribute |
| Copy-paste per-component functions | N copies that drift | Config table + generic function |
| String comparison chains in hot path | Slow, error-prone | Pre-built Set/Map |
| .map/.filter/.slice in hot path | Allocates arrays every tick | Manual for loops |
| State in DOM elements | Not serializable | Store in L21, read in render |
| Modifying L21 in render functions | Violates one-way flow | State changes in tick or operator actions only |
| Creating new delegate functions | Legacy pattern | Call generic function directly |
| Adding unrequested features | Scope creep, untested | Build exactly what was asked for |
| Forgetting captureFullSnapshot | State lost on save/restore | Always update both capture and restore |
| Forgetting APP_VERSION | Wrong version displayed | First thing on every build |
This document is a Project file. Every Claude instance must review it before writing code. If the architecture changes, update this document in the same session.