{"id":20760850,"url":"https://github.com/barryw/sim6502","last_synced_at":"2026-04-12T16:16:47.903Z","repository":{"id":125485922,"uuid":"247335720","full_name":"barryw/sim6502","owner":"barryw","description":"6502 Unit Test CLI","archived":false,"fork":false,"pushed_at":"2026-01-27T13:57:49.000Z","size":4361,"stargazers_count":12,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-27T16:09:03.344Z","etag":null,"topics":["6502","6502-assembly","commodore-64","unit-testing"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/barryw.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2020-03-14T18:56:21.000Z","updated_at":"2026-01-27T13:58:03.000Z","dependencies_parsed_at":"2023-08-11T17:46:49.333Z","dependency_job_id":null,"html_url":"https://github.com/barryw/sim6502","commit_stats":null,"previous_names":[],"tags_count":34,"template":false,"template_full_name":null,"purl":"pkg:github/barryw/sim6502","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barryw%2Fsim6502","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barryw%2Fsim6502/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barryw%2Fsim6502/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barryw%2Fsim6502/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/barryw","download_url":"https://codeload.github.com/barryw/sim6502/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barryw%2Fsim6502/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29145566,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-06T01:13:33.096Z","status":"online","status_checked_at":"2026-02-06T02:00:08.092Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["6502","6502-assembly","commodore-64","unit-testing"],"created_at":"2024-11-17T10:15:58.222Z","updated_at":"2026-04-12T16:16:47.893Z","avatar_url":"https://github.com/barryw.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sim6502 - 6502 Assembly Testing Framework\n        __ _____  ___ ___    _    _       _ _     _______        _      _____ _      _____\n       / /| ____|/ _ \\__ \\  | |  | |     (_) |   |__   __|      | |    / ____| |    |_   _|\n      / /_| |__ | | | | ) | | |  | |_ __  _| |_     | | ___  ___| |_  | |    | |      | |\n     | '_ \\___ \\| | | |/ /  | |  | | '_ \\| | __|    | |/ _ \\/ __| __| | |    | |      | |\n     | (_) |__) | |_| / /_  | |__| | | | | | |_     | |  __/\\__ \\ |_  | |____| |____ _| |_\n      \\___/____/ \\___/____|  \\____/|_| |_|_|\\__|    |_|\\___||___/\\__|  \\_____|______|_____|\n\n\n![.NET Core](https://github.com/barryw/sim6502/workflows/.NET%20Core/badge.svg)\n\n#### Introduction\n\nThis is a tool to help you unit test your 6502 assembly language programs. There's no valid reason why your 6502 programs shouldn't receive the same DevOps treatment as the rest of your modern applications.\n\nIt works by running your assembled programs with a 6502 simulator and then allowing you to make assertions on memory and CPU state. It's very similar to other unit test tools.\n\nA minimal test suite looks like this:\n\n```\nsuites {\n  suite(\"Tests against hardware register library\") {\n    ; Load the program under test\n    symbols(\"/code/include_me_full_r.sym\")\n    load(\"/code/include_me_full_r.prg\")\n    load(\"/code/kernal.rom\", address = $e000)\n\n    test(\"sprites-positions-correctly-without-msb\",\"Sprite X pos \u003c 256 sets MSB to 0\") {\n      x = $00\n      a = $ff\n      y = $00\n      $02 = $40\n      [vic.MSIGX] = $00\n\n      jsr([PositionSprite], stop_on_rts = true, fail_on_brk = true)\n\n      assert([vic.SP0X]   == $ff, \"Sprite 0's X pos is at ff\")\n      assert([vic.SP0Y]   == $40, \"Sprite 0's Y pos is at $40\")\n      assert([vic.MSIGX]  == $00, \"And sprite 0's MSB is set to 0\")\n    }\n  }\n\n  suite(\"Tests against pseudo register library\") {\n    ; Load the program under test\n    symbols(\"/code/include_me_full.sym\")\n    load(\"/code/include_me_full.prg\", strip_header = true)\n    load(\"/code/kernal.rom\", address = $e000)\n\n    test(\"memory-fill-1\", \"Fill an uneven block of memory\") {\n      [r0L] = $bd   ; Stuff $bd into our memory locations. Odd number, right?\n      [r1] = $1234  ; Start at $1234\n      [r2] = $12c   ; and do 300 bytes\n\n      jsr([FillMemory], stop_on_rts = true, fail_on_brk = true)\n\n      assert(cycles \u003c 11541, \"We can fill this block in fewer than 11541 cycles\")\n      assert(memchk($1234, $12c, $bd), \"Memory was filled properly\")\n    }\n  }\n}\n\n```\n\nEach file can contain one or more `suite`s and each suite can contain one more more `test`s. Each suite is tied to a set of binary object files that are the subjects to be tested.\n\nStart your suite by giving it a name. Call it whatever you'd like since the name isn't significant. It's used to identify the suite in output.\n\n```\nsuites {\n  suite(\"My awesome new test suite! Sweet!\") {\n  }\n}\n```\n\nInside the suite you'll first need to define the programs that you'd like to test. You can also load other things that your test code may need. You can include things like the C64 KERNAL, BASIC, etc.\n\n```\nsuites {\n  suite(\"My awesome new test suite! Sweet!\") {\n    load(\"/code/include_me_full.prg\", strip_header = true)\n    load(\"/code/kernal.rom\", address = $e000)\n  }\n}\n```\n\nIf you don't specify the binary file's `address`, it will be inferred by looking at the first 2 bytes of the file. These will be the 16-bit load address. If you don't specify `address`, then you'll need to specify `strip_header = true` so that those 2 bytes are removed before loading to memory.\n\nYou can also include a kickassembler symbol file so that you can use symbol references instead of hardcoded values and addresses.\n\n```\nsuites {\n  suite(\"My awesome new test suite! Sweet!\") {\n    symbols(\"/code/include_me_full.sym\")\n    load(\"/code/include_me_full.prg\", strip_header = true)\n    load(\"/code/kernal.rom\", address = $e000)\n  }\n}\n```\n\nNext, start writing your tests. Tests have 3 main blocks: memory assignment, calling subroutines, assertions. You use the memory assignment area to set things up to test your code. Once memory is set up, you can then call subroutines in the code you want to test. When the subroutine's exit condition is reached, assertions will be performed to make sure your code did what it was supposed to. Here's an example:\n\n```\nsuites {\n  suite(\"My awesome new test suite! Sweet!\") {\n    ; Any line that starts with a semi-colon is treated as a comment\n    symbols(\"/code/include_me_full.sym\")\n    load(\"/code/include_me_full.prg\", strip_header = true)\n    load(\"/code/kernal.rom\", address = $e000)\n\n    test(\"memory-fill-1\", \"Fill an uneven block of memory\") {\n      [r0L] = $bd   ; Stuff $bd into our memory locations. Odd number, right?\n      [r1] = $1234  ; Start at $1234\n      [r2] = $12c   ; and do 300 bytes\n\n      jsr([FillMemory], stop_on_rts = true, fail_on_brk = true)\n\n      assert(cycles \u003c 11541, \"We can fill this block in fewer than 11541 cycles\")\n      assert(memchk($1234, $12c, $bd), \"Memory was filled properly\")\n    }\n  }\n}\n```\n\nThis code contains a single test in a single suite. The test is named `memory-fill-1` and has a description of `Fill an uneven block of memory`. These are not significant and are only used to identify the test in output.\n\nThe first 3 lines of the test are memory assignment lines. They refer to symbols contained in the symbol file `include_me_full.sym`, which must exist or the test fails.\n\nOnce these symbols are resolved to a location, the value to the right of the equals sign is placed in that memory location. If the value is \u003e 255, then a 16-bit word is placed at the symbol location and symbol location +1 ([r1] \u0026 [r1] + 1).\n\nThe `jsr` line will start executing code starting at the address given, which in this case is a symbol called `FillMemory`. The `jsr` function can take 3 parameters:\n\n- stop_on_rts = true|false : Whether to stop executing when a `rts` instruction is encountered. The code will only return if the `rts` instruction exists at the same level as the subroutine. For example, if your subroutine calls other routines, those `rts` calls won't trigger the jsr to exit.\n- stop_on_address = address : If specified, the jsr call will return when the program counter reaches this address.\n- fail_on_brk = true|false : Whether to fail the test if a `brk` instruction is encountered.\n\n\nOnce your subroutine exits, the assertions will be run in order. You can assert any of these things:\n\n- memory location values using either the address or a symbol reference (eg. `[vic.SP0X] == $01` or `$3000 == $80`)\n- processor cycle counts to ensure that code performs as expected (eg. `cycles \u003c 80`)\n- memory compares to verify that copy operations work correctly (eg. `assert(memcmp($e000, $4000, $2000), \"Ensure that KERNAL was copied correctly\")`)\n- memory check to verify that fill operations work correctly (eg. `assert(memchk($1234, $12c, $bd), \"Memory was filled properly\")`)\n\nThe expression syntax is very flexible so that you can do things like this:\n\n```\nassert([c64lib_timers] + $00 + peekbyte([r2H]) * 8 == [ENABLE], \"Timer enabled\")\nassert([c64lib_timers] + $01 + peekbyte([r2H]) * 8 == [TIMER_SINGLE], \"Timer type is single\")\nassert(([c64lib_timers] + $02 + peekbyte([r2H]) * 8).w == $1000, \"Timer's current value\")\nassert(([c64lib_timers] + $04 + peekbyte([r2H]) * 8).w == $1000, \"Timer's frequency\")\nassert(([c64lib_timers] + $06 + peekbyte([r2H]) * 8).w == [ReadJoysticks], \"Timer's callback address\")\n```\n\nYou can tell the CLI whether to return a byte or a word from a memory location by using the `.b` and `.w` suffix on the address expression. By default, a byte will be returned. (eg. `[MyVector].w`)\n\nYou can also return the hi and lo byte for 16-bit number by using the `.l` and `.h` suffix (eg. `[MyVector].h`)\n\n#### Running\n\nYou'll need to have the following things available to the test CLI:\n\n- Your assembled 6502 program in .prg format. If the first 2 bytes don't contain the load address, then you'll need to specify the address with the `address` parameter.\n- Your Kickassembler symbol file. While not required, makes testing a LOT easier!\n- Your test file\n\nRun the CLI with:\n\n```bash\ndotnet Sim6502TestRunner.dll -s {path to your test script}\n```\n\nIf all of your tests pass, the CLI will exit with a return code of 0. If any tests fail, it will return with a 1.\n\nIf you'd like to see the assembly language instructions that it executes while running your tests, add the `-t` flag:\n\n```bash\ndotnet Sim6502TestRunner.dll -t -s {path to your test script}\n```\n\n### Test Filtering\n\nRun a subset of tests using these CLI options:\n\n```bash\n# Run tests matching a glob pattern\ndotnet Sim6502TestRunner.dll -s tests.6502 --filter \"castle*\"\n\n# Run a single test by exact name\ndotnet Sim6502TestRunner.dll -s tests.6502 --test \"attack-knight-center\"\n\n# Run tests with specific tags (OR logic)\ndotnet Sim6502TestRunner.dll -s tests.6502 --filter-tag \"smoke,regression\"\n\n# Exclude tests with certain tags\ndotnet Sim6502TestRunner.dll -s tests.6502 --exclude-tag \"slow\"\n\n# List matching tests without running them\ndotnet Sim6502TestRunner.dll -s tests.6502 --filter \"castle*\" --list\n\n# Combine filters\ndotnet Sim6502TestRunner.dll -s tests.6502 --filter \"castle*\" --filter-tag \"regression\"\n```\n\n| Option | Description |\n|--------|-------------|\n| `--filter \u003cpattern\u003e` | Glob pattern to match test names (e.g., `\"castle*\"`, `\"*move*\"`) |\n| `--test \u003cname\u003e` | Run single test by exact name (highest priority) |\n| `--filter-tag \u003ctags\u003e` | Comma-separated tags; tests matching ANY tag run (OR logic) |\n| `--exclude-tag \u003ctags\u003e` | Exclude tests with any of these tags |\n| `--list` | List matching tests without running them |\n\n**Filter Precedence:**\n1. `--test` (exact match) takes priority\n2. `--filter` (glob) narrows the set\n3. `--filter-tag` further narrows to matching tags\n4. `--exclude-tag` removes from final set\n\nThere is also a Docker image if you'd like to not have to mess with installing the .NET Core framework. You can run it like this:\n\n```bash\ndocker run -v ${PWD}:/code -it ghcr.io/barryw/sim6502:latest -s /code/{your test script} -t\n```\n\nThat would mount the current directory to a directory in the container called `/code` and would expect to see all of your artifacts there, unless you've given them absolute paths. Just make sure you update your test script to point to the correct location of any roms and programs.\n\nIf you'd like to see a larger example of this tool in action, run `make` from the `example` folder. It's the test suite from my c64lib project.\n\n### VICE Backend (Hardware-Accurate Testing)\n\nBy default, sim6502 uses its internal CPU simulator. For hardware-accurate testing that includes VIC-II, SID, CIA, and interrupt behavior, you can run tests against the [VICE](https://vice-emu.sourceforge.io/) emulator via its embedded MCP server.\n\nYour existing test files run unmodified — only the execution backend changes.\n\n```bash\n# Run against a VICE instance already running with MCP enabled\ndotnet Sim6502TestRunner.dll -s tests.6502 --backend vice\n\n# Auto-launch VICE and run tests\ndotnet Sim6502TestRunner.dll -s tests.6502 --backend vice --launch-vice\n\n# Connect to VICE on a custom host/port\ndotnet Sim6502TestRunner.dll -s tests.6502 --backend vice --vice-host 192.168.1.100 --vice-port 7000\n\n# Disable warp mode to watch execution in real time\ndotnet Sim6502TestRunner.dll -s tests.6502 --backend vice --vice-warp false\n```\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `--backend \u003ctype\u003e` | `sim` | `sim`, `vice`, `novavm`, or `verilator` |\n| `--vice-host \u003chost\u003e` | `127.0.0.1` | VICE MCP server host |\n| `--vice-port \u003cport\u003e` | `6510` | VICE MCP server port |\n| `--vice-timeout \u003cms\u003e` | `5000` | Per-test execution timeout in milliseconds |\n| `--vice-warp \u003cbool\u003e` | `true` | Enable warp mode for faster test execution |\n| `--launch-vice` | `false` | Auto-launch VICE with MCP server enabled |\n\n**Prerequisites:** The mainstream VICE source does not include MCP server support. You must build from the [vice-mcp](https://github.com/barryw/vice-mcp) fork, `feature/mcp-server` branch:\n\n```bash\ngit clone -b feature/mcp-server https://github.com/barryw/vice-mcp.git\ncd vice-mcp\n./autogen.sh\n./configure --enable-mcp-server\nmake -j$(nproc)\n```\n\nThen start VICE with:\n\n```bash\nx64sc -mcpserver -mcpserverport 6510\n```\n\n**Test isolation:** Each test suite saves a VICE snapshot after loading binaries. Before each test, the snapshot is restored to ensure a clean state. This is faster than a full machine reset.\n\n**When to use each backend:**\n- **Internal simulator (`sim`):** Fast, deterministic, sufficient for pure computational logic\n- **VICE (`vice`):** Hardware-accurate Commodore emulation — interrupt timing, CIA timers, VIC-II raster effects, memory banking\n- **NovaVM (`novavm`):** Full NovaVM emulator — VGC graphics, SID sound, sprites, blitter, tile engine, copper, DMA, expansion memory\n- **Verilator (`verilator`):** FPGA RTL simulation via Verilator — cycle-accurate hardware verification of the NovaVM design\n\n### NovaVM Backend (NovaVM Emulator Integration)\n\nThe `novavm` and `verilator` backends connect to a NovaVM-compatible system (the e6502 Avalonia emulator or the FPGA Verilator testbench) via a TCP JSON protocol. These backends provide a high-level DSL for testing BASIC programs against the full hardware stack — graphics, sound, sprites, tile maps, DMA, and expansion memory.\n\n```bash\n# Run against the Avalonia emulator (default port 6502)\nsim6502 -s tests/integration/vgc.6502 --backend novavm\n\n# Run against the FPGA Verilator simulation (default port 6503)\nsim6502 -s tests/integration/vgc.6502 --backend verilator\n\n# Custom host/port\nsim6502 -s tests/integration/vgc.6502 --backend novavm --novavm-port 7000\n```\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `--backend novavm` | | Connect to Avalonia emulator on port 6502 |\n| `--backend verilator` | | Connect to Verilator testbench on port 6503 |\n| `--novavm-host \u003chost\u003e` | `127.0.0.1` | NovaVM/Verilator TCP server host |\n| `--novavm-port \u003cport\u003e` | `6502` | TCP server port (auto-overridden to 6503 for verilator backend) |\n| `--novavm-timeout \u003cms\u003e` | `10000` | Timeout for wait operations |\n\n#### Getting Started\n\n**1. Start the target system.** The emulator or FPGA sim must be running before you launch tests.\n\nFor the Avalonia emulator:\n```bash\ndotnet run --project e6502.Avalonia\n```\n\nFor the FPGA Verilator simulation:\n```bash\ncd e6502.FPGA \u0026\u0026 make \u0026\u0026 ./e6502_sim --port 6503\n```\n\n**2. Write a test file.** NovaVM test files use the same `suites { suite { test { } } }` structure as sim/VICE tests, but with high-level commands for interacting with the BASIC interpreter. Here is a complete, working test file:\n\n```\n; my_tests.6502 — Example NovaVM integration test suite\n; Run: sim6502 -s my_tests.6502 --backend novavm\n\nsuites {\n  suite(\"My First Tests\") {\n\n    test(\"addition\", \"BASIC addition works\") {\n      basic(\"10 PRINT 2+3\")          ; enter a BASIC program line\n      run()                           ; type RUN and wait for \"Ready\"\n      assert(screen_contains(\"5\"), \"result is 5\")\n    }\n\n    test(\"poke-and-peek\", \"POKE writes to memory\") {\n      basic(\"10 POKE $2000,42\")\n      run()\n      assert(peekbyte($2000) == 42, \"memory written\")\n    }\n\n    test(\"for-loop\", \"FOR/NEXT accumulates correctly\") {\n      basic(\"10 S=0\")\n      basic(\"20 FOR I=1 TO 10:S=S+I:NEXT\")\n      basic(\"30 PRINT S\")\n      run()\n      assert(screen_contains(\"55\"), \"sum 1..10 = 55\")\n    }\n  }\n}\n```\n\n**3. Run the tests.**\n\n```bash\n# Against the Avalonia emulator\nsim6502 -s my_tests.6502 --backend novavm\n\n# Against the FPGA simulation\nsim6502 -s my_tests.6502 --backend verilator\n```\n\n#### How It Works\n\nEach test follows a simple cycle: enter BASIC lines, run the program, check results.\n\n**`basic(\"...\")`** sends a line of text to the BASIC interpreter, followed by ENTER. The runner waits for the line to be accepted (cursor returns to idle) before continuing. Each `basic()` call is one BASIC line — include the line number:\n\n```\nbasic(\"10 MODE 3\")\nbasic(\"20 GCOLOR 7\")\nbasic(\"30 PLOT 100,50\")\n```\n\n**`run()`** types `RUN` and waits for the `Ready` prompt, which appears after the program finishes. If your program doesn't return to the `Ready` prompt (e.g., it loops forever), `run()` will time out. You can wait for custom text instead:\n\n```\nrun()                     ; wait for \"Ready\" (default)\nrun(wait = \"GAME OVER\")   ; wait for specific text\n```\n\n**`assert(...)`** checks a condition after the program runs. Two forms:\n\n```\n; Screen text assertions — checks if text appears anywhere on the 80x25 screen\nassert(screen_contains(\"42\"), \"found the answer\")\n\n; Screen line assertion — checks a specific row (0-24)\nassert(screen_line(0, \"HELLO\"), \"text on first line\")\n\n; Memory assertions — reads a byte from the emulator's address space\nassert(peekbyte($A000) == 3, \"mode register is 3\")\nassert(peekbyte($2000) == $FF, \"memory is $FF\")\n```\n\n**Test isolation:** The runner performs a `cold_start` before each test, fully resetting the CPU and all hardware (VGC, sprites, blitter, tile engine, copper). Tests cannot leak state into subsequent tests.\n\n#### Multi-Step Tests\n\nA single test can run multiple programs in sequence. Each `basic()` / `run()` cycle is independent — `run()` waits for completion before the next `basic()` starts. Previous program lines are cleared by the `cold_start` between tests, but within a test, new `basic()` lines replace lines with the same number:\n\n```\ntest(\"mode-switching\", \"Switch between display modes\") {\n    basic(\"10 MODE 3\")\n    run()\n    assert(peekbyte($A000) == 3, \"graphics mode set\")\n\n    ; Enter a new line 10 — replaces the previous one\n    basic(\"10 MODE 0\")\n    run()\n    assert(peekbyte($A000) == 0, \"back to text mode\")\n}\n```\n\n#### Hardware Testing Example\n\nThis example tests the VGC graphics, sprite engine, and memory-mapped registers:\n\n```\nsuites {\n  suite(\"Hardware Smoke Test\") {\n\n    test(\"plot-pixel\", \"PLOT writes to graphics RAM\") {\n      basic(\"10 MODE 3\")\n      basic(\"20 GCOLOR 7\")\n      basic(\"30 PLOT 0,0\")\n      run()\n      assert(screen_contains(\"Ready\"), \"plot completed\")\n    }\n\n    test(\"sprite-lifecycle\", \"Define, enable, position, and disable a sprite\") {\n      basic(\"10 MODE 3\")\n      basic(\"20 SPRITEDATA 0,0,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF\")\n      basic(\"30 SPRITE 0,ON\")\n      basic(\"40 SPRITE 0,100,50\")\n      basic(\"50 SPRITE 0,OFF\")\n      run()\n      assert(screen_contains(\"Ready\"), \"sprite lifecycle completed\")\n    }\n\n    test(\"copper-raster-effect\", \"Copper changes background at scanline\") {\n      basic(\"10 COPPER CLEAR\")\n      basic(\"20 COPPER ADD 0,100,BGCOL,3\")\n      basic(\"30 COPPER ON\")\n      basic(\"40 FOR I=1 TO 500:NEXT\")\n      basic(\"50 COPPER OFF\")\n      run()\n      assert(screen_contains(\"Ready\"), \"copper completed\")\n    }\n\n    test(\"blitter-fill\", \"BLITFILL writes to CPU RAM\") {\n      basic(\"10 BLITFILL 0,$2000,0,256,1,$AA\")\n      run()\n      assert(peekbyte($2000) == $AA, \"first byte filled\")\n      assert(peekbyte($20FF) == $AA, \"last byte filled\")\n    }\n\n    test(\"tile-engine\", \"Tile map mode with scroll\") {\n      basic(\"10 MODE 4\")\n      basic(\"20 TILESIZE 8\")\n      basic(\"30 TSCROLL 128,64\")\n      basic(\"40 PRINT TSCROLLX\")\n      basic(\"50 PRINT TSCROLLY\")\n      run()\n      assert(screen_contains(\"128\"), \"scroll X readback\")\n      assert(screen_contains(\"64\"), \"scroll Y readback\")\n    }\n  }\n}\n```\n\n#### Additional Commands\n\n**Waiting for output (beyond `run()`):**\n\n```\nwait_ready()                          ; wait for \"Ready\" prompt\nwait_ready(timeout = 10000)           ; with custom timeout (ms)\nwait_text(\"LOADING\", timeout = 5000)  ; wait for arbitrary text\n```\n\n**Input and execution control:**\n\n```\nsend_key(\"ENTER\")                     ; send a keypress\ncold_start()                          ; manually reset the system\npause()                               ; halt the CPU\npause(cycles_count = 50000)           ; run exactly N cycles then halt\npause(watch = $A010, value = $00)     ; run until memory matches value\nresume()                              ; resume after pause\n```\n\n**Direct memory writes (useful for setting hardware registers):**\n\n```\npoke($A001, 3)                        ; write byte to address\n```\n\n#### Tips\n\n- **String literals in BASIC:** The DSL uses `\"` as its own string delimiter, so BASIC strings need escaping. Use `CHR$()` for embedded quotes: `basic(\"10 PRINT CHR$(72);CHR$(73)\")` prints \"HI\".\n- **Hex values:** Use `$` prefix for hex in both BASIC and assertions: `basic(\"10 POKE $2000,$FF\")`, `assert(peekbyte($2000) == $FF, \"ok\")`.\n- **Timeouts:** The default timeout for `run()` and `wait_text()` is 10 seconds. Long-running programs may need `wait_text(\"Done\", timeout = 30000)`.\n- **Comments:** Use `;` for comments in test files, just like assembly.\n- **FPGA speed:** The Verilator backend is ~60x slower than the Avalonia emulator. Tests that take 1 second on Avalonia may take a minute on Verilator. Factor this into timeouts.\n\n**NovaVM vs. Verilator:** Both backends use the same TCP protocol and DSL. The difference is what's on the other end:\n\n| Feature | novavm (Avalonia) | verilator (FPGA sim) |\n|---------|-------------------|----------------------|\n| Speed | Fast (~realtime) | Slow (~1/60 realtime) |\n| VGC graphics | Yes | Yes |\n| Sprites | Yes | Yes |\n| Tile engine | Yes | Yes |\n| Blitter | Yes | Yes |\n| Copper | Yes | Yes |\n| Expansion memory (XMC) | Yes | Yes |\n| DMA controller (DMACOPY/DMAFILL) | Yes | No |\n| File I/O | Yes | No |\n| Sound (SID/WTS) | Yes | No |\n| Network (NIC) | Yes | No |\n| Cycle accuracy | Approximate | Exact (RTL) |\n\n#### Architecture Overview\n\nsim6502 supports multiple execution backends through the `IExecutionBackend` interface. This allows the same test DSL to execute against different CPU implementations without modification.\n\n**SimulatorBackend**: Wraps the internal 6502/6510/65C02 CPU simulator. This is the default backend, providing fast, deterministic execution for pure computational testing. No hardware peripherals are emulated.\n\n**ViceBackend**: Translates execution commands into JSON-RPC 2.0 calls to VICE's embedded MCP server over HTTP. This provides cycle-accurate emulation of the complete Commodore hardware including VIC-II graphics chip, SID sound chip, CIA timers, and full interrupt support.\n\n**NovaVmBackend**: Connects to the e6502 Avalonia emulator or FPGA Verilator testbench via a TCP JSON protocol. Implements both `IExecutionBackend` (peek/poke/jsr) and `IHighLevelBackend` (BASIC line entry, screen reading, wait operations). Used for integration testing of the full NovaVM hardware stack.\n\nThe `SimBaseListener` class (which processes your test DSL) interacts only with the `IExecutionBackend` and `IHighLevelBackend` interfaces, making it backend-agnostic. Backend selection happens at runtime via the `--backend` CLI flag.\n\n#### Error Handling and Troubleshooting\n\n**Connection Errors**\n\nIf VICE is not running or not reachable when tests start, you will see:\n\n```\nCould not connect to VICE MCP server at 127.0.0.1:6510.\nIs VICE running with -mcpserver?\n```\n\n**Resolution:** Start VICE with MCP server enabled before running tests, or use `--launch-vice` to auto-launch VICE.\n\n**Execution Timeouts**\n\nIf a test does not complete within the configured timeout (default 5000ms), you will see:\n\n```\nExecution timed out after 5000ms. PC=$C340, A=$00, X=$FF.\nCode may be in an infinite loop.\n```\n\nThis typically indicates:\n- An infinite loop in your test code\n- IRQ or NMI handlers taking longer than expected (VICE has these running, sim does not)\n- Code waiting for hardware events that never occur\n\n**Resolution:** Increase timeout with `--vice-timeout 10000` (10 seconds), or review your code for infinite loops. Remember that VICE runs interrupt handlers which the internal simulator does not, so timing will differ.\n\n**Snapshot Failures**\n\nIf VICE cannot save a baseline snapshot during suite setup:\n\n```\nFailed to save snapshot 'sim6502_suite_0': [MCP error message]\n```\n\nThis will skip the entire suite. Common causes:\n- VICE filesystem permissions issues\n- Snapshot storage full\n- VICE version does not support snapshot MCP tools\n\n**Resolution:** Check VICE logs and filesystem permissions. Ensure VICE has write access to its snapshot directory.\n\nIf snapshot restore fails before a test:\n\n```\nFailed to load snapshot 'sim6502_suite_0': [MCP error message]\n```\n\nThe test will fail and sim6502 will attempt to re-save the baseline snapshot for subsequent tests.\n\n**Connection Drops During Suite**\n\nIf the MCP connection to VICE drops mid-suite (VICE crashed, network issue, etc.):\n\n```\nMCP connection lost: [error details]\n```\n\nThe current test will fail. sim6502 will attempt to reconnect before the next suite. If reconnection fails, the test run aborts.\n\n**Resolution:** Check VICE process status. If using `--launch-vice`, VICE may have crashed — check VICE logs. If connecting to remote VICE, verify network stability.\n\n#### Behavioral Differences: sim vs. vice\n\nThe internal simulator and VICE emulator exhibit different behaviors due to hardware emulation scope. Tests may pass on one backend and fail on the other. This is expected and reflects real hardware behavior.\n\n**Hardware Subsystems**\n\n| Feature | sim Backend | vice Backend |\n|---------|-------------|--------------|\n| IRQ interrupts | Not emulated | Fully emulated, fires every frame |\n| NMI interrupts | Not emulated | Fully emulated |\n| CIA timers | Not emulated | Fully emulated, running continuously |\n| VIC-II raster timing | Not emulated | Cycle-accurate |\n| Memory banking | Not emulated | Full banking (BASIC, KERNAL, I/O) |\n\n**Example:** If your code sets up a CIA timer interrupt handler, it will never fire on `sim` backend but will fire on `vice` backend at the configured interval.\n\n**Timing Differences**\n\nVICE is cycle-accurate to the real C64 hardware. The internal simulator counts instruction cycles but does not account for:\n- DMA cycles (VIC-II stealing cycles from CPU)\n- Interrupt overhead\n- Memory banking penalties\n\nA test that asserts `cycles \u003c 1000` may pass on `sim` but fail on `vice` if interrupts fire during execution.\n\n**Memory Access Patterns**\n\nOn VICE, reading from `$D000-$DFFF` returns VIC-II, SID, or CIA register values (live hardware state). On `sim`, these locations return whatever was last written to them.\n\n**Example:**\n```\n# This may behave differently on vice vs. sim\nassert($D012 == $00, \"Raster line is 0\")\n```\n\nOn `vice`, `$D012` reads the current VIC-II raster line (constantly changing). On `sim`, it returns whatever your code last wrote to `$D012` (likely `$00` if uninitialized).\n\n**When Tests Behave Differently**\n\nIf a test passes on `sim` but fails on `vice`:\n- Check for interrupt handlers that may be firing (VICE has IRQs enabled)\n- Check for timing assumptions (VICE is cycle-accurate, sim is approximate)\n- Check for hardware register reads (VICE returns live hardware state)\n\nIf a test passes on `vice` but fails on `sim`:\n- Check if test relies on hardware behavior not present in sim\n- Check if test uses memory banking (not supported in sim)\n- Check for uninitialized memory reads (VICE has KERNAL/BASIC ROMs, sim has random data)\n\n#### How It Works Internally\n\nFor power users who want to understand the VICE backend implementation:\n\n**JSR Execution via Breakpoints**\n\nThe internal simulator tracks JSR call depth natively. VICE does not expose call stack depth via MCP, so the ViceBackend implements `ExecuteJsr` using breakpoints:\n\n1. Read the current stack pointer from VICE\n2. Push a synthetic return address (`$0000`) onto the stack via memory writes\n3. Set the program counter to the target subroutine address\n4. Set a breakpoint at the return address (`$0000`)\n5. If `fail_on_brk` is set, set a second breakpoint at the BRK vector address\n6. Resume VICE execution\n7. Poll VICE execution state until it stops (breakpoint hit or timeout)\n8. Read final registers and cycle count\n9. Clean up breakpoints\n\nThis approach allows `stop_on_rts` to work correctly even when the subroutine calls nested subroutines.\n\n**Snapshot-Based Test Isolation**\n\nEach test suite goes through this lifecycle:\n\n1. **Suite setup:** Backend connects to VICE, loads all binaries specified in `load()` directives\n2. **First test:** Before the first test runs, a baseline snapshot is saved with name `sim6502_suite_N`\n3. **Before each test:** The baseline snapshot is restored, ensuring a clean machine state\n4. **After suite:** Snapshot is left in VICE memory (not explicitly cleaned up)\n\nSnapshots are much faster than full machine resets and preserve all loaded binaries and memory state. This means `setup {}` blocks and individual test memory assignments work correctly.\n\n**JSON-RPC 2.0 Communication**\n\nAll communication with VICE uses JSON-RPC 2.0 over HTTP. Each operation maps to an MCP tool call:\n\n| Operation | MCP Tool |\n|-----------|----------|\n| Write memory | `vice.memory.write` |\n| Read memory | `vice.memory.read` |\n| Set register | `vice.registers.set` |\n| Get registers | `vice.registers.get` |\n| Set breakpoint | `vice.breakpoints.set` |\n| Delete breakpoint | `vice.breakpoints.delete` |\n| Resume execution | `vice.execution.run` |\n| Pause execution | `vice.execution.pause` |\n| Get execution state | `vice.execution.get_state` |\n| Save snapshot | `vice.snapshot.save` |\n| Restore snapshot | `vice.snapshot.load` |\n| Get cycle count | `vice.trace.cycles.get` |\n\nThe `ViceConnection` class handles low-level HTTP transport and JSON serialization. The `ViceBackend` class implements `IExecutionBackend` by translating high-level operations (like `WriteByte`, `ExecuteJsr`) into the appropriate MCP tool calls.\n\n**Warp Mode**\n\nBy default, the VICE backend enables warp mode during test execution for speed. Warp mode runs VICE as fast as possible without throttling to real-time speed. This is disabled automatically when tests complete to restore VICE to normal speed.\n\nUse `--vice-warp false` if you want to watch test execution in real-time (useful for debugging visual or timing-related behavior).\n\n\n---\n\n## Complete DSL Reference\n\n### File Structure\n\nTest files use a hierarchical structure:\n\n```\nsuites {\n  suite(\"Suite Name\") {\n    ; Suite-level configuration\n    symbols(\"path/to/symbols.sym\")\n    load(\"path/to/program.prg\", strip_header = true)\n\n    test(\"test-id\", \"Test Description\") {\n      ; Test contents: assignments, jsr calls, assertions\n    }\n\n    test(\"another-test\", \"Another Test\") {\n      ; ...\n    }\n  }\n\n  suite(\"Another Suite\") {\n    ; ...\n  }\n}\n```\n\n### Comments\n\nLines starting with `;` are comments and are ignored:\n\n```\n; This is a comment\n[r0L] = $ff  ; This is an inline comment\n```\n\n### Suite-Level Functions\n\n#### symbols(filename)\nLoads a KickAssembler symbol file. Must be called before symbols can be referenced.\n\n```\nsymbols(\"path/to/program.sym\")\n```\n\n#### load(filename, [address = addr], [strip_header = true|false])\nLoads a binary program into memory.\n\n| Parameter | Description |\n|-----------|-------------|\n| `filename` | Path to the .prg file |\n| `address` | Optional. Load address (overrides file header) |\n| `strip_header` | Optional. If `true`, skips the first 2 bytes (load address header) |\n\n```\n; Load with embedded address header (first 2 bytes)\nload(\"program.prg\", strip_header = true)\n\n; Load at specific address\nload(\"kernal.rom\", address = $e000)\n\n; Load with header intact (uses embedded load address)\nload(\"program.prg\")\n```\n\n### Number Formats\n\n| Format | Syntax | Example |\n|--------|--------|---------|\n| Hexadecimal | `$xxxx` or `0xXXXX` | `$d020`, `0xFF` |\n| Decimal | Plain digits | `1234`, `255` |\n| Binary | `%xxxxxxxx` | `%10101010`, `%11110000` |\n\n### Boolean Values\n\n```\ntrue\nfalse\n```\n\n### Symbol References\n\nSymbols from loaded symbol files are referenced with square brackets:\n\n```\n[SymbolName]           ; Simple symbol\n[namespace.symbol]     ; Namespaced symbol (e.g., [vic.SP0X])\n```\n\n### Memory Addresses\n\nAddresses can be specified as numbers or symbol references:\n\n```\n$d020                  ; Hexadecimal address\n8192                   ; Decimal address\n[vic.SP0X]             ; Symbol reference\n```\n\n### Registers\n\nThe 6502 registers can be read and written:\n\n| Register | Syntax |\n|----------|--------|\n| Accumulator | `a` or `A` |\n| X Index | `x` or `X` |\n| Y Index | `y` or `Y` |\n\n```\na = $ff     ; Set accumulator to 255\nx = 0       ; Set X register to 0\ny = [r0L]   ; Set Y register from symbol value\n```\n\n### Processor Flags\n\nThe processor status flags can be read and written:\n\n| Flag | Syntax | Description |\n|------|--------|-------------|\n| Carry | `c` or `C` | Carry flag |\n| Negative | `n` or `N` | Negative flag |\n| Zero | `z` or `Z` | Zero flag |\n| Decimal | `d` or `D` | Decimal mode flag |\n| Overflow | `v` or `V` | Overflow flag |\n\n```\nc = true    ; Set carry flag\nn = false   ; Clear negative flag\nz = 1       ; Set zero flag (1 = true)\nd = 0       ; Clear decimal flag (0 = false)\nv = [FALSE] ; Set from symbol\n```\n\n### Assignments\n\n#### Memory Assignment\n\nWrite values to memory addresses:\n\n```\n$d020 = $0e              ; Write byte to address\n$c000 = $1234            ; Write word (2 bytes, little-endian)\n[vic.EXTCOL] = $00       ; Write to symbol address\n[r1] = $4000             ; Write word to symbol address\n```\n\n#### Expression Assignment\n\nWrite to computed addresses:\n\n```\n[Loc1] + $02 = $d0       ; Write to symbol + offset\n$4000 + 8 = $ff          ; Write to address + offset\n```\n\n#### Register Assignment\n\n```\na = $ff\nx = peekbyte($c000)\ny = [r0L] + 1\n```\n\n#### Flag Assignment\n\n```\nc = true\nn = false\nz = 1\n```\n\n### Expressions\n\nExpressions can combine values with operators:\n\n#### Arithmetic Operators\n\n| Operator | Description |\n|----------|-------------|\n| `+` | Addition |\n| `-` | Subtraction |\n| `*` | Multiplication |\n| `/` | Division |\n\n#### Bitwise Operators\n\n| Operator | Description |\n|----------|-------------|\n| `\u0026` | Bitwise AND |\n| `\\|` | Bitwise OR |\n| `^` | Bitwise XOR |\n\n#### Byte/Word Modifiers\n\n| Modifier | Description |\n|----------|-------------|\n| `.b` | Read/compare as byte (8-bit) - default |\n| `.w` | Read/compare as word (16-bit) |\n| `.l` | Low byte of 16-bit value |\n| `.h` | High byte of 16-bit value |\n\n```\n[MyVector].w             ; Read as 16-bit word\n[MyValue].b              ; Read as 8-bit byte (default)\n$1234.l                  ; Low byte = $34\n$1234.h                  ; High byte = $12\n```\n\n#### Expression Examples\n\n```\n[r0L] + 1\n$4000 + peekbyte([offset])\n[base] + peekbyte([index]) * 8\n([c64lib_timers] + $02).w\npeekword([r1]) \u0026 $ff00\n```\n\n### Built-in Functions\n\n#### peekbyte(address)\nReturns the 8-bit value at the specified address.\n\n```\npeekbyte($c000)\npeekbyte([r0L])\n```\n\n#### peekword(address)\nReturns the 16-bit value at address and address+1 (little-endian).\n\n```\npeekword($c000)          ; Returns value at $c000 (low) and $c001 (high)\npeekword([vector])\n```\n\n#### memchk(address, size, value)\nReturns `true` if all bytes in the memory range equal the specified value.\n\n```\nmemchk($1234, $100, $00)           ; Check if 256 bytes are zero\nmemchk([buffer], $40, $ff)         ; Check if 64 bytes are $ff\n```\n\n#### memcmp(source, target, size)\nReturns `true` if the two memory regions are identical.\n\n```\nmemcmp($e000, $4000, $2000)        ; Compare 8KB regions\nmemcmp([src], [dst], peekword([size]))\n```\n\n#### memfill(address, count, value)\nFills a region of memory with a specified value. Useful for initializing test memory.\n\n```\nmemfill($4000, $100, $00)          ; Fill 256 bytes with zero\nmemfill([buffer], 64, $ff)         ; Fill 64 bytes with $ff\nmemfill([r0L], 2, $55)             ; Fill 2 bytes starting at r0L address\n```\n\n#### memdump(address, count)\nPrints a hex dump of memory to the console. Useful for debugging.\n\n```\nmemdump($4000, 16)                 ; Dump 16 bytes at $4000\nmemdump([Board], 128)              ; Dump 128 bytes at symbol address\n```\n\nOutput format:\n```\n[memdump] $4000, 16 bytes:\n4000: 41 42 43 44 45 46 47 48  |  ABCDEFGH|\n4008: 00 00 00 00 00 00 00 00  |  ........|\n```\n\n### Setup Block\n\nThe `setup` block runs before each test in a suite. Use it to initialize common memory state:\n\n```\nsuite(\"Chess Tests\") {\n  symbols(\"chess.sym\")\n  load(\"chess.prg\", strip_header = true)\n\n  setup {\n    ; Runs before EACH test in this suite\n    [whiteKingSq] = $74\n    [blackKingSq] = $04\n    [currentPlayer] = $01\n    memfill([Board], 64, $30)\n  }\n\n  test(\"move-pawn\", \"Pawn moves forward\") {\n    ; Setup already ran - board is initialized\n    ; ...\n  }\n\n  test(\"capture-piece\", \"Pawn captures diagonally\") {\n    ; Setup runs again - board is reset to initial state\n    ; ...\n  }\n}\n```\n\nThe setup block supports:\n- Memory assignments (`$5000 = $42`, `[symbol] = value`)\n- Symbol assignments\n- `memfill()` calls\n- Register/flag assignments\n\nNote: Assertions and JSR calls are NOT allowed in setup blocks.\n\n### Test Options\n\nTests support optional parameters for control flow and debugging:\n\n```\ntest(\"test-id\", \"Description\", skip = true, trace = true, timeout = 10000, tags = \"smoke,regression\") {\n  ; test contents\n}\n```\n\n#### skip = true|false\nSkip test execution. Skipped tests appear in results but don't run.\n\n```\ntest(\"wip-feature\", \"Work in progress\", skip = true) {\n  ; This test won't execute\n}\n```\n\n#### trace = true|false\nEnable execution trace on failure. When enabled, if the test fails, a detailed instruction trace is printed showing every instruction executed with register/flag state.\n\n```\ntest(\"buggy-code\", \"Debug this\", trace = true) {\n  jsr([GenerateMove], stop_on_rts = true, fail_on_brk = true)\n  assert(c == true, \"Should set carry\")\n}\n```\n\nIf this test fails, output includes:\n```\nFAILED: buggy-code - Should set carry\nExpected: c == true, Got: c == false\n\nExecution trace (247 instructions):\n$1832: LDA $2157      A=$B6 X=$74 Y=$00 SP=$F7 NV-bdizc\n$1835: AND #$7F       A=$36 X=$74 Y=$00 SP=$F7 nv-bdizc\n...\n```\n\nFlags are uppercase when set, lowercase when clear.\n\n#### timeout = N\nSet cycle limit for the test. If exceeded, the test fails.\n\n```\ntest(\"must-be-fast\", \"Performance test\", timeout = 10000) {\n  jsr([QuickSort], stop_on_rts = true, fail_on_brk = true)\n  ; Fails if routine takes \u003e 10000 cycles\n}\n```\n\n- `0` disables the timeout\n- Default (unspecified) = no timeout\n\n#### tags = \"tag1,tag2\"\nCategorize tests with comma-separated tags for filtering.\n\n```\ntest(\"castle-kingside\", \"King castles kingside\", tags = \"castling,regression\") {\n  ; ...\n}\n```\n\n### JSR Function (Subroutine Execution)\n\nThe `jsr` function executes code starting at the specified address:\n\n```\njsr(address, stop_condition, fail_on_brk = true|false)\n```\n\n#### Stop Conditions\n\nYou must specify ONE of these stop conditions:\n\n| Option | Description |\n|--------|-------------|\n| `stop_on_rts = true\\|false` | Stop when RTS is executed at the original call level |\n| `stop_on_address = address` | Stop when program counter reaches this address |\n\n**Note:** When using `stop_on_address`, RTS instructions do NOT stop execution. The code continues until the specified address is reached.\n\n#### fail_on_brk Option\n\n| Value | Behavior |\n|-------|----------|\n| `true` | Test fails if BRK instruction is executed |\n| `false` | BRK stops execution but test continues |\n\n#### JSR Examples\n\n```\n; Stop when the subroutine returns\njsr([FillMemory], stop_on_rts = true, fail_on_brk = true)\n\n; Stop at a specific address (numeric)\njsr($2000, stop_on_address = $2100, fail_on_brk = true)\n\n; Stop at a specific address (symbol)\njsr([FillMemory], stop_on_address = [CopyMemory], fail_on_brk = true)\n\n; Using namespaced symbol for stop address\njsr([FillMemory], stop_on_address = [Keyboard.Return], fail_on_brk = false)\n```\n\n### Assertions\n\nAssertions verify that your code behaved correctly:\n\n```\nassert(comparison, \"Description of what's being tested\")\n```\n\n#### Comparison Operators\n\n| Operator | Description |\n|----------|-------------|\n| `==` | Equal to |\n| `!=` or `\u003c\u003e` | Not equal to |\n| `\u003e` | Greater than |\n| `\u003c` | Less than |\n| `\u003e=` | Greater than or equal |\n| `\u003c=` | Less than or equal |\n\n#### Assertion Types\n\n**Memory Value Assertions:**\n```\nassert($d020 == $0e, \"Border color is light blue\")\nassert([vic.SP0X] == $ff, \"Sprite 0 X position is 255\")\nassert(([MyVector].w) == $1000, \"Vector points to $1000\")\n```\n\n**Register Assertions:**\n```\nassert(a == $00, \"Accumulator is zero\")\nassert(x \u003e= 8, \"X register is at least 8\")\nassert(y != $ff, \"Y register is not $ff\")\n```\n\n**Flag Assertions:**\n```\nassert(c == true, \"Carry flag is set\")\nassert(z == false, \"Zero flag is clear\")\nassert(n == true, \"Negative flag is set\")\n```\n\n**Cycle Count Assertions:**\n```\nassert(cycles \u003c 1000, \"Routine completed in under 1000 cycles\")\nassert(cycles \u003c= 500, \"Routine is fast enough\")\n```\n\n**Memory Check Assertions:**\n```\nassert(memchk($1234, $100, $bd), \"Memory filled with $bd\")\nassert(memcmp($e000, $4000, $2000), \"Memory regions match\")\n```\n\n**Complex Expression Assertions:**\n```\nassert([c64lib_timers] + $00 + peekbyte([r2H]) * 8 == [ENABLE], \"Timer enabled\")\nassert(([base] + $02).w == $1000, \"Word value matches\")\n```\n\n### Complete Test Example\n\n```\nsuites {\n  suite(\"Memory Operations Test Suite\") {\n    ; Load symbol file for symbolic references\n    symbols(\"c64lib.sym\")\n\n    ; Load the program under test\n    load(\"c64lib.prg\", strip_header = true)\n\n    ; Load C64 KERNAL ROM for any KERNAL calls\n    load(\"kernal.rom\", address = $e000)\n\n    test(\"fill-memory-basic\", \"Fill 256 bytes with a pattern\") {\n      ; Setup: Configure parameters for FillMemory routine\n      [r0L] = $aa          ; Fill value\n      [r1] = $4000         ; Start address\n      [r2] = $100          ; Size (256 bytes)\n\n      ; Clear destination first\n      a = $00\n      x = $00\n\n      ; Execute the routine\n      jsr([FillMemory], stop_on_rts = true, fail_on_brk = true)\n\n      ; Verify results\n      assert(memchk($4000, $100, $aa), \"Memory filled correctly\")\n      assert(cycles \u003c 5000, \"Completed efficiently\")\n    }\n\n    test(\"copy-memory-kernal\", \"Copy KERNAL ROM to RAM\") {\n      ; Setup copy parameters\n      [r0] = $e000         ; Source: KERNAL ROM\n      [r1] = $4000         ; Destination\n      [r2] = $2000         ; Size: 8KB\n\n      ; Execute\n      jsr([CopyMemory], stop_on_rts = true, fail_on_brk = true)\n\n      ; Verify\n      assert(memcmp($e000, $4000, $2000), \"KERNAL copied correctly\")\n    }\n\n    test(\"sprite-position\", \"Position sprite without MSB\") {\n      ; Setup sprite 0 position\n      x = $00              ; Sprite number\n      a = $ff              ; X position (\u003c 256)\n      y = $40              ; Y position\n      $02 = $40            ; Y position in ZP\n      [vic.MSIGX] = $00    ; Clear MSB register\n\n      ; Execute sprite positioning\n      jsr([PositionSprite], stop_on_rts = true, fail_on_brk = true)\n\n      ; Verify sprite registers\n      assert([vic.SP0X] == $ff, \"X position set correctly\")\n      assert([vic.SP0Y] == $40, \"Y position set correctly\")\n      assert([vic.MSIGX] == $00, \"MSB not set for X \u003c 256\")\n    }\n  }\n}\n```\n\n---\n\n##### Function Reference (Quick Reference)\n\n- `memcmp(source, target, size)`: Compare 2 blocks of memory\n- `memchk(source, size, value)`: Ensure that a block of memory contains `value`\n- `peekbyte(address)`: Return the 8-bit value at `address`\n- `peekword(address)`: Return the 16-bit value at `address` and `address + 1`\n\n---\n\n##### Symbol File Reference\n\nThe only supported symbol file right now is Kickassembler's. To load symbols from a symbol file, load it from the top of your suite with the `symbols` function:\n\n```\nsymbols(\"/code/include_me_full.sym\")\n```\n\nYou can reference symbols within your tests by wrapping the symbol name in brackets:\n\n```\n[MySymbol]\n```\n\nThe symbol must exist, or the test will fail.\n\nThe format of the symbol file looks like this:\n\n```\n.label ENABLE=$80\n.label DISABLE=$00\n.label TIMER_ONE_SECOND=$3c\n.label TIMER_SINGLE=$0\n.label TIMER_STRUCT_BYTES=$40\n\n.label UpdateTimers=$209d\n.label UpdateScreen=$21cc\n\n.label c64lib_timers=$33c\n```\n\nThe CLI also supports symbol files containing namespaces:\n\n```\n.namespace vic {\n  .label SP0X   = $d000\n  .label SP0Y   = $d001\n  .label SP1X   = $d002\n  .label SP1Y   = $d003\n  .label SP2X   = $d004\n  .label SP2Y   = $d005\n  .label SP3X   = $d006\n  .label SP3Y   = $d007\n  .label SP4X   = $d008\n  .label SP4Y   = $d009\n  .label SP5X   = $d00a\n  .label SP5Y   = $d00b\n  .label SP6X   = $d00c\n  .label SP6Y   = $d00d\n  .label SP7X   = $d00e\n  .label SP7Y   = $d00f\n}\n```\n\nIf you wanted to reference the symbol `SP0X` inside of the `vic` namespace, you'd reference it as `[vic.SP0X]`.\n\nYou can also perform simple expressions like `[UpdateTimers] + 1` or `peekword([r0L])`.\n\n\n#### What's missing?\n\nThe internal simulator (`--backend sim`) is a vanilla 6502/6510/65C02 with no concept of C64-specific hardware. There's no VIC-II, SID, or CIA emulation, so testing against programs that use these hardware devices is limited to verifying that the correct values are written to the correct memory-mapped registers.\n\nFor full hardware-accurate testing, use the VICE backend (`--backend vice`) which provides cycle-accurate emulation of the complete machine including all hardware subsystems. See the [VICE Backend](#vice-backend-hardware-accurate-testing) section for details.\n\n---\n\n## Language Server (LSP)\n\nsim6502 includes a Language Server Protocol implementation for IDE integration. This provides real-time feedback while writing `.6502` test files.\n\n### Features\n\n- **Syntax Highlighting** - TextMate grammar for VS Code and compatible editors\n- **Real-time Diagnostics** - Syntax errors and warnings as you type\n- **Code Completion** - Keywords, registers, flags, system types, and built-in functions\n- **Hover Information** - Documentation tooltips for keywords and symbols\n- **Go-to-Definition** - Navigate to symbol definitions (from loaded `.sym` files)\n\n### Building the Language Server\n\n```bash\ncd sim6502-lsp\ndotnet build\n```\n\nThe compiled server will be at `sim6502-lsp/bin/Debug/net10.0/sim6502-lsp.dll`.\n\n### VS Code\n\nThe `sim6502-vscode/` directory contains a ready-to-use VS Code extension.\n\n**Install from source:**\n\n```bash\ncd sim6502-vscode\nnpm install\nnpm run compile\nnpx @vscode/vsce package --allow-missing-repository\ncode --install-extension sim6502-vscode-0.1.0.vsix\n```\n\n**Configuration** (in VS Code settings):\n\n| Setting | Description |\n|---------|-------------|\n| `sim6502.lspPath` | Path to sim6502-lsp project directory (auto-detected if in workspace) |\n| `sim6502.trace.server` | LSP trace level: `off`, `messages`, or `verbose` |\n\n**Troubleshooting:**\n\nIf the language server doesn't start, set the path explicitly in settings:\n```json\n\"sim6502.lspPath\": \"/path/to/sim6502/sim6502-lsp/sim6502-lsp.csproj\"\n```\n\nCheck **Output** → **sim6502 Language Server** for diagnostic messages.\n\n### Neovim\n\nUsing [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig):\n\n```lua\n-- In your init.lua or lua/plugins/lsp.lua\nlocal lspconfig = require('lspconfig')\nlocal configs = require('lspconfig.configs')\n\n-- Register sim6502 as a custom filetype\nvim.filetype.add({\n  extension = {\n    ['6502'] = 'sim6502',\n  },\n})\n\n-- Define the LSP configuration\nif not configs.sim6502 then\n  configs.sim6502 = {\n    default_config = {\n      cmd = { 'dotnet', 'run', '--project', '/path/to/sim6502/sim6502-lsp/sim6502-lsp.csproj' },\n      filetypes = { 'sim6502' },\n      root_dir = function(fname)\n        return lspconfig.util.find_git_ancestor(fname) or vim.fn.getcwd()\n      end,\n      settings = {},\n    },\n  }\nend\n\nlspconfig.sim6502.setup({})\n```\n\n**Alternative using the compiled DLL:**\n\n```lua\ncmd = { 'dotnet', '/path/to/sim6502/sim6502-lsp/bin/Debug/net10.0/sim6502-lsp.dll' },\n```\n\n### Sublime Text\n\nUsing [LSP for Sublime Text](https://github.com/sublimelsp/LSP):\n\n1. Install the LSP package via Package Control\n2. Create `~/.config/sublime-text/Packages/User/LSP-sim6502.sublime-settings`:\n\n```json\n{\n  \"clients\": {\n    \"sim6502\": {\n      \"enabled\": true,\n      \"command\": [\"dotnet\", \"run\", \"--project\", \"/path/to/sim6502/sim6502-lsp/sim6502-lsp.csproj\"],\n      \"selector\": \"source.sim6502\",\n      \"schemes\": [\"file\"]\n    }\n  }\n}\n```\n\n3. Create a syntax definition for `.6502` files or associate them with a generic syntax.\n\n### Emacs\n\nUsing [lsp-mode](https://emacs-lsp.github.io/lsp-mode/):\n\n```elisp\n;; In your init.el or .emacs\n(require 'lsp-mode)\n\n;; Register .6502 files\n(add-to-list 'auto-mode-alist '(\"\\\\.6502\\\\'\" . prog-mode))\n\n;; Configure sim6502 LSP\n(lsp-register-client\n (make-lsp-client\n  :new-connection (lsp-stdio-connection\n                   '(\"dotnet\" \"run\" \"--project\" \"/path/to/sim6502/sim6502-lsp/sim6502-lsp.csproj\"))\n  :major-modes '(prog-mode)\n  :server-id 'sim6502-lsp\n  :activation-fn (lsp-activate-on \"sim6502\")))\n\n;; Enable LSP for .6502 files\n(add-hook 'prog-mode-hook\n          (lambda ()\n            (when (string-match-p \"\\\\.6502\\\\'\" (buffer-file-name))\n              (lsp))))\n```\n\nUsing [eglot](https://github.com/joaotavora/eglot) (built into Emacs 29+):\n\n```elisp\n(require 'eglot)\n\n(add-to-list 'auto-mode-alist '(\"\\\\.6502\\\\'\" . prog-mode))\n\n(add-to-list 'eglot-server-programs\n             '(prog-mode . (\"dotnet\" \"run\" \"--project\" \"/path/to/sim6502/sim6502-lsp/sim6502-lsp.csproj\")))\n\n(add-hook 'prog-mode-hook\n          (lambda ()\n            (when (string-match-p \"\\\\.6502\\\\'\" (buffer-file-name))\n              (eglot-ensure))))\n```\n\n### Helix\n\nAdd to `~/.config/helix/languages.toml`:\n\n```toml\n[[language]]\nname = \"sim6502\"\nscope = \"source.sim6502\"\nfile-types = [\"6502\"]\nlanguage-servers = [\"sim6502-lsp\"]\ncomment-token = \";\"\n\n[language-server.sim6502-lsp]\ncommand = \"dotnet\"\nargs = [\"run\", \"--project\", \"/path/to/sim6502/sim6502-lsp/sim6502-lsp.csproj\"]\n```\n\n### Other Editors\n\nThe language server uses standard LSP over stdin/stdout. Any editor with LSP support can use it:\n\n```bash\n# Run the server directly\ndotnet run --project /path/to/sim6502/sim6502-lsp/sim6502-lsp.csproj\n\n# Or use the compiled DLL\ndotnet /path/to/sim6502/sim6502-lsp/bin/Debug/net10.0/sim6502-lsp.dll\n```\n\nConfigure your editor's LSP client to launch the server with one of these commands and associate it with `.6502` files.\n\n---\n\n#### Thanks\n\nThanks to Aaron Mell for building the 6502 simulator (https://github.com/aaronmell/6502Net). It was a tremendous help in building this tool.\n\nThanks to Terence Parr and Sam Harwell for ANTLR. (https://www.antlr.org/)\n\n#### License\n\nANTLR 4.8 is Copyright (C) 2012 Terence Parr and Sam Harwell. All Rights Reserved.\n\nThe 6502 Simulator and associated test suite are Copyright (C) 2013 by Aaron Mell. All Rights Reserved.\n\nThe 6502 Unit Test CLI and associated test suite are Copyright (C) 2020 by Barry Walker. All Rights Reserved.\n\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbarryw%2Fsim6502","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbarryw%2Fsim6502","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbarryw%2Fsim6502/lists"}