Skip to main content

A toy computer in Python.

Project description

dt31

PyPI - Version License: MIT pdoc Ruff Coverage Badge

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 run 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 .dt assembly files with text-based syntax
  • Command-Line Interface: Execute .dt files directly with the dt31 command
  • 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], or LC["a"] as a shortcut for L[ord("a")]
  • Registers: CPU registers R.a, R.b, R.c
  • Memory: Memory addresses M[100], indirect addressing M[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, and c)
  • 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

  • --debug or -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 PATH or -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 PATH or -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

# Control blank lines (default: preserve from source)
dt31 format --blank-lines auto program.dt    # Add blank lines before labels
dt31 format --blank-lines none program.dt    # Remove automatic blank lines
dt31 format --blank-lines preserve program.dt  # Preserve source formatting (default)

# 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, and Add are 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:

  1. Literals: In text syntax, bare numbers are literals (no L[...] wrapper needed)
  2. Characters: Use single quotes 'A' instead of LC["A"]
  3. Memory: The M prefix is optional (both [100] and M[100] work)
  4. Labels: Bare identifiers are labels (no Label(...) constructor needed)
  5. 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

# Control blank lines (default: "preserve")
text = program_to_text(program, blank_lines="auto")      # Add blank lines before labels
text = program_to_text(program, blank_lines="none")      # No automatic blank lines
text = program_to_text(program, blank_lines="preserve")  # Preserve source formatting

# 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:

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

License

MIT License

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dt31-0.10.0.tar.gz (37.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dt31-0.10.0-py3-none-any.whl (41.7 kB view details)

Uploaded Python 3

File details

Details for the file dt31-0.10.0.tar.gz.

File metadata

  • Download URL: dt31-0.10.0.tar.gz
  • Upload date:
  • Size: 37.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.15 {"installer":{"name":"uv","version":"0.9.15","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

Hashes for dt31-0.10.0.tar.gz
Algorithm Hash digest
SHA256 88a64f8ec92fd051e6320be49e328fa1edced19917ce656df28ae087ea46a0c8
MD5 dd5470fbb1e64a50ec735f6bbb6e1a6d
BLAKE2b-256 21c0cd2d86420918a1bebfb586324da108a179afb3c1295ca84f85878b9b873a

See more details on using hashes here.

File details

Details for the file dt31-0.10.0-py3-none-any.whl.

File metadata

  • Download URL: dt31-0.10.0-py3-none-any.whl
  • Upload date:
  • Size: 41.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.15 {"installer":{"name":"uv","version":"0.9.15","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

Hashes for dt31-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 92edb484ed76f3ae8b94f424c0c764404d6737a9c3e8c7b8874de6d751e45325
MD5 118f1207ac4a12b084f85978f054a295
BLAKE2b-256 2203eda3a564d3586b9a29a3dd1912ff0552ad70d8eb5b6639373ae0d5187ba8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page