app #1 simpleasm: 从 oci 迁过来,asm.famzheng.me 已上线
- 后端 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:
@@ -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]
|
||||
@@ -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!' }
|
||||
}
|
||||
Reference in New Issue
Block a user