A toy computer in Python.
Project description
dt31
A toy computer and assembly language written in Python. Build programs with 60+ built-in instructions for interacting with registers, memory, and the stack. Write your programs in the native assembly syntax or directly with the Python API.
| countdown.dt | countdown.py |
|---|---|
; countdown.dt
; copy 5 to register a
CP 5, R.a
loop:
; print register a
NOUT R.a, 1
; a = a - 1
SUB R.a, 1
; jump to loop if a > 0
JGT loop, R.a, 0
; run with: `dt31 countdown.dt`
; output: 5 4 3 2 1
|
from dt31 import DT31, I, L, Label, R
# create vm with default settings
cpu = DT31()
program = [
I.CP(5, R.a),
loop := Label("loop"),
I.NOUT(R.a, L[1]),
I.SUB(R.a, L[1]),
I.JGT(loop, R.a, L[0]),
]
cpu.run(program)
|
Features
- Simple CPU Architecture: Configurable registers, fixed-size memory, and stack-based operations
- Rich Instruction Set: 60+ instructions including arithmetic, bitwise operations, logic, control flow, and I/O
- Assembly Support: Two-pass assembler with label resolution for jumps and function calls
- Assembly Parser: Parse and execute
.dtassembly files with text-based syntax - Command-Line Interface: Execute
.dtfiles directly with thedt31command - Python API: Build and run programs programmatically with an intuitive API
- Debug Mode: Step-by-step execution with state inspection and breakpoints
- Pure Python: Zero dependencies
Installation
pip install dt31
Getting Started
Hello World
Create a file hello.dt
; Output "Hi!"
COUT 'H', 0
COUT 'i', 0
COUT '!', 0
and run it with the dt31 interpreter
dt31 hello.dt
# Hi!
Basic Arithmetic
; Add two numbers
CP 10, R.a ; Copy 10 into register a
CP 5, R.b ; Copy 5 into register b
ADD R.a, R.b ; a = a + b
NOUT R.a, 1 ; Output a with newline
Save as add.dt and run: dt31 add.dt to output 15.
Loops with Labels
; Count from 1 to 10
CP 1, R.a ; Start counter at 1
loop:
NOUT R.a, 1 ; Print counter
ADD R.a, 1 ; Increment counter
JLT loop, R.a, 11 ; Jump to loop if a < 11
Save as count.dt and run: dt31 count.dt to output 1 2 3 4 5 6 7 8 9 10.
Function Calls
; Print a greeting multiple times
CP 3, R.a ; Counter: print 3 times
print_loop:
CALL greet ; Call the greeting function
SUB R.a, 1. ; R.a -= 1
JGT print_loop, R.a, 0 ; loop if R.a > 0
JMP end
greet:
; Reusable greeting function
COUT 'H', 0
COUT 'i', 0
COUT '!', 0
COUT ' ', 0
RET
end:
; output: `Hi! Hi! Hi! `
Functions use the stack for return addresses and can be called multiple times. See the examples directory for more complex examples.
Core Concepts
Operands
dt31 provides several operand types for referencing values:
- Literals: Constant values
L[42], orLC["a"]as a shortcut forL[ord("a")] - Registers: CPU registers
R.a,R.b,R.c - Memory: Memory addresses
M[100], indirect addressingM[R.a] - Labels: Named jump targets
Label("loop")
See the operands documentation for details.
Instructions
The instruction set includes:
- Arithmetic:
ADD,SUB,MUL,DIV,MOD - Bitwise:
BAND,BOR,BXOR,BNOT,BSL,BSR - Comparisons:
LT,GT,LE,GE,EQ,NE - Logic:
AND,OR,XOR,NOT - Control Flow:
JMP,RJMP,JEQ,JNE,JGT,JGE,JIF - Functions:
CALL,RCALL,RET - Stack:
PUSH,POP,SEMP - I/O:
NOUT,COUT,NIN,CIN - Data Movement:
CP
Users can easily define their own custom instructions by subclassing dt31.instructions.Instruction.
See the instructions documentation for the complete reference.
CPU Architecture
The DT31 CPU includes:
- Registers: General-purpose registers (default:
a,b, andc) - Memory: Fixed-size byte array (default: 256 slots)
- Stack: For temporary values and function calls (default: 256 slots)
- Instruction Pointer: Tracks current instruction in register
ip
See the CPU documentation for API details.
Command-Line Interface
Basic Usage
Execute .dt assembly files directly:
dt31 run program.dt # Execute program
dt31 check program.dt # Validate syntax
dt31 format program.dt # Format file in-place
CLI Options
Run Command
--debugor-d: Enable step-by-step debug output--registers a,b,c,d: Specify custom registers (auto-detected by default)--memory 512: Set memory size in bytes (default: 256)--stack-size 512: Set stack size (default: 256)--custom-instructions PATHor-i PATH: Load custom instruction definitions from a Python file--dump {none,error,success,all}: When to dump CPU state (default: none)--dump-file FILE: File path for CPU state dump (auto-generates timestamped filename if not specified)
Check Command
--custom-instructions PATHor-i PATH: Load custom instruction definitions from a Python file
Examples:
# Validate syntax only (no execution)
dt31 check program.dt
# Validate with custom instructions
dt31 check --custom-instructions my_instructions.py program.dt
# Run with debug output
dt31 run --debug program.dt
# Use custom memory size
dt31 run --memory 1024 program.dt
# Specify registers explicitly
dt31 run --registers a,b,c,d,e program.dt
# Use custom instructions
dt31 run --custom-instructions my_instructions.py program.dt
# Dump CPU state on error (for debugging crashes)
dt31 run --dump error program.dt # Auto-generates program_crash_TIMESTAMP.json
See the CLI documentation for complete details.
Code Formatting
The dt31 format command formats .dt assembly files with consistent style, following Black/Ruff conventions (formats in-place by default).
Basic Usage:
dt31 format program.dt # Format file in-place
dt31 format --check program.dt # Check if formatting needed (exit 1 if yes)
dt31 format --diff program.dt # Show formatting changes without modifying
Exit Codes:
| Code | Meaning |
|---|---|
0 |
Success (formatted, already formatted, or --check passed) |
1 |
Error (file not found, parse error, --check failed, IO error) |
Formatting Options:
All formatting options from program_to_text() are available as CLI flags:
# Indentation (default: 4 spaces)
dt31 format --indent-size 2 program.dt
# Comment margin (default: 2 spaces before semicolon)
dt31 format --comment-margin 3 program.dt
# Inline labels (default: labels on separate lines)
dt31 format --label-inline program.dt
# No blank lines before labels (default: blank line before labels)
dt31 format --no-blank-line-before-label program.dt
# Auto-align inline comments (calculates column based on longest instruction)
dt31 format --align-comments program.dt
# Align inline comments at specific column
dt31 format --align-comments --comment-column 40 program.dt
# Auto-align with custom margin (default: 2 spaces after longest instruction)
dt31 format --align-comments --comment-margin 4 program.dt
# Strip all comments from output
dt31 format --strip-comments program.dt
# Show default arguments (default: hidden)
dt31 format --show-default-args program.dt
Custom Instructions:
Format files that use custom instructions:
dt31 format --custom-instructions my_instructions.py program.dt
Common Workflows:
# Check if files need formatting (CI/pre-commit)
dt31 format --check program.dt
# Preview formatting changes
dt31 format --diff program.dt
# Format with custom style
dt31 format --indent-size 2 --label-inline program.dt
# Check formatting and show diff if needed
dt31 format --check --diff program.dt
Examples:
Before formatting:
CP 5,R.a
loop:NOUT R.a,1
SUB R.a,1
JGT loop,R.a,0
After dt31 format program.dt:
CP 5, R.a
loop:
NOUT R.a, 1
SUB R.a, 1, R.a
JGT loop, R.a, 0
After dt31 format --label-inline program.dt (default hides args):
CP 5, R.a
loop: NOUT R.a, 1
SUB R.a, 1
JGT loop, R.a, 0
After dt31 format --align-comments program.dt (with comments):
; Input with unaligned comments
CP 5, R.a ; Initialize counter
ADD R.a, R.b, R.c ; Add values
; Output with auto-aligned comments
CP 5, R.a ; Initialize counter
ADD R.a, R.b, R.c ; Add values
Custom Instructions
Define custom instructions in a Python file and load them with --custom-instructions. Your file must export an INSTRUCTIONS dict and instruction names must be all-caps:
# my_instructions.py
from dt31.instructions import UnaryOperation
from dt31.operands import Operand, Reference
class TRIPLE(UnaryOperation):
"""Triple a value."""
def __init__(self, a: Operand, out: Reference | None = None):
super().__init__("TRIPLE", a, out)
def _calc(self, cpu: "DT31") -> int:
return self.a.resolve(cpu) * 3
INSTRUCTIONS = {"TRIPLE": TRIPLE}
Use in assembly:
CP 5, R.a
TRIPLE R.a
NOUT R.a, 1 ; Outputs 15
Run with: dt31 run --custom-instructions my_instructions.py program.dt
Security Warning: Loading custom instruction files executes arbitrary Python code. Only load files from trusted sources.
See the instructions documentation for more details on creating custom instructions.
CPU State Dumps
The --dump option captures complete CPU state to JSON for debugging. Dumps include:
- CPU state: registers, memory, stack, and loaded program (as assembly text)
- Error information (on error dumps): exception type, message, traceback, and the instruction that caused the error
Error dumps include both repr and str formats of the failing instruction for easier debugging:
{
"cpu_state": {
"registers": {"a": 10, "b": 0, "ip": 2},
"memory": [...],
"stack": [],
"program": "CP 10, R.a\nCP 0, R.b\nDIV R.a, R.b",
"config": {"memory_size": 256, "stack_size": 256, "wrap_memory": false}
},
"error": {
"type": "ZeroDivisionError",
"message": "integer division or modulo by zero",
"instruction": {
"repr": "DIV(a=R.a, b=R.b, out=R.a)",
"str": "DIV R.a, R.b, R.a"
},
"traceback": "..."
}
}
Assembly Language Reference
Syntax Rules
Instructions and Operands:
INSTRUCTION operand1, operand2, operand3
- Instructions are case-insensitive (
ADD,add, andAddare all valid) - Register names and label names are case-sensitive
- Operands are separated by commas (spaces around commas are optional)
- Comments start with
;and continue to end of line - Blank lines and indentation are ignored
Operand Types
The assembly text syntax differs from Python syntax:
| Operand Type | Assembly Syntax | Python Syntax | Example |
|---|---|---|---|
| Numeric Literal | 42, -5 |
L[42], L[-5] |
CP 42, R.a |
| Character Literal | 'A' |
LC["A"] |
COUT 'H', 0 |
| Register | R.a |
R.a |
ADD R.a, R.b |
| Memory (direct) | [100] or M[100] |
M[100] |
CP 42, [100] |
| Memory (indirect) | [R.a] or M[R.a] |
M[R.a] |
CP [R.a], R.b |
| Label | loop |
Label("loop") |
JMP loop |
Key Differences:
- Literals: In text syntax, bare numbers are literals (no
L[...]wrapper needed) - Characters: Use single quotes
'A'instead ofLC["A"] - Memory: The
Mprefix is optional (both[100]andM[100]work) - Labels: Bare identifiers are labels (no
Label(...)constructor needed) - Registers: Must use
R.prefix in both syntaxes
Label Definition
Labels mark positions in code:
; Label on its own line
loop:
ADD R.a, 1
JLT loop, R.a, 10
; Label on same line as instruction
start: CP 0, R.a
Label names must contain only alphanumeric characters and underscores.
Python API
While assembly syntax is the primary way to use dt31, you can also build and run programs programmatically using the Python API.
Creating and Running Programs
from dt31 import DT31, I, L, M, R
# Create CPU instance
cpu = DT31()
# Write program as list of instructions
program = [
I.CP(42, R.a),
I.CP(100, M[R.a]),
I.NOUT(R.a, L[1]),
I.NOUT(M[R.a], L[1])
]
# Run the program
cpu.run(program)
# 42
# 100
Parsing Assembly from Python
from dt31 import DT31
from dt31.parser import parse_program
cpu = DT31()
assembly = """
CP 5, R.a
loop:
NOUT R.a, 1
SUB R.a, 1
JGT loop, R.a, 0
"""
program = parse_program(assembly)
cpu.run(program)
# 5 4 3 2 1
Converting Programs to Text
Convert Python programs to assembly text format with configurable formatting:
from dt31 import I, L, Label, LC, R
from dt31.assembler import program_to_text
# Create a program in Python
program = [
I.CP(5, R.a),
loop := Label("loop"),
I.COUT(LC["*"]),
I.SUB(R.a, L[1]),
I.JGT(loop, R.a, L[0]),
]
# Convert to assembly text (default formatting)
text = program_to_text(program)
print(text)
# CP 5, R.a
#
# loop:
# COUT '*', 0
# SUB R.a, 1, R.a
# JGT loop, R.a, 0
Formatting Options
The program_to_text function supports various formatting options:
# Custom indentation (default: 4 spaces)
text = program_to_text(program, indent_size=2)
# Inline labels (default: False, labels on separate lines)
text = program_to_text(program, label_inline=True)
# loop: COUT '*', 0
# No blank lines before labels (default: True)
text = program_to_text(program, blank_line_before_label=False)
# Auto-align inline comments (default: False, comment_column: None)
commented_program = [
I.CP(5, R.a).with_comment("Initialize"),
I.ADD(R.a, L[1]).with_comment("Increment"),
]
text = program_to_text(commented_program, align_comments=True)
# CP 5, R.a ; Initialize
# ADD R.a, 1, R.a ; Increment
# Align comments at specific column
text = program_to_text(commented_program, align_comments=True, comment_column=30)
# CP 5, R.a ; Initialize
# ADD R.a, 1, R.a ; Increment
# Auto-align with custom margin (default: 2)
text = program_to_text(commented_program, align_comments=True, comment_margin=4)
# CP 5, R.a ; Initialize
# ADD R.a, 1, R.a ; Increment
# Strip all comments
text = program_to_text(commented_program, strip_comments=True)
# CP 5, R.a
# ADD R.a, 1, R.a
# Show default arguments (default is to hide them)
text = program_to_text(program, hide_default_args=False)
# CP 5, R.a
#
# loop:
# COUT '*', 0
# SUB R.a, 1, R.a
# JGT loop, R.a, 0
Labels and Function Calls
Labels offer a great use-case for the Python walrus operator.
from dt31.operands import I, LC, Label
program = [
I.CALL(print_hi := Label("print_hi")),
I.JMP(end := Label("end")),
print_hi,
I.COUT(LC['H']),
I.COUT(LC['i']),
I.RET(),
end,
]
cpu.run(program)
# Hi
Debugging with Step Execution
cpu = DT31()
cpu.load(program)
# Execute one instruction at a time
cpu.step(debug=True) # Prints instruction and state
print(cpu.state) # Inspect CPU state
# Execute a full program one instruction at a time
cpu.run(program, debug=True)
Accessing CPU State
# Get register values
value = cpu.get_register('a')
# Set register values
cpu.set_register('b', 42)
# Access memory
cpu.set_memory(100, 255)
byte = cpu.get_memory(100)
# Get full state snapshot
state = cpu.state # Returns dict with registers, memory, stack, ip
Documentation
Full API documentation is available at the docs site. Generate the latest docs with:
uv run invoke docs
uv run invoke serve-docs # Serve locally at http://localhost:8080
Key documentation pages:
- DT31 CPU Class - CPU methods and state management
- Instructions - Complete instruction reference
- Operands - Operand types and usage
- Parser - Assembly text parsing
- Assembler - Label resolution and assembly
- CLI - Command-line interface
Development
# Install dependencies
uv sync --dev
# Set up pre-commit hooks
uv run prek install --install-hooks
uv run prek install --hook-type pre-push
# Run tests
uv run invoke test
DT31 is open-source and contributors are welcome on Github.
Planned work
- Data section
- Globbing support for CLI
- Interpreter resume from dump (maybe)
- Input error-handling (maybe)
- File I/O (maybe)
License
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dt31-0.9.1.tar.gz.
File metadata
- Download URL: dt31-0.9.1.tar.gz
- Upload date:
- Size: 36.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.14 {"installer":{"name":"uv","version":"0.9.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf072e9dfc6045c0ea9ba8ecd0f0da39bc85eea82c0b02ff65d37306bc0f276c
|
|
| MD5 |
2a5d590ee35de33b0199213f3f013392
|
|
| BLAKE2b-256 |
b95528060b2034d0796d4b13b88e85dbeee228df7d1fc0d045e823dd03c5111e
|
File details
Details for the file dt31-0.9.1-py3-none-any.whl.
File metadata
- Download URL: dt31-0.9.1-py3-none-any.whl
- Upload date:
- Size: 41.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.14 {"installer":{"name":"uv","version":"0.9.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2dbae935e8342be53d1d1e187f044706feca82f22f934e89121bb1d2c72033ac
|
|
| MD5 |
db02709fb3b7d15ba1a032c754bf2d92
|
|
| BLAKE2b-256 |
8e15ed05d0ee05a935880b023d1b4923a81bd2ed5e0ff9c488c1ef6b85f58a19
|