This website is meant to be read and understood quickly by humans, but is only fully parsable, on a technical level, with the aid of an AI system. Read why →
Loop MMT
Reprint · a previously-published work

This is a previously published Loop 2.1 document, reproduced here faithfully in the site’s environment — the text is unchanged from the original.

Authored by Claude (Anthropic), an AI — not by a human.

Open or download the original, exactly as it was given →

Loop 2.1 — Coding Standards & Architecture Guide

Purpose of This Document

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.

The Rule

If a pattern for what you're building already exists in the codebase, follow it exactly. If no pattern exists, establish one consistent with existing patterns. No one-off approaches.

Section 1

Architecture

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.

The L21 Namespace 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-ObjectContents L21.loopsFour loop state objects: bits arrays, pause/gate, flash, word tracking L21.busesNine bus state objects (a–i): bits, active, src, dst, flash, transfer tracking L21.aluRegisters A–D, result, operand selection, operation, capture route, writeback, flags L21.cmpComparator flags, packed word, writeback buffer L21.ctrFive counters (working/alu/memory/big/global): values, triggers, output buffers L21.wscrWorking scratch: four slots, capture state, writeback buffers L21.mem16 memory slots, write/auto-inc/addr-read, batch queue, file load queue L21.injInject channel: buffer, pending, flash, active, writeback position L21.clockRunning, tickCount, hz, timing, accuracy samples L21.switchBits16-element array: operator input switch state L21.extExternal Bus E buffers: outBuf, inQueue L21.pm / pm2Pattern matcher: mask, pattern, rewrite, cascade, bridge, match count L21.tg1 / tg2Threshold gate: threshold, mode, clamp, cascade, bridge, match count L21.loggerSession log: active, lines, tick count, start time L21.statsSession statistics: operator actions, transfers, ALU ops, etc. Adding New StateWhen adding a feature that requires state, add a new sub-object to L21. Do not create top-level variables. Do not store simulation state in DOM elements or closure variables.

Const Aliases 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 vs Simulation State 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.

Constants and Config Tables 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 (aliased DEFS) — 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. Config-Driven, Not Code-DrivenWhen you find yourself writing if (busId === 'a') ... else if (busId === 'b') ..., stop. Add an entry to a config table and write one generic function that reads it.

Section 2

Naming Conventions

Functions camelCase, whole English words, reads like prose. No abbreviations except universal ones (ALU, PM, TG, P2P, CBX, DOM). PatternExamplesUsed For verbNountogglePause, injectValue, clearAllInputSwitchesOperator-triggered actions tickNountickPreamble, tickReadPhase, tickRotatePhaseTick engine phases renderNounrenderLoop, renderAllComponentsCanvas/display updates refreshNounDisplayrefreshMemoryDisplay, refreshWorkingScratchDisplayDOM-only display refresh handleNounhandleSpeedChange, handleChallengeAnswerWordEvent responses buildNounbuildInputSwitchLevers, buildNetBusPeerOptionsDOM construction at init cbxVerbcbxPingNetwork, cbxReceiveMessageCBX protocol p2pVerbp2pFlushWordBufferToQueueP2P data channel challVerbchallSetStatus, challUpdateLiveStatsChallenge module netmonVerbnetmonCapture, netmonRefreshStreamNetwork 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.

Variables and Properties 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.

DOM and CSS 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.

Section 3

Tick Engine

Phase Structure 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().

Hot-Path Rules 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 ctrFireBulk for bulk counter increments.

Section 4

Event Handling

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.

No Inline onclick

Do not add onclick="...". All clicks through delegation. Only exception: skin selector buttons in header (legacy).

Section 5

Rendering

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.

Section 6

The Bus Pattern — Model for Everything

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.

Section 7

Adding a New Component — Checklist

  1. State. New sub-object in L21. All properties, types, defaults. Const alias.
  2. Config table. If multi-instance or has metadata, create/extend a table.
  3. Tick logic. Add to appropriate phase. Follow hot-path rules.
  4. Operator actions. Named functions per conventions. Modify L21 state.
  5. HTML. data-action on buttons. No inline onclick.
  6. Rendering. Read L21, write DOM/canvas. Add to renderAllComponents(). Dirty flags if expensive.
  7. Snapshot. Add to both captureFullSnapshot() and restoreFromSnapshot().
  8. Session log. Dual-format entries (MACHINE.CODE || Human description). OP. prefix for operator actions.
  9. Tests. Initialization, key behaviors, edge cases.

Do Not Skip Steps

Skipping snapshot = state lost on save/restore. Skipping log = incomplete records. Skipping tests = silent regressions.

Section 8

State and Snapshots

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.

Section 9

Session Logging

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.

Section 10

Code Organization

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.

Section 11

Versioning

Version lives in exactly one place: const APP_VERSION = '307';. Title and UI read from it dynamically. No other version string in the file.

This Has Broken Three Times

Instances updated the filename but not APP_VERSION. This is the #1 process failure. Search for APP_VERSION (one instance) and update on every build. No exceptions.

Section 12

Delivery Checklist

  1. APP_VERSION updated. Search for const APP_VERSION. Change string. Confirm one instance.
  2. Syntax check passes.sed -n '/<script>/,/<\/script>/p' file.html | sed '1d' | sed 's/<\/script>//' > /tmp/check.js && node --check /tmp/check.js
  3. Test suite passes. All tests green.
  4. Filename matches. loop2-stage2 (NNN).html where NNN = APP_VERSION.
  5. No unrelated changes. No drive-by refactoring, no bonus features.

Reference

Anti-Patterns

Anti-PatternWhyInstead Top-level let/var for sim stateInvisible to snapshotsAdd to L21 getElementById in hot pathDOM lookup every tickCache in EL at init onclick="..." in HTMLBypasses delegationdata-action attribute Copy-paste per-component functionsN copies that driftConfig table + generic function String comparison chains in hot pathSlow, error-pronePre-built Set/Map .map/.filter/.slice in hot pathAllocates arrays every tickManual for loops State in DOM elementsNot serializableStore in L21, read in render Modifying L21 in render functionsViolates one-way flowState changes in tick or operator actions only Creating new delegate functionsLegacy patternCall generic function directly Adding unrequested featuresScope creep, untestedBuild exactly what was asked for Forgetting captureFullSnapshotState lost on save/restoreAlways update both capture and restore Forgetting APP_VERSIONWrong version displayedFirst thing on every build