app #1 simpleasm: 从 oci 迁过来,asm.famzheng.me 已上线
deploy cube / build-and-deploy (push) Successful in 1m18s
deploy simpleasm / build-and-deploy (push) Successful in 1m45s

- 后端 FastAPI 重写为 axum + rusqlite (musl static, 2.8MB)
- 前端原样搬运 (Vue3 + Vite + Pinia + vue-router + vite-plugin-yaml)
- k8s: cube-simpleasm ns + 1Gi PVC (k3s local-path) + Recreate strategy
- CI: 复刻 deploy-cube.yml,按 apps/simpleasm/** 触发
- cube 门户里 simpleasm 状态从 pending 改成 live
- 数据冷启 (Fam 拍板不带历史进度)
This commit is contained in:
Fam Zheng
2026-05-04 15:12:22 +01:00
parent 5b2e53c040
commit 388b505e0b
40 changed files with 3985 additions and 3 deletions
@@ -0,0 +1,5 @@
const modules = import.meta.glob('./levels/*.yaml', { eager: true })
export const levels = Object.values(modules)
.map(m => m.default)
.sort((a, b) => a.id - b.id)
@@ -0,0 +1,50 @@
id: 1
title: Meet the Registers
subtitle: The robot's memory slots
description: Learn the MOV instruction to load values into registers
tutorial:
- title: What is a register?
text: >
The CPU is the brain of the computer, and **registers** are the tiny drawers
right next to it — the fastest storage there is! Our machine has 8 registers:
**R0** through **R7**.
- title: The MOV instruction
text: >
`MOV` puts a number into a register. Numbers are prefixed with **#** to mark
them as immediate values:
code: |
MOV R0, #42 ; put 42 into R0
MOV R1, #100 ; put 100 into R1
- title: The XHLT instruction
text: >
Every program ends with `XHLT` (halt = stop) to tell the machine "we're done!"
code: |
MOV R0, #42
XHLT
goal: Put the number **42** into the **R0** register
initialState: {}
testCases:
- init: {}
expected:
registers:
R0: 42
hints:
- "MOV format: MOV register, #number"
- "Try: MOV R0, #???"
- "Answer: MOV R0, #42 then XHLT"
starThresholds: [2, 3, 5]
starterCode: |
; Put 42 into the R0 register
; Tip: prefix numbers with #
XHLT
showMemory: false
@@ -0,0 +1,45 @@
id: 2
title: Data Mover
subtitle: Copying between registers
description: Learn how to copy data between registers
tutorial:
- title: Register-to-register copy
text: >
MOV can also **copy** the value of one register into another (no # needed):
code: |
MOV R1, R0 ; copy R0 into R1
- title: It's a copy, not a move!
text: >
Despite being called "MOV" (move), it's actually a **copy**. After it runs
R0 still holds its value, and R1 now matches it.
goal: R0 already holds **7** — copy it into both **R1** and **R2**
initialState:
registers:
R0: 7
testCases:
- init: {}
expected:
registers:
R0: 7
R1: 7
R2: 7
hints:
- "MOV register, register — copies right into left"
- "MOV R1, R0 copies R0 into R1"
- "Answer: MOV R1, R0 / MOV R2, R0 / XHLT"
starThresholds: [3, 4, 6]
starterCode: |
; R0 = 7
; Copy R0 into R1 and R2
XHLT
showMemory: false
@@ -0,0 +1,53 @@
id: 3
title: Add and Subtract
subtitle: The power of three operands
description: Learn the ADD and SUB instructions
tutorial:
- title: ADD — three-operand addition
text: >
ARM-style addition is neat: **three operands**! The first is the destination,
the other two are the values being combined:
code: |
ADD R2, R0, R1 ; R2 = R0 + R1
ADD R0, R0, #10 ; R0 = R0 + 10
- title: SUB — subtraction
text: >
SUB works the same way, three operands:
code: |
SUB R2, R0, R1 ; R2 = R0 - R1
SUB R0, R0, #5 ; R0 = R0 - 5
- title: Why three operands?
text: >
You can drop the result straight into a new register, **no extra copy needed**!
goal: R0=**15**, R1=**27** — compute R0+R1 into **R2** (R0 and R1 unchanged)
initialState:
registers:
R0: 15
R1: 27
testCases:
- init: {}
expected:
registers:
R0: 15
R1: 27
R2: 42
hints:
- "First operand of ADD is the destination, the next two are added"
- "ADD R2, R0, R1 — result goes in R2"
- "Answer: ADD R2, R0, R1 / XHLT"
starThresholds: [2, 3, 5]
starterCode: |
; R0=15, R1=27
; Compute R0 + R1 into R2
XHLT
showMemory: false
@@ -0,0 +1,47 @@
id: 4
title: Multiply and Divide
subtitle: Stronger arithmetic
description: Learn the MUL and XDIV instructions
tutorial:
- title: MUL — multiplication
text: >
MUL is also three operands:
code: |
MOV R0, #6
MOV R1, #7
MUL R2, R0, R1 ; R2 = 6 × 7 = 42
- title: XDIV — integer division
text: >
XDIV does integer division (the fractional part is discarded):
code: |
MOV R0, #100
MOV R1, #4
XDIV R2, R0, R1 ; R2 = 100 ÷ 4 = 25
goal: Compute **6 × 7** into R0 and **100 ÷ 4** into R1
initialState: {}
testCases:
- init: {}
expected:
registers:
R0: 42
R1: 25
hints:
- "MOV the numbers into registers first, then MUL/XDIV"
- "MUL R0, R2, R3 puts R2×R3 into R0"
- "Answer: MOV R2, #6 / MOV R3, #7 / MUL R0, R2, R3 / MOV R2, #100 / MOV R3, #4 / XDIV R1, R2, R3 / XHLT"
starThresholds: [7, 9, 12]
starterCode: |
; Compute 6×7 into R0
; Compute 100÷4 into R1
XHLT
showMemory: false
@@ -0,0 +1,58 @@
id: 5
title: Bitwise Magic
subtitle: The secret life of 0s and 1s
description: Learn the AND, ORR, EOR, and MVN instructions
tutorial:
- title: The binary world
text: >
Computers store everything as **0**s and **1**s. 42 in binary is `00101010`.
The state panel on the right shows the binary form of every register!
- title: AND — both must be 1
text: >
AND compares bit by bit, returning 1 only when both bits are 1. Great for
"extracting" specific bits:
code: |
; 11111111 (255)
; AND 00001111 (15)
; = 00001111 (15)
AND R0, R0, #15
- title: Other bitwise ops
text: >
**ORR** — 1 if either bit is 1 (OR)
**EOR** — 1 if the bits differ (XOR)
**MVN** — flip every bit (NOT)
code: |
ORR R0, R0, #240 ; set the upper 4 bits
EOR R0, R0, #255 ; flip the lower 8 bits
MVN R0, R0 ; flip every bit
goal: R0 = **255** (binary 11111111). Use AND to extract the **lower 4 bits** so R0 becomes **15**
initialState:
registers:
R0: 255
testCases:
- init: {}
expected:
registers:
R0: 15
hints:
- "AND keeps the bits you want and clears the rest"
- "The mask for the lower 4 bits is 15 (binary 00001111)"
- "Answer: AND R0, R0, #15 / XHLT"
starThresholds: [2, 3, 5]
starterCode: |
; R0 = 255 (binary 11111111)
; Use AND to extract the lower 4 bits
XHLT
showMemory: false
@@ -0,0 +1,54 @@
id: 6
title: Shift Operations
subtitle: A dance of bits
description: Learn the LSL and LSR instructions
tutorial:
- title: LSL — Logical Shift Left
text: >
All bits move left, zeros fill in on the right. **Shifting left by 1 = multiplying by 2**.
Shifting left by 3 = multiplying by 8:
code: |
; 5 = 00000101
LSL R0, R0, #1 ; 00001010 = 10 (×2)
LSL R0, R0, #1 ; 00010100 = 20 (×2)
- title: LSR — Logical Shift Right
text: >
All bits move right, zeros fill in on the left. **Shifting right by 1 = dividing by 2**:
code: |
MOV R0, #40
LSR R0, R0, #1 ; 20 (÷2)
LSR R0, R0, #2 ; 5 (÷4)
- title: A programmer's trick
text: >
On a real ARM CPU, shifts are far cheaper than multiply/divide!
`LSL R0, R0, #3` beats `MUL R0, R0, #8`.
goal: R0 = **5**. Use **shift only** to make it **40** (40 = 5 × 8 = 5 × 2³)
initialState:
registers:
R0: 5
testCases:
- init: {}
expected:
registers:
R0: 40
hints:
- "8 = 2³ — multiplying by 8 is shifting left by 3"
- "LSL R0, R0, #3"
- "It's just one instruction!"
starThresholds: [2, 3, 5]
blockedOps: [MUL, XDIV]
starterCode: |
; R0 = 5
; Use LSL to turn R0 into 40 (no MUL allowed)
XHLT
showMemory: false
@@ -0,0 +1,62 @@
id: 7
title: Memory Read and Write
subtitle: Open up the bigger storage
description: Learn the LDR and STR instructions
tutorial:
- title: What is memory?
text: >
8 registers isn't a lot! **Memory** is like a row of 256 lockers, each with
its own number (0-255).
- title: LDR — load from memory
text: >
Put the address into a register, then use `LDR` to read from there:
code: |
MOV R1, #0 ; address = 0
LDR R0, [R1] ; R0 = memory[0]
- title: STR — store into memory
text: >
`STR` writes a register's value into memory:
code: |
MOV R1, #5 ; address = 5
STR R0, [R1] ; memory[5] = R0
- title: Offset addressing
text: >
You can also add an offset: `[R1, #4]` means address R1+4:
code: |
MOV R1, #0
LDR R0, [R1, #0] ; memory[0]
LDR R2, [R1, #1] ; memory[1]
goal: memory[0]=**10**, memory[1]=**20** — store their sum into **memory[2]**
initialState:
memory:
0: 10
1: 20
testCases:
- init: {}
expected:
memory:
2: 30
hints:
- "LDR the values into registers, add them, then STR the result back"
- "MOV R3, #0 sets a base address; LDR R0, [R3, #0] reads the first value"
- "Answer: MOV R3, #0 / LDR R0, [R3, #0] / LDR R1, [R3, #1] / ADD R2, R0, R1 / STR R2, [R3, #2] / XHLT"
starThresholds: [6, 8, 10]
starterCode: |
; memory[0]=10, memory[1]=20
; Compute the sum and store into memory[2]
;
; Tip: MOV an address into a register first
; then use LDR/STR to read/write memory
XHLT
showMemory: true
memoryRange: [0, 15]
@@ -0,0 +1,79 @@
id: 8
title: Compare and Branch
subtitle: Letting the program decide
description: Learn CMP and conditional branch instructions
tutorial:
- title: Up to now...
text: >
Programs ran top to bottom in order. With **branching**, the program can
finally make decisions!
- title: CMP — compare
text: >
`CMP` compares two values and remembers the result (the values themselves
are unchanged):
code: |
CMP R0, #10 ; compare R0 with 10
- title: Conditional branches
text: >
After comparing, use **B** (Branch) to jump:
code: |
BEQ label ; jump if Equal
BNE label ; jump if Not Equal
BGT label ; jump if Greater Than
BLT label ; jump if Less Than
B label ; unconditional jump
- title: Labels
text: >
A **label** is a marker in your code that branches jump to. Add a colon after the name:
code: |
CMP R0, #10
BGT big
MOV R1, #0 ; R0 <= 10
B done ; skip the next part
big:
MOV R1, #1 ; R0 > 10
done:
XHLT
goal: R0=**15**. If R0 > 10 then R1 = **1**; otherwise R1 = **0**
initialState:
registers:
R0: 15
testCases:
- init:
registers:
R0: 15
expected:
registers:
R1: 1
- init:
registers:
R0: 5
expected:
registers:
R1: 0
- init:
registers:
R0: 10
expected:
registers:
R1: 0
hints:
- "Default R1 to 0, then compare R0 with 10"
- "If R0 > 10, jump to a label that sets R1 to 1"
- "Answer: MOV R1, #0 / CMP R0, #10 / BLE done / MOV R1, #1 / done: XHLT"
starThresholds: [5, 7, 9]
starterCode: |
; If R0 > 10 then R1 = 1
; Otherwise R1 = 0
XHLT
showMemory: false
@@ -0,0 +1,52 @@
id: 9
title: Loops
subtitle: The power of repetition
description: Use branches to build a loop
tutorial:
- title: What is a loop?
text: >
A loop runs the same code **over and over**. In assembly, a loop is just
**branching back to a label**!
- title: Loop structure
text: >
① initialize ② do work ③ update the counter ④ test + branch back:
code: |
MOV R4, #0 ; ① initialize
loop: ; loop start
ADD R4, R4, #1 ; ②③ counter += 1
CMP R4, #5 ; ④ reached 5?
BLE loop ; not yet, jump back
XHLT
- title: Watch out!
text: >
Forget to update the counter and you've made an **infinite loop**
(don't worry — execution stops automatically after 10000 steps).
goal: Compute **1+2+3+...+10** into **R0** (the answer is 55)
initialState: {}
testCases:
- init: {}
expected:
registers:
R0: 55
hints:
- "R0 accumulates the sum, R4 is the counter (1 to 10)"
- "Loop body: ADD R0, R0, R4 / ADD R4, R4, #1 / CMP R4, #10 / BLE loop"
- "Full: MOV R0, #0 / MOV R4, #1 / loop: ADD R0, R0, R4 / ADD R4, R4, #1 / CMP R4, #10 / BLE loop / XHLT"
starThresholds: [7, 9, 12]
starterCode: |
; Compute 1+2+3+...+10
; Result goes in R0
;
; Tip: use a register as the counter
XHLT
showMemory: false
@@ -0,0 +1,81 @@
id: 10
title: Final Challenge
subtitle: Find the maximum
description: Bring all your skills together!
tutorial:
- title: The last level!
text: >
You've learned registers, arithmetic, bitwise ops, memory, branches and loops.
Now combine **all of it**!
- title: The challenge
text: >
Memory addresses 0-4 hold five numbers. Find the **maximum value** and its
**position**. You'll need: a loop + memory reads + compare-and-branch.
- title: Approach
text: |
1. Assume the first number is the largest (R0 = memory[0], R1 = position 0)
2. Loop through the remaining numbers
3. If a larger one shows up, update both the max and its position
4. Continue until all 5 numbers are checked
code: |
; Pseudocode:
; R0 = max = mem[0]
; R1 = maxIdx = 0
; for R4 = 1 to 4:
; R5 = mem[R4]
; if R5 > R0: R0=R5, R1=R4
goal: memory[0..4] holds 5 numbers — store the **maximum** in **R0** and its **position** in **R1**
initialState:
memory:
0: 5
1: 3
2: 8
3: 1
4: 7
testCases:
- init:
memory:
0: 5
1: 3
2: 8
3: 1
4: 7
expected:
registers:
R0: 8
R1: 2
- init:
memory:
0: 1
1: 9
2: 4
3: 9
4: 2
expected:
registers:
R0: 9
R1: 1
hints:
- "R0 = max, R1 = position, R4 = loop counter, R5 = current value"
- "Use MOV R3, R4 / LDR R5, [R3] to read mem[R4]"
- "Loop body: MOV R3, R4 / LDR R5, [R3] / CMP R5, R0 / BLE skip / MOV R0, R5 / MOV R1, R4 / skip: ADD R4, R4, #1 / CMP R4, #5 / BLT loop"
starThresholds: [12, 15, 20]
starterCode: |
; memory[0..4] = [5, 3, 8, 1, 7]
; Find the max into R0, the position into R1
;
; Tip: use R4 as the loop variable
; use MOV + LDR to read memory
XHLT
showMemory: true
memoryRange: [0, 15]
+375
View File
@@ -0,0 +1,375 @@
const REGISTERS = ['R0','R1','R2','R3','R4','R5','R6','R7']
const OPCODES = [
'MOV','ADD','SUB','MUL','XDIV','XMOD',
'AND','ORR','EOR','MVN','LSL','LSR',
'CMP','B','BEQ','BNE','BGT','BLT','BGE','BLE',
'LDR','STR','PUSH','POP','XOUT','XHLT','NOP',
]
const BRANCH_OPS = ['B','BEQ','BNE','BGT','BLT','BGE','BLE']
const MAX_STEPS = 10000
function parseNumber(s) {
s = s.trim()
if (/^0x[0-9a-fA-F]+$/i.test(s)) return parseInt(s, 16)
if (/^0b[01]+$/i.test(s)) return parseInt(s.slice(2), 2)
if (/^-?\d+$/.test(s)) return parseInt(s, 10)
return null
}
function splitOperands(str) {
const result = []; let cur = ''; let depth = 0
for (const ch of str) {
if (ch === '[' || ch === '{') depth++
if (ch === ']' || ch === '}') depth--
if (ch === ',' && depth === 0) { result.push(cur.trim()); cur = '' }
else cur += ch
}
if (cur.trim()) result.push(cur.trim())
return result
}
function parseOperand(s) {
s = s.trim()
const upper = s.toUpperCase()
if (REGISTERS.includes(upper)) return { type: 'reg', value: upper }
// Immediate: #42, #0xFF, #0b101
if (s.startsWith('#')) {
const n = parseNumber(s.slice(1))
if (n !== null) return { type: 'imm', value: n }
}
// Memory: [R0], [R0, #4], [#addr]
const memMatch = s.match(/^\[([^\],]+)(?:,\s*(.+))?\]$/)
if (memMatch) {
const base = memMatch[1].trim()
const baseUp = base.toUpperCase()
if (REGISTERS.includes(baseUp)) {
let offset = 0
if (memMatch[2]) {
let off = memMatch[2].trim()
if (off.startsWith('#')) off = off.slice(1)
offset = parseNumber(off) || 0
}
return { type: 'mem', base: baseUp, offset }
}
// [#addr] direct
let addr = base.startsWith('#') ? base.slice(1) : base
const n = parseNumber(addr)
if (n !== null) return { type: 'mem_direct', addr: n & 0xFF }
}
// {R0} for PUSH/POP
const braceMatch = s.match(/^\{(.+)\}$/)
if (braceMatch) {
const inner = braceMatch[1].trim().toUpperCase()
if (REGISTERS.includes(inner)) return { type: 'reg', value: inner }
}
// Label
if (/^[a-zA-Z_]\w*$/.test(s)) return { type: 'label', value: s.toLowerCase() }
// Bare number (lenient)
const n = parseNumber(s)
if (n !== null) return { type: 'imm', value: n }
return { type: 'unknown', raw: s }
}
export function parse(code) {
const lines = code.split('\n')
const instructions = []
const labels = {}
const errors = []
// Pass 1: find labels
let idx = 0
for (let i = 0; i < lines.length; i++) {
let line = lines[i]
const ci = line.indexOf(';'); if (ci >= 0) line = line.slice(0, ci)
line = line.trim(); if (!line) continue
const lm = line.match(/^([a-zA-Z_]\w*)\s*:(.*)$/)
if (lm) { labels[lm[1].toLowerCase()] = idx; line = lm[2].trim(); if (!line) continue }
idx++
}
// Pass 2: parse instructions
for (let i = 0; i < lines.length; i++) {
let line = lines[i]
const ci = line.indexOf(';'); if (ci >= 0) line = line.slice(0, ci)
line = line.trim(); if (!line) continue
const lm = line.match(/^([a-zA-Z_]\w*)\s*:(.*)$/)
if (lm) { line = lm[2].trim(); if (!line) continue }
const parts = line.match(/^(\w+)(?:\s+(.*))?$/)
if (!parts) { errors.push({ line: i, msg: `Syntax error: ${lines[i].trim()}` }); continue }
const opcode = parts[1].toUpperCase()
if (!OPCODES.includes(opcode)) { errors.push({ line: i, msg: `Unknown instruction: ${parts[1]}` }); continue }
let operands = []
if (parts[2]) {
operands = splitOperands(parts[2]).map(parseOperand)
const bad = operands.find(o => o.type === 'unknown')
if (bad) { errors.push({ line: i, msg: `Unrecognized operand: ${bad.raw}` }); continue }
}
instructions.push({ opcode, operands, srcLine: i })
}
return { instructions, labels, errors }
}
export function createVM() {
const state = {
registers: { R0:0,R1:0,R2:0,R3:0,R4:0,R5:0,R6:0,R7:0 },
flags: { zero:false, carry:false, negative:false, overflow:false },
memory: new Array(256).fill(0),
stack: [],
pc: 0, halted: false, error: null,
output: [], input: [],
stepCount: 0, instructions: [], labels: {},
cmpA: 0, cmpB: 0,
}
function getVal(op) {
switch (op.type) {
case 'reg': return state.registers[op.value]
case 'imm': return op.value
case 'mem': return state.memory[(state.registers[op.base] + (op.offset||0)) & 0xFF]
case 'mem_direct': return state.memory[op.addr]
default: throw new Error(`Cannot read: ${JSON.stringify(op)}`)
}
}
function setReg(op, val, changes) {
val = ((val % 65536) + 65536) % 65536
if (op.type !== 'reg') throw new Error('Destination must be a register')
const old = state.registers[op.value]
state.registers[op.value] = val
changes.push({ type: 'reg', name: op.value, old, val })
}
function setMem(addr, val, changes) {
addr = addr & 0xFF
val = ((val % 65536) + 65536) % 65536
const old = state.memory[addr]
state.memory[addr] = val
changes.push({ type: 'mem', addr, old, val })
}
function updateFlags(val) {
val = ((val % 65536) + 65536) % 65536
state.flags.zero = val === 0
state.flags.negative = (val & 0x8000) !== 0
}
function condMet(op) {
const a = state.cmpA, b = state.cmpB
switch (op) {
case 'B': return true
case 'BEQ': return a === b
case 'BNE': return a !== b
case 'BGT': return a > b
case 'BLT': return a < b
case 'BGE': return a >= b
case 'BLE': return a <= b
}
return false
}
// For 2-or-3 operand arithmetic: if 3 ops → Rd = op(Rn, Rm); if 2 ops → Rd = op(Rd, Rm)
function arith3(ops, fn) {
if (ops.length >= 3) return fn(getVal(ops[1]), getVal(ops[2]))
return fn(getVal(ops[0]), getVal(ops[1]))
}
function step() {
if (state.halted || state.error) return null
if (state.pc >= state.instructions.length) { state.halted = true; return null }
if (state.stepCount >= MAX_STEPS) {
state.error = 'Exceeded maximum step count (10000) — likely an infinite loop!'; return null
}
const instr = state.instructions[state.pc]
const { opcode, operands: ops } = instr
const changes = []
let jumped = false
state.stepCount++
try {
switch (opcode) {
case 'NOP': break
case 'XHLT': state.halted = true; break
case 'MOV':
setReg(ops[0], getVal(ops[1]), changes)
break
case 'ADD': { const r = arith3(ops, (a,b) => a+b); state.flags.carry = r > 0xFFFF; setReg(ops[0], r, changes); updateFlags(r); break }
case 'SUB': { const r = arith3(ops, (a,b) => a-b); state.flags.carry = r < 0; setReg(ops[0], r, changes); updateFlags(r); break }
case 'MUL': { const r = arith3(ops, (a,b) => a*b); state.flags.carry = r > 0xFFFF; setReg(ops[0], r, changes); updateFlags(r); break }
case 'XDIV': {
const r = arith3(ops, (a,b) => { if(b===0) throw new Error('Division by zero!'); return Math.floor(a/b) })
setReg(ops[0], r, changes); updateFlags(r); break
}
case 'XMOD': {
const r = arith3(ops, (a,b) => { if(b===0) throw new Error('Division by zero!'); return a%b })
setReg(ops[0], r, changes); updateFlags(r); break
}
case 'AND': { const r = arith3(ops, (a,b) => a&b); setReg(ops[0], r, changes); updateFlags(r); break }
case 'ORR': { const r = arith3(ops, (a,b) => a|b); setReg(ops[0], r, changes); updateFlags(r); break }
case 'EOR': { const r = arith3(ops, (a,b) => a^b); setReg(ops[0], r, changes); updateFlags(r); break }
case 'MVN': { const r = (~getVal(ops[1])) & 0xFFFF; setReg(ops[0], r, changes); updateFlags(r); break }
case 'LSL': { const r = arith3(ops, (a,b) => (a << b) & 0xFFFF); setReg(ops[0], r, changes); updateFlags(r); break }
case 'LSR': { const r = arith3(ops, (a,b) => a >>> b); setReg(ops[0], r, changes); updateFlags(r); break }
case 'CMP': {
state.cmpA = getVal(ops[0]); state.cmpB = getVal(ops[1])
const d = state.cmpA - state.cmpB
state.flags.zero = d === 0; state.flags.negative = d < 0; state.flags.carry = state.cmpA < state.cmpB
break
}
case 'B': case 'BEQ': case 'BNE': case 'BGT': case 'BLT': case 'BGE': case 'BLE': {
if (condMet(opcode)) {
const lbl = ops[0].value
if (state.labels[lbl] === undefined) throw new Error(`Unknown label: ${lbl}`)
state.pc = state.labels[lbl]; jumped = true
}
break
}
case 'LDR': {
const memOp = ops[1]
let addr
if (memOp.type === 'mem') addr = (state.registers[memOp.base] + (memOp.offset||0)) & 0xFF
else if (memOp.type === 'mem_direct') addr = memOp.addr
else throw new Error('LDR needs a memory address, e.g. [R0] or [R0, #4]')
const v = state.memory[addr]
setReg(ops[0], v, changes)
changes.push({ type: 'mem_read', addr })
break
}
case 'STR': {
const memOp = ops[1]
let addr
if (memOp.type === 'mem') addr = (state.registers[memOp.base] + (memOp.offset||0)) & 0xFF
else if (memOp.type === 'mem_direct') addr = memOp.addr
else throw new Error('STR needs a memory address, e.g. [R0] or [R0, #4]')
setMem(addr, getVal(ops[0]), changes)
break
}
case 'PUSH': {
if (state.stack.length >= 64) throw new Error('Stack overflow!')
const v = getVal(ops[0])
state.stack.push(v)
changes.push({ type: 'stack_push', val: v })
break
}
case 'POP': {
if (state.stack.length === 0) throw new Error('Stack is empty!')
setReg(ops[0], state.stack.pop(), changes)
changes.push({ type: 'stack_pop' })
break
}
case 'XOUT': {
const v = getVal(ops[0])
state.output.push(v)
changes.push({ type: 'output', val: v })
break
}
}
} catch (e) {
state.error = `Line ${instr.srcLine + 1}: ${e.message}`
return { pc: state.pc, instr, changes, error: state.error }
}
if (!jumped) state.pc++
return { pc: state.pc, instr, changes, srcLine: instr.srcLine }
}
function loadProgram(code) {
const result = parse(code)
if (result.errors.length > 0) { state.error = result.errors[0].msg; return result }
state.instructions = result.instructions
state.labels = result.labels
state.pc = 0; state.halted = false; state.error = null; state.stepCount = 0
state.output = []; state.stack = []
state.flags = { zero:false, carry:false, negative:false, overflow:false }
state.cmpA = 0; state.cmpB = 0
return result
}
function run() {
while (!state.halted && !state.error && state.stepCount < MAX_STEPS) step()
if (state.stepCount >= MAX_STEPS && !state.halted && !state.error)
state.error = 'Exceeded maximum step count (10000) — likely an infinite loop!'
}
function reset() {
state.registers = { R0:0,R1:0,R2:0,R3:0,R4:0,R5:0,R6:0,R7:0 }
state.flags = { zero:false, carry:false, negative:false, overflow:false }
state.memory = new Array(256).fill(0)
state.stack = []; state.pc = 0; state.halted = false; state.error = null
state.output = []; state.input = []; state.stepCount = 0
state.cmpA = 0; state.cmpB = 0
}
return { state, step, run, loadProgram, reset }
}
export function countInstructions(code) {
let c = 0
for (const line of code.split('\n')) {
let l = line; const ci = l.indexOf(';'); if (ci >= 0) l = l.slice(0, ci); l = l.trim()
if (!l) continue
const lm = l.match(/^[a-zA-Z_]\w*\s*:(.*)$/); if (lm) { l = lm[1].trim(); if (!l) continue }
c++
}
return c
}
export function validateLevel(level, vm) {
const s = vm.state
if (s.error) return { passed: false, msg: s.error }
for (const tc of level.testCases) {
vm.reset()
if (level.initialState) {
if (level.initialState.registers) Object.assign(vm.state.registers, level.initialState.registers)
if (level.initialState.memory) for (const [a,v] of Object.entries(level.initialState.memory)) vm.state.memory[+a] = v
if (level.initialState.input) vm.state.input = [...level.initialState.input]
}
if (tc.init) {
if (tc.init.registers) Object.assign(vm.state.registers, tc.init.registers)
if (tc.init.memory) for (const [a,v] of Object.entries(tc.init.memory)) vm.state.memory[+a] = v
if (tc.init.input) vm.state.input = [...tc.init.input]
}
vm.run()
if (vm.state.error) return { passed: false, msg: vm.state.error }
const exp = tc.expected
if (exp.registers) {
for (const [r,v] of Object.entries(exp.registers)) {
if (vm.state.registers[r] !== v)
return { passed: false, msg: `${r} should be ${v}, but got ${vm.state.registers[r]}` }
}
}
if (exp.memory) {
for (const [a,v] of Object.entries(exp.memory)) {
if (vm.state.memory[+a] !== v)
return { passed: false, msg: `memory[${a}] should be ${v}, but got ${vm.state.memory[+a]}` }
}
}
if (exp.output) {
for (let i = 0; i < exp.output.length; i++) {
if (vm.state.output[i] !== exp.output[i])
return { passed: false, msg: `Output #${i+1} should be ${exp.output[i]}, but got ${vm.state.output[i] ?? 'none'}` }
}
}
}
return { passed: true, msg: 'Passed!' }
}