Metadata-Version: 2.4
Name: lc3lab
Version: 0.2.0
Summary: Dependency-free LC-3 assembler, simulator and practice-problem grader
Author: Kevin Zhong
License: MIT
Project-URL: Homepage, https://github.com/CLCK0622/lc3lab
Project-URL: Issues, https://github.com/CLCK0622/lc3lab/issues
Keywords: lc-3,lc3,assembler,simulator,education,ece220
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Education
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Education
Classifier: Topic :: Software Development :: Assemblers
Classifier: Topic :: System :: Emulators
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# lc3lab

A dependency-free LC-3 assembler, simulator and grading library in pure Python.

```bash
pip install lc3lab
```

Python 3.9+, nothing else. It behaves the same on macOS, Windows, Linux and university servers, and it is meant to be **scripted**: run a program from the shell, assert on memory and registers from Python, or build an autograded problem set on top of it.

[中文说明](https://github.com/CLCK0622/lc3lab/blob/main/README.zh-CN.md)

## Command line

```bash
lc3lab run main.asm data.asm            # assemble both, run from main.asm's .ORIG, print registers and x6000
lc3lab run echo.asm --kb "hi q"         # feed keyboard input (\n escapes allowed)
lc3lab run prog.asm --show x6000 x6010  # choose which memory words to print
lc3lab asm main.asm                     # listing: address, machine word, source line, symbol table
```

`run` exits non-zero if the program does not `HALT`, and prints the last instructions it executed, so you can see where it got stuck or ran off the end.

## Python API

```python
from lc3lab import assemble_file, LC3

prog = assemble_file("main.asm")     # prog.segments, prog.symbols, prog.used_ops
m = LC3()
m.load(prog)
m.feed_keyboard("hi q")              # optional
m.pc = prog.origin
status = m.run(max_instr=300_000)    # "halt" | "limit" | "error"

m.reg[0], m.cc, m.mem[0x6000], m.output_text(), m.error, m.recent_trace()
```

`m.poke(addr, value)` sets up memory, `m.read`/`m.write` go through the device registers, `m.visited` is the set of every PC executed.

## Grading library

`lc3lab.grading` turns a list of `Test` objects into a grader with readable failure reports:

```python
# problems/01_strlen/grader.py
import sys
from lc3lab.grading import Test, main

def case(s):
    return Test('"%s" -> %d' % (s, len(s)), setup={0x5000: s}, expect_mem={0x6000: len(s)})

TESTS = [case(""), case("A"), case("hello")]

def random_case(rng):                      # optional: enables --random N
    return case("".join(rng.choice("abc ") for _ in range(rng.randint(0, 20))))

if __name__ == "__main__":
    sys.exit(main(__file__, "Problem 1 -- String Length", TESTS, random_case=random_case))
```

```
$ python3 grader.py            # grades main.asm next to it
$ python3 grader.py other.asm  # or any file
$ python3 grader.py -v         # registers and output on failure
$ python3 grader.py --random 200 --seed 7
$ python3 grader.py --dump-tests   # writes tests/testN.asm, loadable in any simulator
```

A `Test` can describe:

| field | meaning |
|---|---|
| `setup` | `{address: int \| [ints] \| "string"}` written to memory first (strings are null-terminated) |
| `kb` | keyboard characters fed to the program |
| `expect_mem`, `expect_regs`, `expect_output` | what must hold after `HALT` |
| `call`, `regs_in`, `preserve`, `must_call` | **call a subroutine directly**: every register is filled with garbage, `R7` points at a return address, and the test checks the result, that `preserve`d registers are untouched, that control came back with `RET` (not `HALT`), and that `must_call` was executed (nested calls) |
| `max_instr`, `note` | instruction budget; a note copied into the generated test file |

`main(...)` also accepts `forbid=["getc", "out"]` (reject programs using those instructions) and `require_labels=[...]`.

Failure reports name the address or register, expected vs. actual, and for an infinite loop the instructions it is spinning on:

```
  [FAIL] test2: "A" -> 1
         did not HALT after 300000 instructions (infinite loop?)
         looping around:
             main.asm:8    ADD R0, R0, #1
             main.asm:9    BR LOOP
             main.asm:6    LDR R2, R1, #0
             main.asm:7    BRz DONE
         mem[x6000]: expected x0001 (1), got x0000 (0)
```

A directory of `NN_name/grader.py` problems is a *problem set*; `lc3lab list`, `lc3lab grade [ID|all] [--solutions] [--random N]` and `lc3lab dump-tests` operate on one (found via `--problems DIR`, `$LC3LAB_PROBLEMS`, `./problems` or the current directory).

## Online-judge style verdicts

```bash
lc3lab judge problems/05_find_first/grader.py my.asm          # AC / WA / TLE / RE / CE, score, per-case table
lc3lab judge problems/05_find_first/grader.py my.asm --json   # the same as JSON
```

```python
from lc3lab.grading import load_problem, judge

problem = load_problem("problems/05_find_first/grader.py")   # runs the grader with main() stubbed out
result = judge(source_text, problem)                           # {"verdict": "WA", "score": 67, "tests": [...], ...}
```

`judge` maps each case to `AC`, `WA` (wrong memory/registers/output), `TLE` (instruction limit), `RE` (ran outside the program, illegal opcode, ...) and the whole submission to `CE` on an assembly error or a violated `forbid`/`require_labels` rule. This is what a local judge such as lc3oj builds on.

## Language server

```bash
lc3lab lsp        # Language Server Protocol over stdio
```

Diagnostics come from the real assembler, so what the editor underlines is exactly what the grader would reject, plus a few lints (a comment glued to an instruction without a space before `;`, labels never referenced, a file that never `HALT`s). Hover shows the syntax and semantics of every instruction and directive, the address of a label, or the value of a number; completion offers opcodes, directives, registers and the labels of the file; go-to-definition and document symbols work on labels.

Neovim (0.10+), in `init.lua`:

```lua
vim.filetype.add({ extension = { asm = "lc3" } })
vim.api.nvim_create_autocmd("FileType", {
  pattern = "lc3",
  callback = function() vim.lsp.start({ name = "lc3lab", cmd = { "lc3lab", "lsp" } }) end,
})
```

The same features are available as plain functions (`lc3lab.lsp.diagnostics(text)`, `hover`, `completion`, `definition`, `document_symbols`) for editors embedded in web pages.

## Simulator behaviour

- **Assembler**: Patt & Patel syntax as accepted by `lc3as`: labels (case-insensitive, optional trailing colon), `ADD AND NOT BR* JMP JSR JSRR LD LDI LDR LEA ST STI STR TRAP RET RTI`, aliases `GETC OUT PUTS IN PUTSP HALT`, `.ORIG .FILL .BLKW .STRINGZ .END`, numbers as `#10 #-3 x1F xFFFF 10 -3`. Several `.ORIG` segments per file are allowed. Errors say what and where: `imm5` out of range, a label beyond the `PC`-offset reach, duplicate or undefined labels, misspelled opcodes.
- **ISA edition**: 3rd edition by default, so `LEA` does not set the condition codes (matches LC3Tools). `LC3(lea_sets_cc=True)` gives the 2nd-edition behaviour of the classic `lc3sim`.
- **TRAP**: `x20`–`x25` are serviced by the simulator, and they overwrite `R7` exactly like hardware. Other vectors jump through the trap table if you populate it.
- **Memory-mapped I/O with timing**: `KBSR xFE00`, `KBDR xFE02`, `DSR xFE04`, `DDR xFE06`, `MCR xFFFE`. A keystroke becomes ready only after a delay (`kb_first_delay`, then `kb_gap` between keys); the display starts busy and stays busy for `display_busy` instructions after each write, longer than the key gap. A program that skips the `KBSR` poll reads a stale value (`m.stale_kb_reads`), one that skips the `DSR` poll loses characters (`m.dropped_chars`). All three are constructor arguments.
- **Diagnostics**: executing an address outside the loaded program (fell off the end, bad return address), the reserved opcode, `RTI` in user mode and `GETC` with no input stop the run with `m.error` set; `m.recent_trace()` maps the last PCs back to source lines.

## Why another LC-3 tool?

| | Install | Command line / scriptable | Grading library | Platforms |
|---|---|---|---|---|
| [LC3Tools](https://github.com/chiragsakhuja/lc3tools) (UT Austin) | prebuilt GUI: universal `.dmg`, `.exe`, AppImage | only if you build it from source with CMake | no | macOS, Windows, Linux |
| Classic `lc3as` / `lc3sim` (Patt & Patel) | compile from source; not on Homebrew or pip | yes | no | Linux; macOS with patching |
| Web simulators | none | no | no | any browser |
| **lc3lab** | `pip install lc3lab`, zero dependencies | yes, plus a language server | yes, with subroutine-level tests, random cases and judge verdicts | macOS, Windows, Linux (CI runs all three) |

lc3lab is not a debugger. Use LC3Tools or a web simulator to step through code; the `tests/testN.asm` files that `--dump-tests` writes load there unchanged.

## Development

```bash
git clone https://github.com/CLCK0622/lc3lab.git && cd lc3lab
python3 -m unittest discover -s tests -v
python3 -m lc3lab run some.asm        # runs from the checkout without installing
```

See [CONTRIBUTING.md](https://github.com/CLCK0622/lc3lab/blob/main/CONTRIBUTING.md) for the release process.

## License

MIT
