lc3vm is a simulator for the LC-3 computer. It runs LC-3 programs on your machine. No hardware needed.
The LC-3 (Little Computer 3) is a teaching computer. Many universities use it to teach systems programming. It is small and simple on purpose.
It has:
- 16-bit words
- 8 general-purpose registers (R0–R7)
- 65,536 words of memory
- 15 instructions
- Keyboard and display I/O
You write a program, assemble it to a .obj file, and run it with lc3vm.
- Run LC-3 programs without hardware
- Use
--traceto print every instruction as it runs — good for debugging - The source code is small (under 400 lines) and easy to follow
You need OCaml and opam. Run these four commands:
opam switch create lc3 5.4.1
eval $(opam env)
opam install dune alcotest
dune buildImportant
This project was built with OCaml 5.4.1. Earlier versions may not work.
Step 1 — Build:
dune buildStep 2 — Generate the example program:
dune exec lc3vm-gen-hello
# Writes: temp/example_hello.objStep 3 — Run it:
dune exec lc3vm -- temp/example_hello.objExpected output:
Hello, LC-3!
--- HALT ---
Step 4 — Run with trace:
dune exec lc3vm -- --trace temp/example_hello.objlc3vm [--trace] <program.obj>
| Flag | Short | What it does |
|---|---|---|
--trace |
-t |
Print machine state and instruction before each step |
--help |
-h |
Show usage |
The input must be a big-endian LC-3 object file. The first word is the load address. The words that follow are the program. Most LC-3 assemblers — such as lc3as and complx — produce this format.
Note
To generate .obj files from OCaml, see bin/gen_hello.ml. It writes the exact binary format step by step.
Each line shows the full machine state, then the current instruction:
PC=x3000 [--P] R0=x0000 R1=x0000 ... R7=x0000 | LEA R0, #2
PC=x3001 [--P] R0=x3003 R1=x0000 ... R7=x0000 | TRAP x22
The [NZP] field shows the condition flags. N = negative, Z = zero, P = positive. A - means that flag is off.
lc3vm supports all 15 LC-3 opcodes:
| Group | Instructions |
|---|---|
| Arithmetic | ADD (reg/imm), AND (reg/imm), NOT |
| Control flow | BR (n/z/p), JMP, RET, JSR, JSRR |
| Load | LD, LDR, LDI, LEA |
| Store | ST, STR, STI |
| Trap | GETC, OUT, PUTS, IN, PUTSP, HALT |
| System | RTI |
Warning
RTI is not supported in user mode. It raises a runtime error.
lc3vm/
├── bin/
│ ├── main.ml CLI: parses flags, calls loader and executor
│ └── gen_hello.ml Writes a hello-world .obj to temp/
├── lib/
│ ├── machine.ml CPU state: memory, registers, flags, I/O
│ ├── decode.ml Converts raw 16-bit words into typed instructions
│ ├── exec.ml Runs instructions; fetch-decode-execute loop
│ └── loader.ml Reads .obj files into machine memory
└── test/
└── test_lc3vm.ml Unit tests (Alcotest)
The three library modules follow the same shape as a compiler:
- Machine holds the data (like an AST)
- Decode parses bits into typed instructions (like a parser)
- Exec runs instructions and produces output (like code generation)
Each module enforces its own rule at the boundary: mask16 keeps all values in 16-bit range, and sign_extend converts offsets at decode time. Because of this, the executor never needs to check overflow.
dune build # Build everything
dune test --force # Run all unit tests
dune build @check # Type-check only — no linking (fast)Tip
Use dune build @check for quick feedback while you edit. It takes under a second.
Tests are in test/test_lc3vm.ml. They cover:
- Sign extension and 16-bit masking
- Register and memory read/write
- Condition codes
- All instruction decoding variants
- All executor behaviors
- Loader error cases (empty file, truncated file)
- Fork the repository
- Create a branch for your change
- Run
dune test --forceto check your work - Open a pull request
- Developed on Debian with OCaml 5.4.1 in an opam local switch
- Requires the
unixlibrary (included with OCaml) - Uses
Unix.selectfor non-blocking keyboard input
Warning
Unix.select does not work on Windows without a compatibility layer. Keyboard I/O (KBSR/KBDR reads) will not work correctly on Windows.