diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d2f0b0..bcf4aef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - python-version: ["3.13"] + python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout repository @@ -22,15 +22,51 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Upgrade pip and install build backend + - name: Install uv run: | - python -m pip install --upgrade pip - python -m pip install build + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH - - name: Install project with dependencies + - name: Install dependencies run: | - pip install . + uv pip install --system -e . --group dev - - name: Run unittest + - name: Run pytest with coverage run: | - python -m unittest discover -v -s tests \ No newline at end of file + pytest --cov=src/tiny8 --cov-report=term-missing --cov-report=xml + + - name: Upload coverage to Codecov + if: matrix.python-version == '3.13' + uses: codecov/codecov-action@v4 + with: + files: ./coverage.xml + fail_ci_if_error: false + + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: | + uv pip install --system -e . --group dev + + - name: Run ruff check + run: | + ruff check src/ tests/ + + - name: Run ruff format check + run: | + ruff format --check src/ tests/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 80b931d..3a0c30d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/ dist/ wheels/ *.egg-info +.pytest_cache/ # Virtual environments .venv diff --git a/.vscode/settings.json b/.vscode/settings.json index bef49d0..b8c7d72 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,13 +7,9 @@ }, "editor.defaultFormatter": "charliermarsh.ruff" }, - "python.testing.unittestArgs": [ - "-v", - "-s", - "./tests", - "-p", - "test_*.py" + "python.testing.pytestArgs": [ + "tests" ], - "python.testing.pytestEnabled": false, - "python.testing.unittestEnabled": true + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 600cc9f..d3dab86 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,109 +1,352 @@ -# Contributing to tiny8 +# Contributing to Tiny8 + +Thank you for your interest in contributing to Tiny8! This guide will help you get started with contributing to our educational 8-bit CPU simulator. + +Whether you're fixing a bug, adding a feature, improving documentation, or creating new assembly examples, your contributions are welcome and appreciated. + +## ๐ŸŒŸ Ways to Contribute + +### ๐Ÿ› Bug Reports +- Search [existing issues](https://github.com/sql-hkr/tiny8/issues) first +- Provide a clear description and minimal reproduction steps +- Include your Python version and operating system +- Add relevant code snippets or assembly programs + +### ๐Ÿ’ก Feature Requests +- Open an issue describing the feature and its use case +- Discuss design decisions before implementing large changes +- Consider backward compatibility and educational value + +### ๐Ÿ“ Documentation +- Fix typos and improve clarity +- Add more code examples +- Enhance API documentation +- Create tutorials or guides + +### ๐ŸŽ“ Assembly Examples +- Create educational assembly programs +- Document algorithms and implementation details +- Add visualizations demonstrating key concepts + +### ๐Ÿงช Testing +- Improve test coverage +- Add edge case tests +- Test on different platforms + +## ๐Ÿš€ Getting Started + +### Prerequisites + +- **Python 3.11+** (3.11, 3.12, or 3.13 recommended) +- **Git** for version control +- **uv** (optional but recommended) for fast package management + +### Development Setup + +1. **Fork and clone the repository:** + + ```bash + git clone https://github.com/YOUR_USERNAME/tiny8.git + cd tiny8 + ``` + +2. **Set up development environment:** + + Using `uv` (recommended): + ```bash + # Install uv if you haven't already + # macOS/Linux: + curl -LsSf https://astral.sh/uv/install.sh | sh + + # Windows: + powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + + # Create virtual environment and install dependencies + uv venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + uv sync + ``` + + Using standard `pip`: + ```bash + python -m venv .venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + pip install -e ".[dev]" + ``` + +3. **Verify your setup:** + + ```bash + # Run tests + pytest + + # Check linting + ruff check . + + # Try the CLI + tiny8 examples/fibonacci.asm + ``` + +## ๐Ÿงช Testing + +### Running Tests -Thank you for your interest in contributing to tiny8 โ€” a compact AVR-like 8-bit CPU simulator and examples. This short guide explains how to get the project running locally, how to run tests and examples, the code style we prefer, and what to include when submitting a pull request. +```bash +# Run all tests +pytest -## Quick setup +# Run with coverage +pytest --cov=src/tiny8 --cov-report=html -```bash -git clone https://github.com/sql-hkr/tiny8.git -cd tiny8 -uv venv -source .venv/bin/activate -uv sync +# Run specific test file +pytest tests/test_arithmetic.py + +# Run specific test +pytest tests/test_arithmetic.py::test_add_basic + +# Verbose output +pytest -v + +# Show print statements +pytest -s ``` -> [!TIP] -> [uv](https://docs.astral.sh/uv/) is an extremely fast Python package and project manager, written in Rust. To install it, run: -> -> ```bash -> # On macOS and Linux. -> curl -LsSf https://astral.sh/uv/install.sh | sh -> -> # On Windows. -> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" -> ``` +### Writing Tests + +- Tests are located in the `tests/` directory +- Use `pytest` framework (configured in `pytest.ini`) +- Follow existing test patterns for consistency +- Test both success and failure cases +- Keep tests fast, focused, and deterministic + +**Example test structure:** + +```python +def test_feature_name(): + """Test description.""" + # Arrange + cpu = CPU() + cpu.load_program(...) + + # Act + cpu.step() + + # Assert + assert cpu.read_reg(16) == expected_value +``` -This flow sets up a development virtual environment, installs development requirements, and prepares the project for local editing and testing. +## ๐ŸŽจ Code Style -## Running examples +### Linting and Formatting -Examples live in the `examples/` directory. Run an example like: +We use **ruff** for both linting and formatting: ```bash -uv run examples/fibonacci.py +# Check for issues +ruff check . + +# Auto-fix issues +ruff check . --fix + +# Format code +ruff format . + +# Check formatting without changing +ruff format . --check ``` -> [!IMPORTANT] -> You will need `ffmpeg` installed on your system to generate a GIF or MP4 file. +### Style Guidelines + +- **Type hints**: Use type annotations for function signatures +- **Docstrings**: Use Google-style docstrings for public APIs +- **Line length**: 88 characters (ruff default) +- **Imports**: Group stdlib, third-party, and local imports +- **Naming**: + - Functions/variables: `snake_case` + - Classes: `PascalCase` + - Constants: `UPPER_CASE` + - Private methods: `_leading_underscore` + +**Example:** + +```python +def execute_instruction(self, opcode: int, operands: list[int]) -> None: + """Execute a single instruction. + + Args: + opcode: The instruction opcode. + operands: List of operand values. + + Raises: + InvalidOpcodeError: If opcode is not recognized. + """ + # Implementation +``` -### Tests +## ๐Ÿ“š Documentation -- Tests live in the `tests/` folder. Add unit tests for any bug fixes or new features. -- Use Python's built-in `unittest` framework for new tests. Keep tests small, fast and deterministic when possible. -- Aim for coverage of edge cases and error paths for logic changes. +### Building Documentation -### Linting & formatting +```bash +# Generate API documentation +sphinx-apidoc -efo docs/api/ src/ -- We use `ruff` for linting and auto-fixes. Run: +# Build HTML docs +cd docs +make html - ```bash - ruff check . - ruff format . - ``` +# View docs +open _build/html/index.html # macOS +# or +xdg-open _build/html/index.html # Linux +# or +start _build/html/index.html # Windows +``` -- When fixing/implementing code, make sure lint errors are resolved or justified in your PR. +### Documentation Guidelines + +- Keep documentation synchronized with code changes +- Add docstrings to all public functions, classes, and modules +- Update `README.md` for user-facing changes +- Update `docs/index.rst` for significant features +- Include code examples in docstrings when helpful + +## ๐ŸŽฏ Assembly Examples + +### Creating Examples + +1. Place assembly files in `examples/` directory +2. Use `.asm` extension +3. Include comprehensive comments +4. Start with a header comment explaining the algorithm + +**Example template:** + +```asm +; Algorithm Name +; Brief description of what this program does +; +; Algorithm explanation: +; - Step 1 description +; - Step 2 description +; +; Registers used: +; - R16: Purpose +; - R17: Purpose +; +; Expected result: Description + + ldi r16, 0 ; Initialize + ; ... your code + +done: + jmp done ; Halt +``` -## Documentation +### Example Best Practices -Docs are generated from the `docs/` directory. To build the HTML docs locally: +- **Educational value**: Demonstrate a clear concept +- **Comments**: Explain the "why", not just the "what" +- **Deterministic**: Use fixed inputs for reproducibility +- **Test coverage**: Add corresponding unit tests +- **Documentation**: Reference in README or docs if significant + +## ๐Ÿ”„ Pull Request Process + +### Before Submitting + +1. **Create an issue** for non-trivial changes to discuss approach +2. **Branch from main**: Use descriptive branch names + - `fix/short-description` for bug fixes + - `feat/short-description` for features + - `docs/short-description` for documentation + - `test/short-description` for tests +3. **Make atomic commits** with clear messages +4. **Update tests** to cover your changes +5. **Run the full test suite** and linter +6. **Update documentation** if needed + +### Commit Message Guidelines + +Write clear, concise commit messages: -```bash -sphinx-apidoc -efo docs/api/ src/ -make -C docs html -open docs/_build/html/index.html ``` +feat: add MUL instruction support -### Pull Request process +- Implement 8-bit multiplication +- Update instruction decoder +- Add comprehensive tests +- Update documentation + +Closes #123 +``` -1. Open an issue first for non-trivial changes (design or API changes) so maintainers can provide feedback before you invest time. -2. Work on a topic branch (not `main`). Use a descriptive branch name: `fix/short-desc`, `feat/short-desc`, or `doc/short-desc`. -3. Commit messages should be short and descriptive. If your changes are a bug fix or feature, reference the issue number if one exists. -4. Include or update tests that cover your changes. -5. Run the test suite, linter, and build the docs locally before opening the PR. -6. In the PR description include: - - What the change does and why - - Any notable design decisions or trade-offs - - How to run tests/examples to verify the change +**Format:** +- First line: `: ` (50 chars max) +- Types: `feat`, `fix`, `docs`, `test`, `refactor`, `perf`, `chore` +- Body: Explain what and why (optional) +- Footer: Reference issues (optional) -### PR checklist +### PR Checklist -- [ ] Branched from current `main` -- [ ] Tests added/updated -- [ ] Linting passes (`ruff`) and formatting applied +Before submitting your PR, ensure: + +- [ ] Code follows style guidelines (`ruff check` passes) +- [ ] All tests pass (`pytest` succeeds) +- [ ] New tests added for new functionality - [ ] Documentation updated (if applicable) -- [ ] Clear PR description and linked issue (if any) +- [ ] Commit messages are clear and descriptive +- [ ] PR description explains changes and motivation +- [ ] Branch is up-to-date with main +- [ ] No unrelated changes included + +## ๐Ÿ” Code Review Process + +- Maintainers will review PRs within a few days +- Address feedback constructively +- Push additional commits to the same branch +- Request re-review after addressing comments + +## ๐ŸŒ Community Guidelines + +### Code of Conduct + +- **Be respectful**: Treat everyone with respect and professionalism +- **Be welcoming**: Foster an inclusive environment for all contributors +- **Be collaborative**: Work together constructively +- **Be patient**: Remember we're all volunteers with varying experience levels + +### Communication +- **Issues**: Use for bug reports, feature requests, and discussions +- **Pull Requests**: Use for code contributions +- **Discussions**: Use GitHub Discussions for questions and ideas +- **Email**: For security issues only (see below) -## Adding examples +## ๐Ÿ”’ Security Issues -- Place assembly programs under `examples/` and a small runner script if needed. -- Keep examples deterministic (fixed inputs) unless you document why randomness is used. -- If an example adds new instructions or behavior, add tests under `tests/`. +If you discover a security vulnerability: -## Code of conduct +1. **Do NOT** open a public issue +2. Email the maintainers directly (see `pyproject.toml` for contact info) +3. Provide detailed information about the vulnerability +4. Allow time for a fix before public disclosure -Be respectful and professional. Follow common community standards when discussing issues and reviewing contributions. +## ๐Ÿ“„ License -## Reporting security issues +By contributing to Tiny8, you agree that your contributions will be licensed under the [MIT License](LICENSE). -If you discover a security vulnerability, please contact the maintainers directly (see `pyproject.toml` author email) instead of creating a public issue. +Your contributions will be attributed in the project's commit history and release notes. -## License +## โ“ Questions? -By contributing you agree that your contributions will be licensed under the project's MIT License. +- Check the [documentation](https://sql-hkr.github.io/tiny8/) +- Search [existing issues](https://github.com/sql-hkr/tiny8/issues) +- Open a new issue for discussion +- Join [GitHub Discussions](https://github.com/sql-hkr/tiny8/discussions) -## Need help? +## ๐Ÿ™ Thank You! -Open an issue describing what you'd like to do, or reach out via the author email in `pyproject.toml`. +Thank you for taking the time to contribute to Tiny8! Your efforts help make this project a better learning tool for everyone exploring computer architecture. -Thank you for helping improve Tiny8! \ No newline at end of file +Every contribution, no matter how small, makes a difference. We appreciate your support! โญ diff --git a/README.md b/README.md index c946969..a331cca 100644 --- a/README.md +++ b/README.md @@ -1,297 +1,406 @@ # Tiny8 -![PyPI version](https://img.shields.io/pypi/v/tiny8) -![License](https://img.shields.io/github/license/sql-hkr/tiny8) -![Python versions](https://img.shields.io/pypi/pyversions/tiny8) -![CI](https://img.shields.io/github/actions/workflow/status/sql-hkr/tiny8/ci.yml?label=CI) +[![PyPI version](https://img.shields.io/pypi/v/tiny8)](https://pypi.org/project/tiny8/) +[![License](https://img.shields.io/github/license/sql-hkr/tiny8)](LICENSE) +[![Python versions](https://img.shields.io/pypi/pyversions/tiny8)](https://pypi.org/project/tiny8/) +[![CI](https://img.shields.io/github/actions/workflow/status/sql-hkr/tiny8/ci.yml?label=CI)](https://github.com/sql-hkr/tiny8/actions) -Tiny8 is a lightweight toolkit that allows you to explore how computers work at their core through small-scale memory models, handcrafted assembly, and lightweight in-memory data structures. -Designed for rapid experimentation, Tiny8 embraces minimalism with zero unnecessary dependencies, a clean design, and intuitive visualization tools that make learning, debugging, and tinkering enjoyable. +> **An educational 8-bit CPU simulator with interactive visualization** -![bubblesort](/docs/_static/examples/bubblesort.gif) +Tiny8 is a lightweight and educational toolkit for exploring the fundamentals of computer architecture through hands-on assembly programming and real-time visualization. Designed for learning and experimentation, it features an AVR-inspired 8-bit CPU with 32 registers, a rich instruction set, and powerful debugging tools โ€” all with zero heavy dependencies. -โญ๏ธ NEW FEATURE! - +
+ Animated bubble sort visualization +

Real-time visualization of a bubble sort algorithm executing on Tiny8

+
-Why Tiny8? +## โœจ Features -- Lightweight: tiny install footprint and no heavy runtime dependencies. -- Educational: clear primitives and examples that demonstrate CPU concepts, memory layout, and algorithms. -- Fast feedback loop: assemble, run, and visualize within seconds to iterate on ideas. -- Extensible: meant for experiments, teaching, demos, and small tools that benefit from a predictable, tiny VM. +### ๐ŸŽฏ **Interactive Terminal Debugger** +CLI visualizer screenshot -Who should use it? +- **Vim-style navigation**: Step through execution with intuitive keyboard controls +- **Change highlighting**: See exactly what changed at each step (registers, flags, memory) +- **Advanced search**: Find instructions, track register/memory changes, locate PC addresses +- **Marks and bookmarks**: Set and jump to important execution points +- **Vertical scrolling**: Handle programs with large memory footprints -- Students learning low-level programming, assembly, or computer architecture who want hands-on examples. -- Educators building demos and interactive lessons that visualize how registers and memory change. -- Hobbyists and hackers experimenting with toy CPUs, compact data layouts, or custom instruction ideas. -- Developers who want a tiny, readable simulator to prototype algorithms that manipulate memory directly. +### ๐ŸŽฌ **Graphical Animation** +- Generate high-quality GIF/MP4 videos of program execution +- Visualize register evolution, memory access patterns, and flag changes +- Perfect for presentations, documentation, and learning materials -Get started +### ๐Ÿ—๏ธ **Complete 8-bit Architecture** +- **32 general-purpose registers** (R0-R31) +- **8-bit ALU** with arithmetic, logical, and bit manipulation operations +- **Status register (SREG)** with 8 condition flags +- **2KB address space** for unified memory and I/O +- **Stack operations** with dedicated stack pointer +- **AVR-inspired instruction set** with 60+ instructions -- Follow the Installation section below to install from PyPI or set up a development environment. -- See the Examples section (like the bubble sort demo) to run real programs and watch the visualizer in action. -- Dive into the API Reference for details on the CPU, assembler, and visualization helpers. +### ๐Ÿ“š **Educational Focus** +- Clean, readable Python implementation +- Comprehensive examples (Fibonacci, bubble sort, factorial, and more) +- Step-by-step execution traces for debugging +- Full API documentation and instruction set reference -## Installation +## ๐Ÿš€ Quick Start -Tiny8 supports Python 3.11 and newer. It has no heavy external dependencies and is suitable for inclusion in virtual environments. Follow the steps below to prepare your environment and install from source or PyPI. +### Installation -### Prerequisites +```bash +pip install tiny8 +``` + +### Your First Program -- Python 3.11+ -- Git (for installing from the repository) -- Recommended: create and use a virtual environment +Create `fibonacci.asm`: +```asm +; Fibonacci Sequence Calculator +; Calculates the 10th Fibonacci number (F(10) = 55) +; F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2) +; +; Results stored in registers: +; R16 and R17 hold the two most recent Fibonacci numbers -### From source (development) + ldi r16, 0 ; F(0) = 0 + ldi r17, 1 ; F(1) = 1 + ldi r18, 9 ; Counter: 9 more iterations to reach F(10) + +loop: + add r16, r17 ; F(n) = F(n-1) + F(n-2) + mov r19, r16 ; Save result temporarily + mov r16, r17 ; Shift: previous = current + mov r17, r19 ; Shift: current = new result + dec r18 ; Decrement counter + brne loop ; Continue if counter != 0 + +done: + jmp done ; Infinite loop at end +``` +Run it: ```bash -git clone https://github.com/sql-hkr/tiny8.git -cd tiny8 -uv venv -source .venv/bin/activate -uv sync +tiny8 fibonacci.asm # Interactive debugger +tiny8 fibonacci.asm -m ani -o fibonacci.gif # Generate animation ``` -> [!TIP] -> [uv](https://docs.astral.sh/uv/) is an extremely fast Python package and project manager, written in Rust. To install it, run: -> -> ```bash -> # On macOS and Linux. -> curl -LsSf https://astral.sh/uv/install.sh | sh -> -> # On Windows. -> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" -> ``` +### Python API -This flow sets up a development virtual environment, installs development requirements, and prepares the project for local editing and testing. +```python +from tiny8 import CPU, assemble_file -### From PyPI (stable) +asm = assemble_file("fibonacci.asm") +cpu = CPU() +cpu.load_program(asm) +cpu.run(max_steps=1000) -```bash -uv add tiny8 +print(f"Result: R17 = {cpu.read_reg(17)}") # Final Fibonacci number ``` -## CLI Visualizer +## ๐Ÿ’ก Why Tiny8? -Tiny8 includes a lightweight terminal-based visualizer that lets you step through a program's execution trace in your terminal. It shows the status register (SREG), the 32 general-purpose registers, and a compact view of a configurable memory range for each step. +**For Students** โ€” Write assembly, see immediate results with visual feedback. Understand how each instruction affects CPU state without abstractions. -Key points: +**For Educators** โ€” Interactive demonstrations, easy assignment creation, and generate animations for lectures. -- The CLI visualizer expects the CPU to have a populated `step_trace` (run the CPU first with `cpu.run(...)`). -- Controls are keyboard-driven (play/pause, step forward/back, jump, quit) and work in most POSIX terminals that support curses. -- For higher-fidelity animations (GIFs) and interactive matplotlib views, use the `Visualizer` class which requires `matplotlib`. +**For Hobbyists** โ€” Rapid algorithm prototyping at the hardware level with minimal overhead and an extensible, readable codebase. -Interactive controls: +## ๐Ÿ“– Documentation -```text -Space - toggle play/pause -l or > - next step -h or < - previous step -w - jump forward 10 steps -b - jump back 10 steps -0 - jump to first step -$ - jump to last step -q or ESC - quit -``` +- [**Full Documentation**](https://sql-hkr.github.io/tiny8/) โ€” Complete API reference and guides +- [**Instruction Set Reference**](#instruction-set-reference) โ€” All 60+ instructions +- [**CLI Guide**](#interactive-cli-controls) โ€” Terminal debugger keyboard shortcuts +- [**Examples**](#examples) โ€” Sample programs with explanations +- [**Contributing**](CONTRIBUTING.md) โ€” Guidelines for contributors -Programmatic usage +## ๐ŸŽฎ Interactive CLI Controls -You can invoke the terminal visualizer directly from Python after running the CPU: +The terminal-based debugger provides powerful navigation and inspection capabilities. -```python -from tiny8 import CPU, assemble_file -from tiny8 import run_cli +### Navigation & Playback -prog, labels = assemble_file("examples/bubblesort.asm") -cpu = CPU() -cpu.load_program(prog, labels) -cpu.run(max_cycles=15000) +- `l` / `h` or `โ†’` / `โ†` โ€” Step forward/backward +- `w` / `b` โ€” Jump ยฑ10 steps +- `0` / `$` โ€” Jump to first/last step +- `Space` โ€” Play/pause auto-execution +- `[` / `]` โ€” Decrease/increase playback speed -# Run the curses-based CLI visualizer -run_cli(cpu, mem_addr_start=100, mem_addr_end=131) -``` +### Display & Inspection -Tiny8 provides a `tiny8` console script (see `pyproject.toml`). You can run the CLI or the animation mode directly: +- `r` โ€” Toggle register display (all/changed only) +- `M` โ€” Toggle memory display (all/non-zero only) +- `=` โ€” Show detailed step information +- `j` / `k` โ€” Scroll memory view up/down -```bash -# Run the curses-based CLI visualizer for an assembly file -uv run tiny8 examples/bubblesort.asm # --mode cli --mem-start 100 --mem-end 131 +### Search & Navigation Commands (press `:`) -# Produce an animated GIF using matplotlib (requires matplotlib) -uv run tiny8 examples/bubblesort.asm --mode ani -o bubblesort.gif --mem-start 100 --mem-end 131 --plot-every 100 --fps 60 -``` +- `:123` โ€” Jump to step 123 +- `:+50` / `:-20` โ€” Relative jumps +- `:/ldi` โ€” Search forward for instruction "ldi" +- `:?add` โ€” Search backward for "add" +- `:@0x100` โ€” Jump to PC address 0x100 +- `:r10` โ€” Find next change to register R10 +- `:r10=42` โ€” Find where R10 equals 42 +- `:m100` โ€” Find next change to memory[100] +- `:fZ` โ€” Find next change to flag Z -> [!IMPORTANT] -> Tiny8 uses Python's built-in curses module (Unix-like systems). On Windows, use an appropriate terminal that supports curses or run via WSL. +### Marks & Help -## Examples +- `ma` โ€” Set mark 'a' at current step +- `'a` โ€” Jump to mark 'a' +- `/` โ€” Show help screen +- `q` or `ESC` โ€” Quit -### Bubble sort +## ๐ŸŽ“ Examples -This example demonstrates a simple bubble sort algorithm implemented in assembly language for the Tiny8 CPU. The program first fills a section of RAM with pseudo-random bytes, then sorts those bytes using the bubble sort algorithm. Finally, a Python script runs the assembly program and visualizes the sorting process. +The `examples/` directory contains programs demonstrating key concepts: -bubblesort.asm: +| Example | Description | +|---------|-------------| +| `fibonacci.asm` | Fibonacci sequence using registers | +| `bubblesort.asm` | Sorting algorithm with memory visualization | +| `factorial.asm` | Recursive factorial calculation | +| `find_max.asm` | Finding maximum value in array | +| `is_prime.asm` | Prime number checking algorithm | +| `gcd.asm` | Greatest common divisor (Euclidean algorithm) | -```asm -; Bubble sort using RAM (addresses 100..131) - 32 elements -; Purpose: fill RAM[100..131] with pseudo-random bytes and sort them -; Registers (use R16..R31 for LDI immediates): -; R16 - base address (start = 100) -; R17 - index / loop counter for initialization -; R18 - PRNG state (seed) -; R19..R24 - temporary registers used in loops and swaps -; R25 - PRNG multiplier (kept aside to avoid clobber in MUL) -; -; The code below is split into two phases: -; 1) init_loop: generate and store 32 pseudo-random bytes at RAM[100..131] -; 2) outer/inner loops: perform a simple bubble sort over those 32 bytes - - ; initialize pointers and PRNG - ldi r16, 100 ; base address - ldi r17, 0 ; index = 0 - ldi r18, 123 ; PRNG seed - ldi r25, 75 ; PRNG multiplier (kept in r25 so mul doesn't clobber it) - -init_loop: - ; PRNG step: r2 := lowbyte(r2 * 75), then tweak - mul r18, r25 ; r18 = low byte of (r18 * 75) - inc r18 ; small increment to avoid repeating patterns - ; store generated byte into memory at base + index - st r16, r18 ; RAM[base] = r18 - inc r16 ; advance base pointer - inc r17 ; increment index - ldi r23, 32 - cp r17, r23 - brne init_loop - -; Bubble sort for 32 elements (perform passes until i == 31) - ldi r18, 0 ; i = 0 (outer loop counter) -outer_loop: - ldi r19, 0 ; j = 0 (inner loop counter) -inner_loop: - ; compute address of element A = base + j - ldi r20, 100 - add r20, r19 - ld r21, r20 ; r21 = A - ; compute address of element B = base + j + 1 - ldi r22, 100 - add r22, r19 - ldi r23, 1 - add r22, r23 - ld r24, r22 ; r24 = B - ; compare A and B (we'll swap if A < B) - cp r21, r24 ; sets carry if r21 < r24 - brcc no_swap - ; swap A and B: store B into A's address, A into B's address - st r20, r24 - st r22, r21 -no_swap: - inc r19 - ldi r23, 31 - cp r19, r23 - breq end_inner - jmp inner_loop -end_inner: - inc r18 - ldi r23, 31 - cp r18, r23 - breq done - jmp outer_loop +### Bubble Sort -done: - jmp done +Sort 32 bytes in memory: + +```bash +tiny8 examples/bubblesort.asm -ms 0x60 -me 0x80 # Watch live +tiny8 examples/bubblesort.asm -m ani -o sort.gif -ms 0x60 -me 0x80 # Create GIF ``` -Python Code: +### Using Python ```python -from tiny8 import CPU, Visualizer, assemble_file +from tiny8 import CPU, assemble_file -prog, labels = assemble_file("examples/bubblesort.asm") cpu = CPU() -cpu.load_program(prog, labels) -cpu.run(max_cycles=15000) - -print([cpu.read_ram(i) for i in range(100, 132)]) - -viz = Visualizer(cpu) -base = 100 -viz.animate_combined( - interval=1, - mem_addr_start=base, - mem_addr_end=base + 31, - plot_every=100, - # filename="bubblesort.gif", - # fps=60, -) +cpu.load_program(*assemble_file("examples/bubblesort.asm")) +cpu.run() + +print("Sorted:", [cpu.read_ram(i) for i in range(100, 132)]) ``` -Example Output: +## ๐Ÿ”ง CLI Options + +### Command Syntax ```bash -[247, 243, 239, 238, 227, 211, 210, 195, 190, 187, 186, 171, 167, 159, 155, 150, 142, 139, 135, 130, 127, 106, 102, 94, 54, 50, 34, 26, 23, 15, 10, 6] +tiny8 FILE [OPTIONS] ``` -## Instruction set summary - -Below is a concise, categorized summary of the Tiny8 instruction set (mnemonics are case-insensitive). This is a quick reference โ€” for implementation details see `src/tiny8/cpu.py`. +### General Options + +| Option | Description | +|--------|-------------| +| `-m, --mode {cli,ani}` | Visualization mode: `cli` for interactive debugger (default), `ani` for animation | +| `-v, --version` | Show version and exit | +| `--max-steps N` | Maximum execution steps (default: `15000`) | + +### Memory Display Options + +| Option | Description | +|--------|-------------| +| `-ms, --mem-start ADDR` | Starting memory address (decimal or `0xHEX`, default: `0x00`) | +| `-me, --mem-end ADDR` | Ending memory address (decimal or `0xHEX`, default: `0xFF`) | + +### CLI Mode Options + +| Option | Description | +|--------|-------------| +| `-d, --delay SEC` | Initial playback delay in seconds (default: `0.15`) | + +### Animation Mode Options + +| Option | Description | +|--------|-------------| +| `-o, --output FILE` | Output filename (`.gif`, `.mp4`, `.png`) | +| `-f, --fps FPS` | Frames per second (default: `60`) | +| `-i, --interval MS` | Update interval in milliseconds (default: `1`) | +| `-pe, --plot-every N` | Update plot every N steps (default: `100`, higher = faster) | + +> **Windows**: CLI debugger requires WSL or `windows-curses`. Animation works natively. + +## ๐Ÿ“‹ Instruction Set Reference + +Tiny8 implements an AVR-inspired instruction set with 62 instructions organized into logical categories. All mnemonics are case-insensitive. Registers are specified as R0-R31, immediates support decimal, hex (`$FF` or `0xFF`), and binary (`0b11111111`) notation. + +### Data Transfer + +| Instruction | Description | Example | +|-------------|-------------|---------| +| `LDI Rd, K` | Load 8-bit immediate into register | `ldi r16, 42` | +| `MOV Rd, Rr` | Copy register to register | `mov r17, r16` | +| `LD Rd, Rr` | Load from RAM at address in Rr | `ld r18, r16` | +| `ST Rr, Rs` | Store Rs to RAM at address in Rr | `st r16, r18` | +| `IN Rd, port` | Read from I/O port into register | `in r16, 0x3F` | +| `OUT port, Rr` | Write register to I/O port | `out 0x3F, r16` | +| `PUSH Rr` | Push register onto stack | `push r16` | +| `POP Rd` | Pop from stack into register | `pop r16` | + +### Arithmetic Operations + +| Instruction | Description | Example | +|-------------|-------------|---------| +| `ADD Rd, Rr` | Add registers | `add r16, r17` | +| `ADC Rd, Rr` | Add with carry | `adc r16, r17` | +| `SUB Rd, Rr` | Subtract registers | `sub r16, r17` | +| `SUBI Rd, K` | Subtract immediate | `subi r16, 10` | +| `SBC Rd, Rr` | Subtract with carry | `sbc r16, r17` | +| `SBCI Rd, K` | Subtract immediate with carry | `sbci r16, 5` | +| `INC Rd` | Increment register | `inc r16` | +| `DEC Rd` | Decrement register | `dec r16` | +| `MUL Rd, Rr` | Multiply (result in Rd:Rd+1) | `mul r16, r17` | +| `DIV Rd, Rr` | Divide (quotientโ†’Rd, remainderโ†’Rd+1) | `div r16, r17` | +| `NEG Rd` | Two's complement negation | `neg r16` | +| `ADIW Rd, K` | Add immediate to word (16-bit) | `adiw r24, 1` | +| `SBIW Rd, K` | Subtract immediate from word | `sbiw r24, 1` | + +### Logical & Bit Operations + +| Instruction | Description | Example | +|-------------|-------------|---------| +| `AND Rd, Rr` | Logical AND | `and r16, r17` | +| `ANDI Rd, K` | AND with immediate | `andi r16, 0x0F` | +| `OR Rd, Rr` | Logical OR | `or r16, r17` | +| `ORI Rd, K` | OR with immediate | `ori r16, 0x80` | +| `EOR Rd, Rr` | Exclusive OR | `eor r16, r17` | +| `EORI Rd, K` | XOR with immediate | `eori r16, 0xFF` | +| `COM Rd` | One's complement | `com r16` | +| `CLR Rd` | Clear register (XOR with self) | `clr r16` | +| `SER Rd` | Set register to 0xFF | `ser r16` | +| `TST Rd` | Test for zero or negative | `tst r16` | +| `SWAP Rd` | Swap nibbles (high/low 4 bits) | `swap r16` | +| `SBI port, bit` | Set bit in I/O register | `sbi 0x18, 3` | +| `CBI port, bit` | Clear bit in I/O register | `cbi 0x18, 3` | + +### Shifts & Rotates + +| Instruction | Description | Example | +|-------------|-------------|---------| +| `LSL Rd` | Logical shift left | `lsl r16` | +| `LSR Rd` | Logical shift right | `lsr r16` | +| `ROL Rd` | Rotate left through carry | `rol r16` | +| `ROR Rd` | Rotate right through carry | `ror r16` | + +### Control Flow + +| Instruction | Description | Example | +|-------------|-------------|---------| +| `JMP label` | Unconditional jump | `jmp loop` | +| `RJMP offset` | Relative jump | `rjmp -5` | +| `CALL label` | Call subroutine | `call function` | +| `RCALL offset` | Relative call | `rcall -10` | +| `RET` | Return from subroutine | `ret` | +| `RETI` | Return from interrupt | `reti` | +| `BRNE label` | Branch if not equal (Z=0) | `brne loop` | +| `BREQ label` | Branch if equal (Z=1) | `breq done` | +| `BRCS label` | Branch if carry set (C=1) | `brcs overflow` | +| `BRCC label` | Branch if carry clear (C=0) | `brcc no_carry` | +| `BRGE label` | Branch if greater/equal | `brge positive` | +| `BRLT label` | Branch if less than | `brlt negative` | +| `BRMI label` | Branch if minus (N=1) | `brmi negative` | +| `BRPL label` | Branch if plus (N=0) | `brpl positive` | + +### Compare Instructions + +| Instruction | Description | Example | +|-------------|-------------|---------| +| `CP Rd, Rr` | Compare registers (Rd - Rr) | `cp r16, r17` | +| `CPI Rd, K` | Compare with immediate | `cpi r16, 42` | +| `CPSE Rd, Rr` | Compare, skip if equal | `cpse r16, r17` | + +### Skip Instructions + +| Instruction | Description | Example | +|-------------|-------------|---------| +| `SBRS Rd, bit` | Skip if bit in register is set | `sbrs r16, 7` | +| `SBRC Rd, bit` | Skip if bit in register is clear | `sbrc r16, 7` | +| `SBIS port, bit` | Skip if bit in I/O register is set | `sbis 0x16, 3` | +| `SBIC port, bit` | Skip if bit in I/O register is clear | `sbic 0x16, 3` | + +### MCU Control + +| Instruction | Description | Example | +|-------------|-------------|---------| +| `NOP` | No operation | `nop` | +| `SEI` | Set global interrupt enable | `sei` | +| `CLI` | Clear global interrupt enable | `cli` | + +### Status Register (SREG) Flags + +The 8-bit status register contains condition flags updated by instructions: + +| Bit | Flag | Description | +|-----|------|-------------| +| 7 | **I** | Global interrupt enable | +| 6 | **T** | Bit copy storage | +| 5 | **H** | Half carry (BCD arithmetic) | +| 4 | **S** | Sign bit (N โŠ• V) | +| 3 | **V** | Two's complement overflow | +| 2 | **N** | Negative | +| 1 | **Z** | Zero | +| 0 | **C** | Carry/borrow | + +Flags are used for conditional branching and tracking arithmetic results. + +### Assembly Syntax Notes + +- **Comments**: Use `;` for line comments +- **Labels**: Must end with `:` (e.g., `loop:`) +- **Registers**: Case-insensitive R0-R31 (r16, R16 equivalent) +- **Immediates**: Decimal (42), hex ($2A, 0x2A), binary (0b00101010) +- **Whitespace**: Flexible indentation, spaces/tabs interchangeable + +## ๐Ÿ—๏ธ Architecture Overview + +### CPU Components + +- **32 General-Purpose Registers** (R0-R31) โ€” 8-bit working registers +- **Program Counter (PC)** โ€” 16-bit, addresses up to 64KB +- **Stack Pointer (SP)** โ€” 16-bit, grows downward from high memory +- **Status Register (SREG)** โ€” 8 condition flags (I, T, H, S, V, N, Z, C) +- **64KB Address Space** โ€” Unified memory for RAM and I/O + +### Memory Map -- Data transfer - - LDI Rd, K โ€” load immediate into register - - MOV Rd, Rr โ€” copy register - - LD Rd, Rr_addr โ€” load from RAM at address in register - - ST Rr_addr, Rr โ€” store register into RAM at address in register - - IN Rd, port โ€” read byte from RAM/IO into register - - OUT port, Rr โ€” write register to RAM/IO - - PUSH Rr / POP Rd โ€” stack push/pop +```text +0x0000 - 0x001F Memory-mapped I/O (optional) +0x0020 - 0xFFFF Available RAM (stack grows downward from top) +``` -- Arithmetic - - ADD Rd, Rr โ€” add registers - - ADC Rd, Rr โ€” add with carry - - SUB Rd, Rr / SUBI Rd, K โ€” subtraction - - SBC Rd, Rr / SBCI Rd, K โ€” subtract with carry/borrow - - INC Rd / DEC Rd โ€” increment / decrement - - MUL Rd, Rr โ€” 8x8 -> 16 multiply (low/high in Rd/Rd+1) - - DIV Rd, Rr โ€” unsigned divide (quotient->Rd, remainder->Rd+1) - - NEG Rd โ€” two's complement negate - - CLR Rd / SER Rd โ€” clear or set register to all ones +## ๐Ÿงช Testing -- Logical and bit ops - - AND Rd, Rr / ANDI Rd, K โ€” bitwise AND - - OR Rd, Rr / ORI Rd, K โ€” bitwise OR - - EOR Rd, Rr / EORI Rd, K โ€” exclusive OR - - COM Rd โ€” one's complement - - SWAP Rd โ€” swap nibbles - - TST Rd โ€” test for zero or minus - - SBI/CBI / SBIS/SBIC / SBRS/SBRC โ€” set/clear/test single bits and conditional skips +```bash +pytest # Run all tests +pytest --cov=src/tiny8 --cov-report=html # With coverage +pytest tests/test_arithmetic.py # Specific test file +``` -- Shifts & rotates - - LSL Rd / LSR Rd โ€” logical shift left/right - - ROL Rd / ROR Rd โ€” rotate through carry +## ๐Ÿค Contributing -- Word (16-bit) ops - - SBIW / ADIW โ€” simplified word add/subtract helpers for register pairs +Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. -- Control flow - - JMP label / RJMP offset โ€” unconditional jump - - CALL label / RCALL offset โ€” call subroutine (push return address) - - RET / RETI โ€” return from subroutine / return from interrupt (sets I) - - BRNE / BREQ / BRCS / BRCC / BRGE / BRLT โ€” conditional branches based on flags - - CP Rd, Rr / CPI Rd, K โ€” compare (sets flags) - - CPSE Rd, Rr โ€” compare and skip if equal +**Areas for contribution**: New instructions, example programs, documentation, visualizations, performance optimizations. -Use the assembler in `src/tiny8/assembler.py` (or `parse_asm`) to write programs โ€” register operands are specified as R0..R31 and immediates accept decimal, $hex, 0x, or 0b binary notation. +## ๐Ÿ“„ License -## API Reference +MIT License โ€” see [LICENSE](LICENSE) for details. -The API section documents the public modules, classes, functions, and configuration options. See: +## ๐Ÿ“ž Support -- [tiny8 package](https://sql-hkr.github.io/tiny8/api/tiny8.html) +- **Issues**: [GitHub Issues](https://github.com/sql-hkr/tiny8/issues) +- **Discussions**: [GitHub Discussions](https://github.com/sql-hkr/tiny8/discussions) +- **Documentation**: [sql-hkr.github.io/tiny8](https://sql-hkr.github.io/tiny8/) -## License +--- -Tiny8 is licensed under the MIT License. See [LICENSE](LICENSE) for details. +**Made with โค๏ธ for learners, educators, and curious minds** -Contributions, bug reports, and pull requests are welcome; please follow the repository's CONTRIBUTING guidelines. +*Star โญ the repo if you find it useful!* diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/_static/examples/bubblesort.gif b/docs/_static/examples/bubblesort.gif deleted file mode 100644 index 6e634aa..0000000 Binary files a/docs/_static/examples/bubblesort.gif and /dev/null differ diff --git a/docs/architecture.rst b/docs/architecture.rst new file mode 100644 index 0000000..4f7f9bb --- /dev/null +++ b/docs/architecture.rst @@ -0,0 +1,361 @@ +Architecture +============ + +This guide provides a detailed overview of the Tiny8 CPU architecture, including its registers, memory model, instruction execution, and status flags. + +CPU Overview +------------ + +Tiny8 implements a simplified 8-bit CPU architecture inspired by the AVR family (ATmega series). The design prioritizes educational clarity over cycle-accurate emulation, making it ideal for learning computer architecture fundamentals. + +Key Specifications +~~~~~~~~~~~~~~~~~~ + +* **Word size**: 8 bits +* **Registers**: 32 general-purpose 8-bit registers (R0-R31) +* **Memory**: 2KB RAM (configurable) +* **Stack**: Grows downward from high memory +* **Instruction set**: ~60 instructions (AVR-inspired) +* **Status register**: 8 flags (SREG) + +Register Architecture +--------------------- + +General-Purpose Registers +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tiny8 has 32 general-purpose 8-bit registers, labeled **R0** through **R31**. + +.. code-block:: text + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ R0 โ”‚ R1 โ”‚ R2 โ”‚ ... โ”‚ R31 โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ 32 x 8-bit registers โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +All registers are fully general-purpose - there are no dedicated accumulator, index, or pointer registers. However, some instructions may work only with specific register ranges: + +* **LDI (Load Immediate)**: Only works with R16-R31 +* **Most arithmetic/logic**: Works with any register R0-R31 + +.. note:: + While all registers are general-purpose, you may want to establish conventions in your programs (e.g., using R0-R15 for temporary values and R16-R31 for important data). + +Special Registers +~~~~~~~~~~~~~~~~~ + +In addition to general-purpose registers, the CPU has several special registers: + +Program Counter (PC) +^^^^^^^^^^^^^^^^^^^^ + +* Points to the current instruction in the program +* Automatically increments after each instruction +* Modified by branch, jump, and call instructions +* **Width**: Depends on program size (virtual, not a physical register) + +Stack Pointer (SP) +^^^^^^^^^^^^^^^^^^ + +* Points to the top of the stack in memory +* Initialized to the end of RAM (default: 0x07FF with 2KB RAM) +* Decrements on PUSH, increments on POP +* Used implicitly by CALL and RET instructions + +Status Register (SREG) +^^^^^^^^^^^^^^^^^^^^^^ + +* 8-bit register containing condition flags +* Updated by arithmetic, logic, and comparison instructions +* Used by conditional branch instructions + +.. code-block:: text + + SREG: [ I | T | H | S | V | N | Z | C ] + Bit 7 Bit 0 + +See the `Status Flags`_ section for detailed flag descriptions. + +Memory Model +------------ + +Address Space +~~~~~~~~~~~~~ + +Tiny8 uses a flexible, byte-addressable memory model with configurable RAM size. + +**Default Configuration:** + +* RAM size: 2048 bytes (2KB) +* Address range: 0x0000 to 0x07FF +* Stack pointer initializes to 0x07FF (ram_size - 1) + +**Memory Layout:** + +.. code-block:: text + + 0x07FF โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ† SP (initial position) + โ”‚ โ”‚ + โ”‚ Stack Area โ”‚ Stack grows downward (PUSH decrements SP) + โ”‚ โ”‚ + โ”‚ โ†“ โ†“ โ†“ โ†“ โ†“ โ†“ โ”‚ + โ”‚ โ”‚ + โ”‚ โ”‚ + โ”‚ Free RAM โ”‚ Available for program use + โ”‚ โ”‚ + โ”‚ โ”‚ + โ”‚ โ†‘ โ†‘ โ†‘ โ†‘ โ†‘ โ†‘ โ”‚ + โ”‚ โ”‚ + โ”‚ Data Area โ”‚ Variables and data grow upward + โ”‚ โ”‚ + 0x0000 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Program starts here (PC=0) + +**Key Points:** + +* Memory is byte-addressable (each address holds one 8-bit value) +* Full 2KB address space available by default +* No fixed boundaries between stack and data areas +* Stack and data can grow toward each other (watch for collisions!) +* All memory initializes to 0 on CPU creation +* RAM size is configurable via ``Memory(ram_size=...)`` + +**Memory Access:** + +The CPU uses register-indirect addressing for memory operations: + +.. code-block:: asm + + ; Load from memory + ldi r26, 0x00 ; Set address low byte + ldi r27, 0x02 ; Set address high byte (address = 0x0200) + ld r16, r26 ; Load byte from address in R26 into R16 + + ; Store to memory + ldi r26, 0x50 ; Address = 0x50 + ldi r16, 42 ; Value to store + st r26, r16 ; Store R16 to memory[R26] + +Memory Operations +~~~~~~~~~~~~~~~~~ + +**Stack Operations** + +The stack is used for temporary storage and subroutine calls: + +.. code-block:: asm + + push r16 ; Push R16 onto stack (SP decrements) + pop r17 ; Pop from stack into R17 (SP increments) + + call my_function ; Pushes return address, then jumps + ret ; Pops return address, then returns + +**I/O Operations** + +Access I/O ports and special registers: + +.. code-block:: asm + + in r16, 0x3F ; Read from I/O port 0x3F into R16 + out 0x3F, r16 ; Write R16 to I/O port 0x3F + +Memory Initialization +~~~~~~~~~~~~~~~~~~~~~ + +* All memory is initialized to 0 on CPU creation +* The assembler loads program instructions starting at address 0 +* Stack pointer is initialized to the top of memory + +Status Flags +------------ + +The Status Register (SREG) contains 8 condition flags that reflect the result of operations and control program flow. + +Flag Descriptions +~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 10 15 75 + + * - Bit + - Name + - Description + * - 7 + - **I** (Interrupt) + - Global interrupt enable flag. When set, interrupts are enabled. + * - 6 + - **T** (Transfer) + - Bit copy storage. Used by BLD and BST instructions for bit manipulation. + * - 5 + - **H** (Half Carry) + - Half-carry flag. Set when there's a carry from bit 3 to bit 4 in arithmetic operations. Used for BCD arithmetic. + * - 4 + - **S** (Sign) + - Sign flag, computed as N โŠ• V (N XOR V). Indicates true sign of result considering two's complement overflow. + * - 3 + - **V** (Overflow) + - Two's complement overflow flag. Set when signed arithmetic produces a result outside the range -128 to +127. + * - 2 + - **N** (Negative) + - Negative flag. Set when the result of an operation has bit 7 set (i.e., the result is negative in two's complement). + * - 1 + - **Z** (Zero) + - Zero flag. Set when the result of an operation is zero. + * - 0 + - **C** (Carry) + - Carry flag. Set when there's a carry out of bit 7 (unsigned overflow) or a borrow in subtraction. + +Flag Updates +~~~~~~~~~~~~ + +Different instructions update flags in different ways: + +**Arithmetic Instructions** (ADD, SUB, ADC, SBC) + Update all flags: C, Z, N, V, S, H + +**Logical Instructions** (AND, OR, EOR) + Update Z, N, S; Clear V; Leave C unchanged + +**Comparison** (CP, CPI) + Update all flags like subtraction, but don't store result + +**Test** (TST) + Update Z, N, S, V; Clear V + +**Increment/Decrement** (INC, DEC) + Update Z, N, V, S; Leave C unchanged + +Using Flags for Branches +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Conditional branch instructions test specific flag conditions: + +.. code-block:: asm + + ; Branch if equal (Z flag set) + breq label + + ; Branch if not equal (Z flag clear) + brne label + + ; Branch if carry set (C flag set) + brcs label + + ; Branch if less than (signed: S flag set) + brlt label + + ; Branch if lower (unsigned: C flag set) + brlo label + +Instruction Execution +--------------------- + +Fetch-Decode-Execute Cycle +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tiny8 uses a simplified instruction execution model: + +1. **Fetch**: Read instruction at address PC +2. **Decode**: Parse instruction mnemonic and operands +3. **Execute**: Perform the operation +4. **Update**: Increment PC, update flags, record traces + +.. code-block:: text + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ FETCH โ”‚ โ† Read instruction at PC + โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” + โ”‚ DECODE โ”‚ โ† Parse mnemonic and operands + โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” + โ”‚ EXECUTE โ”‚ โ† Perform operation + โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” + โ”‚ UPDATE โ”‚ โ† Update PC, flags, traces + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +Instruction Format +~~~~~~~~~~~~~~~~~~ + +Instructions are stored as tuples in memory: + +.. code-block:: python + + (mnemonic, (operand1, operand2, ...)) + + # Examples: + ("ldi", (("reg", 16), 42)) # ldi r16, 42 + ("add", (("reg", 16), ("reg", 17))) # add r16, r17 + ("jmp", ("loop",)) # jmp loop + +Operand Types +~~~~~~~~~~~~~ + +Operands can be: + +* **Register**: ``("reg", N)`` where N is 0-31 +* **Immediate**: Integer value +* **Label**: String referring to a program location +* **Address**: Memory address (integer) + +Step Tracing +~~~~~~~~~~~~ + +The CPU automatically records traces during execution: + +**Register Trace** + Records all register changes as ``(step, register, new_value)`` + +**Memory Trace** + Records all memory writes as ``(step, address, new_value)`` + +**Step Trace** + Records full CPU state snapshots for visualization + +These traces enable the interactive debugger and animation features. + +Performance Characteristics +--------------------------- + +Execution Model +~~~~~~~~~~~~~~~ + +* **Single-cycle execution**: Each instruction completes in one "step" +* **No pipeline**: Instructions execute sequentially +* **No timing accuracy**: Simplified model for education + +.. note:: + Real AVR microcontrollers have variable instruction timing (1-4 cycles) and pipelined execution. Tiny8 abstracts these details for simplicity. + +Limitations +~~~~~~~~~~~ + +* No I/O peripherals (timers, UART, etc.) +* No interrupt handling (I flag exists but not functional) +* Simplified flag semantics +* No program memory vs. data memory separation (Von Neumann architecture) + +Design Philosophy +----------------- + +Tiny8's architecture is designed with these principles: + +1. **Simplicity**: Easy to understand and implement +2. **Educational value**: Teaches fundamental concepts +3. **Inspectability**: Full visibility into CPU state +4. **Extensibility**: Easy to add new instructions +5. **Practicality**: Can run real algorithms and demonstrate CS concepts + +The architecture strikes a balance between realism (AVR-inspired) and pedagogy (simplified execution model), making it suitable for: + +* Computer architecture courses +* Assembly language learning +* Algorithm visualization +* Embedded systems concepts +* Compiler/assembler development diff --git a/docs/assembly_language.rst b/docs/assembly_language.rst new file mode 100644 index 0000000..105bc55 --- /dev/null +++ b/docs/assembly_language.rst @@ -0,0 +1,488 @@ +Assembly Language +================= + +This guide covers the Tiny8 assembly language syntax, including instruction format, operand types, labels, and assembler directives. + +Syntax Overview +--------------- + +Basic Structure +~~~~~~~~~~~~~~~ + +A Tiny8 assembly program consists of: + +* **Instructions**: CPU operations (e.g., ``add``, ``mov``, ``ldi``) +* **Labels**: Named locations in the program +* **Comments**: Documentation prefixed with ``;`` +* **Blank lines**: For readability (ignored by assembler) + +Example Program +~~~~~~~~~~~~~~~ + +.. code-block:: asm + + ; Calculate the sum of 1 to N + ; N is stored in R16, result in R17 + + ldi r16, 10 ; N = 10 + ldi r17, 0 ; Sum = 0 + ldi r18, 1 ; Counter = 1 + + loop: + add r17, r18 ; Sum += Counter + inc r18 ; Counter++ + cp r18, r16 ; Compare Counter with N + brlo loop ; Loop if Counter < N + breq loop ; Loop if Counter == N + + done: + jmp done ; Infinite loop + +Comments +-------- + +Single-Line Comments +~~~~~~~~~~~~~~~~~~~~ + +Comments start with a semicolon (``;``) and extend to the end of the line: + +.. code-block:: asm + + ldi r16, 42 ; Load the answer + ; This is a full-line comment + add r16, r17 ; Add registers + +Comments are stripped during assembly and don't affect the generated program. + +Whitespace +~~~~~~~~~~ + +* Leading and trailing whitespace is ignored +* Multiple spaces between tokens are treated as single space +* Blank lines are allowed and ignored + +Labels +------ + +Defining Labels +~~~~~~~~~~~~~~~ + +Labels mark locations in your program and can be used as jump/branch targets: + +.. code-block:: asm + + start: ; Label on its own line + ldi r16, 0 + + loop: dec r16 ; Label before instruction + brne loop + +Label Rules +~~~~~~~~~~~ + +* Labels must end with a colon (``:``) +* Label names are case-sensitive +* Valid characters: letters, digits, underscore +* Cannot start with a digit +* Cannot be a reserved instruction mnemonic + +**Valid labels:** + +.. code-block:: asm + + start: + loop_1: + CalculateFibonacci: + _private: + +**Invalid labels:** + +.. code-block:: text + + 123start: ; Cannot start with digit + my-label: ; Hyphens not allowed + add: ; Reserved instruction name + +Using Labels +~~~~~~~~~~~~ + +Labels are most commonly used with control flow instructions: + +.. code-block:: asm + + ; Unconditional jump + jmp start + + ; Conditional branches + breq equal_case + brne not_equal + brlo lower_case + + ; Subroutines + call subroutine + + subroutine: + ; ... code ... + ret + +Operand Types +------------- + +Registers +~~~~~~~~~ + +Register operands are specified as ``r`` followed by the register number (0-31): + +.. code-block:: asm + + mov r0, r1 ; R0-R31 are valid + add r16, r17 + ldi r31, 255 + +.. note:: + Some instructions (like ``ldi``) only work with registers R16-R31. + +Immediate Values +~~~~~~~~~~~~~~~~ + +Immediate values are constants embedded in the instruction: + +**Decimal** (default) + +.. code-block:: asm + + ldi r16, 42 ; Decimal 42 + ldi r17, 255 ; Decimal 255 + ldi r18, -1 ; Negative values allowed + +**Hexadecimal** (prefix with ``0x`` or ``$``) + +.. code-block:: asm + + ldi r16, 0xFF ; Hexadecimal FF (255) + ldi r17, $A5 ; Hexadecimal A5 (165) + ldi r18, 0x10 ; Hexadecimal 10 (16) + +**Binary** (prefix with ``0b``) + +.. code-block:: asm + + ldi r16, 0b11111111 ; Binary (255) + ldi r17, 0b10101010 ; Binary (170) + ldi r18, 0b00001111 ; Binary (15) + +**With Immediate Marker** (optional ``#`` prefix) + +.. code-block:: asm + + ldi r16, #42 ; # is optional and ignored + ldi r17, #0xFF + +Memory Addresses +~~~~~~~~~~~~~~~~ + +Memory addresses are used with load and store instructions: + +.. code-block:: asm + + lds r16, 0x0200 ; Load from address 0x0200 + sts 0x0300, r16 ; Store to address 0x0300 + lds r17, 512 ; Decimal addresses also work + +Label References +~~~~~~~~~~~~~~~~ + +Labels can be used as operands for jumps, branches, and calls: + +.. code-block:: asm + + jmp start + breq equal_handler + call calculate_sum + + start: + ; ... + + equal_handler: + ; ... + + calculate_sum: + ; ... + ret + +Instruction Format +------------------ + +General Pattern +~~~~~~~~~~~~~~~ + +Instructions follow this pattern: + +.. code-block:: text + + [label:] mnemonic [operand1[, operand2[, ...]]] + +* **label**: Optional label ending with ``:`` +* **mnemonic**: Instruction name (e.g., ``add``, ``mov``, ``ldi``) +* **operands**: Zero or more operands separated by commas + +Operand Count +~~~~~~~~~~~~~ + +Different instructions take different numbers of operands: + +.. code-block:: asm + + ; Zero operands + nop ; No operation + ret ; Return from subroutine + + ; One operand + inc r16 ; Increment register + dec r17 ; Decrement register + jmp loop ; Jump to label + push r16 ; Push register + + ; Two operands + mov r16, r17 ; Move R17 to R16 + add r16, r17 ; Add R17 to R16 + ldi r16, 42 ; Load immediate into R16 + lds r16, 0x0200 ; Load from memory + +Case Sensitivity +~~~~~~~~~~~~~~~~ + +* **Instructions**: Case-insensitive (``ADD``, ``add``, ``Add`` are all valid) +* **Registers**: Case-insensitive (``r16``, ``R16`` are both valid) +* **Labels**: Case-sensitive (``Loop`` and ``loop`` are different) + +.. code-block:: asm + + ; These are all equivalent + ADD r16, r17 + add r16, r17 + Add R16, R17 + + ; But these labels are different + Loop: + ; ... + jmp loop ; Error! Label "loop" not defined + +Program Structure +----------------- + +Typical Program Layout +~~~~~~~~~~~~~~~~~~~~~~ + +Most Tiny8 programs follow this structure: + +.. code-block:: asm + + ; ============================================ + ; Program: Description + ; Author: Your Name + ; Description: What the program does + ; ============================================ + + ; --- Initialization --- + ldi r16, initial_value + ldi r17, 0 + + ; --- Main Loop --- + main_loop: + ; ... main program logic ... + jmp main_loop + + ; --- Subroutines --- + subroutine1: + ; ... subroutine code ... + ret + + subroutine2: + ; ... subroutine code ... + ret + + ; --- End --- + done: + jmp done ; Infinite loop + +Initialization +~~~~~~~~~~~~~~ + +Initialize registers and memory at the start of your program: + +.. code-block:: asm + + ; Initialize working registers + ldi r16, 0 ; Counter + ldi r17, 1 ; Accumulator + ldi r18, 10 ; Loop limit + + ; Initialize memory if needed + ldi r19, 0xFF + sts 0x0200, r19 ; Store initial value + +Main Loop +~~~~~~~~~ + +Most programs have a main execution loop: + +.. code-block:: asm + + main: + ; Read input + lds r16, input_addr + + ; Process + call process_data + + ; Write output + sts output_addr, r16 + + ; Repeat + jmp main + +Program Termination +~~~~~~~~~~~~~~~~~~~ + +Since Tiny8 doesn't have a "halt" instruction, programs typically end with an infinite loop: + +.. code-block:: asm + + done: + jmp done ; Loop forever + + ; Or explicitly spin + end: + nop + jmp end + +Assembler Behavior +------------------ + +Two-Pass Assembly +~~~~~~~~~~~~~~~~~ + +The assembler makes two passes through your code: + +1. **First pass**: Collect all labels and their addresses +2. **Second pass**: Resolve label references and generate instructions + +This allows forward references: + +.. code-block:: asm + + ; Forward reference (allowed) + jmp forward_label + nop + nop + forward_label: + ret + +Number Parsing +~~~~~~~~~~~~~~ + +The assembler recognizes several number formats: + +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 + + * - Format + - Example + - Value + * - Decimal + - ``42`` + - 42 + * - Negative decimal + - ``-10`` + - -10 (stored as 246 in 8-bit) + * - Hexadecimal (0x) + - ``0xFF`` + - 255 + * - Hexadecimal ($) + - ``$FF`` + - 255 + * - Binary + - ``0b11111111`` + - 255 + +Error Handling +~~~~~~~~~~~~~~ + +The assembler will report errors for: + +* Invalid instruction mnemonics +* Wrong number of operands +* Invalid register numbers (< 0 or > 31) +* Undefined label references +* Invalid number formats + +Best Practices +-------------- + +Code Organization +~~~~~~~~~~~~~~~~~ + +1. **Use meaningful labels**: ``calculate_sum`` not ``label1`` +2. **Comment liberally**: Explain what and why, not just how +3. **Group related code**: Keep subroutines together +4. **Use blank lines**: Separate logical sections + +.. code-block:: asm + + ; Good: Clear structure and documentation + ; Calculate factorial of N + ; Input: R16 = N + ; Output: R17 = N! + factorial: + ldi r17, 1 ; result = 1 + + fact_loop: + mul r17, r16 ; result *= N + dec r16 ; N-- + brne fact_loop ; Continue if N != 0 + ret + +Naming Conventions +~~~~~~~~~~~~~~~~~~ + +* **Labels**: Use ``snake_case`` or ``CamelCase`` consistently +* **Constants**: Use ``UPPER_CASE`` for important constants +* **Temporary values**: Use lower registers (R0-R15) +* **Important data**: Use upper registers (R16-R31) + +Register Allocation +~~~~~~~~~~~~~~~~~~~ + +Plan your register usage: + +.. code-block:: asm + + ; Document register usage at top of program + ; R16: Loop counter + ; R17: Accumulator + ; R18: Temporary storage + ; R19-R20: Function parameters + +Value Ranges +~~~~~~~~~~~~ + +Remember that Tiny8 uses 8-bit values: + +* Unsigned range: 0 to 255 +* Signed range: -128 to +127 +* Overflow wraps around + +.. code-block:: asm + + ldi r16, 255 + inc r16 ; R16 = 0 (wraps around) + + ldi r17, 0 + dec r17 ; R17 = 255 (wraps around) + +Common Patterns +--------------- + +See the :doc:`getting_started` guide for common assembly patterns like loops, conditionals, and memory operations. diff --git a/docs/conf.py b/docs/conf.py index 72d90f8..0fb9e23 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,12 +3,20 @@ # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import os +import sys + +# Add source directory to path for autodoc +sys.path.insert(0, os.path.abspath("../src")) + # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "Tiny8" copyright = "2025, sql-hkr" author = "sql-hkr" +release = "0.1.1" +version = "0.1" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -16,9 +24,11 @@ extensions = [ "sphinx.ext.autodoc", "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.mathjax", - "sphinx.ext.napoleon", + "sphinx.ext.githubpages", ] templates_path = ["_templates"] @@ -26,13 +36,79 @@ language = "en" +# -- Autodoc configuration --------------------------------------------------- +autodoc_default_options = { + "members": True, + "member-order": "bysource", + "special-members": "__init__", + "undoc-members": True, + "exclude-members": "__weakref__", +} + +autodoc_typehints = "description" +autodoc_type_aliases = {} + +# -- Napoleon settings ------------------------------------------------------- +napoleon_google_docstring = True +napoleon_numpy_docstring = True +napoleon_include_init_with_doc = True +napoleon_include_private_with_doc = False +napoleon_include_special_with_doc = True +napoleon_use_admonition_for_examples = False +napoleon_use_admonition_for_notes = False +napoleon_use_admonition_for_references = False +napoleon_use_ivar = False +napoleon_use_param = True +napoleon_use_rtype = True +napoleon_preprocess_types = False +napoleon_type_aliases = None +napoleon_attr_annotations = True + +# -- Intersphinx configuration ----------------------------------------------- +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "matplotlib": ("https://matplotlib.org/stable/", None), +} + # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "shibuya" -html_static_path = ["_static"] +html_static_path = [] # No static files yet + +# Theme options +html_theme_options = { + "github_url": "https://github.com/sql-hkr/tiny8", +} + +html_title = f"{project} {version} documentation" +html_short_title = project +html_favicon = None +html_logo = None # -- Options for todo extension ---------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/extensions/todo.html#configuration -todo_include_todos = True +todo_include_todos = False # Hide TODOs in production + +# -- Options for LaTeX output ------------------------------------------------ +latex_elements = { + "papersize": "letterpaper", + "pointsize": "10pt", +} + +# -- Options for manual page output ------------------------------------------ +man_pages = [("index", "tiny8", "Tiny8 Documentation", [author], 1)] + +# -- Options for Texinfo output ---------------------------------------------- +texinfo_documents = [ + ( + "index", + "Tiny8", + "Tiny8 Documentation", + author, + "Tiny8", + "An educational 8-bit CPU simulator with interactive visualization", + "Miscellaneous", + ), +] diff --git a/docs/examples/array_sum.rst b/docs/examples/array_sum.rst new file mode 100644 index 0000000..6c5640c --- /dev/null +++ b/docs/examples/array_sum.rst @@ -0,0 +1,10 @@ +Array Sum +========= + +Sums elements of an array stored in memory. + +.. literalinclude:: ../../examples/array_sum.asm + :language: asm + :linenos: + +Demonstrates memory access and array processing. diff --git a/docs/examples/bubblesort.rst b/docs/examples/bubblesort.rst index 5469e88..1b8ac72 100644 --- a/docs/examples/bubblesort.rst +++ b/docs/examples/bubblesort.rst @@ -1,109 +1,380 @@ -Bubble sort -============ +Bubble Sort +=========== -This example demonstrates a simple bubble sort algorithm implemented in assembly language for the Tiny8 CPU. The program first fills a section of RAM with pseudo-random bytes, then sorts those bytes using the bubble sort algorithm. Finally, a Python script runs the assembly program and visualizes the sorting process. +This advanced example demonstrates bubble sort: a classic sorting algorithm that sorts an array in memory. -.. image:: ../_static/examples/bubblesort.gif - :alt: Bubble sort +Overview +-------- + +**Difficulty**: Advanced + +**Concepts**: Nested loops, memory operations, array manipulation, pseudo-random number generation + +**Output**: Sorted array at RAM[0x60..0x7F] (32 bytes in ascending order) + +The Program +----------- + +.. literalinclude:: ../../examples/bubblesort.asm + :language: asm + :linenos: + +Algorithm Overview +------------------ + +The program has two main phases: + +1. **Initialization**: Generate 32 pseudo-random values and store them in RAM +2. **Bubble Sort**: Sort the array using the bubble sort algorithm + +Phase 1: Array Initialization +------------------------------ + +Pseudo-Random Number Generator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The program uses a simple linear congruential generator (LCG): + +.. code-block:: text + + seed(n+1) = (seed(n) ร— 75) + 1 + +This generates a sequence of pseudo-random 8-bit values. .. code-block:: asm - :caption: bubblesort.asm - - ; Bubble sort using RAM (addresses 100..131) - 32 elements - ; Purpose: fill RAM[100..131] with pseudo-random bytes and sort them - ; Registers (use R16..R31 for LDI immediates): - ; R16 - base address (start = 100) - ; R17 - index / loop counter for initialization - ; R18 - PRNG state (seed) - ; R19..R24 - temporary registers used in loops and swaps - ; R25 - PRNG multiplier (kept aside to avoid clobber in MUL) - ; - ; The code below is split into two phases: - ; 1) init_loop: generate and store 32 pseudo-random bytes at RAM[100..131] - ; 2) outer/inner loops: perform a simple bubble sort over those 32 bytes - - ; initialize pointers and PRNG - ldi r16, 100 ; base address - ldi r17, 0 ; index = 0 - ldi r18, 123 ; PRNG seed - ldi r25, 75 ; PRNG multiplier (kept in r25 so mul doesn't clobber it) - - init_loop: - ; PRNG step: r2 := lowbyte(r2 * 75), then tweak - mul r18, r25 ; r18 = low byte of (r18 * 75) - inc r18 ; small increment to avoid repeating patterns - ; store generated byte into memory at base + index - st r16, r18 ; RAM[base] = r18 - inc r16 ; advance base pointer - inc r17 ; increment index - ldi r23, 32 - cp r17, r23 - brne init_loop - - ; Bubble sort for 32 elements (perform passes until i == 31) - ldi r18, 0 ; i = 0 (outer loop counter) - outer_loop: - ldi r19, 0 ; j = 0 (inner loop counter) - inner_loop: - ; compute address of element A = base + j - ldi r20, 100 - add r20, r19 - ld r21, r20 ; r21 = A - ; compute address of element B = base + j + 1 - ldi r22, 100 - add r22, r19 - ldi r23, 1 - add r22, r23 - ld r24, r22 ; r24 = B - ; compare A and B (we'll swap if A < B) - cp r21, r24 ; sets carry if r21 < r24 - brcc no_swap - ; swap A and B: store B into A's address, A into B's address - st r20, r24 - st r22, r21 - no_swap: - inc r19 - ldi r23, 31 - cp r19, r23 - breq end_inner - jmp inner_loop - end_inner: - inc r18 - ldi r23, 31 - cp r18, r23 - breq done - jmp outer_loop - - done: - jmp done + ldi r18, 123 ; PRNG seed (starting value) + ldi r25, 75 ; PRNG multiplier + init_loop: + mul r18, r25 ; multiply seed by 75 + inc r18 ; add 1 + st r16, r18 ; store at RAM[base + index] + ; ... -.. code-block:: python - :caption: bubblesort.py +After 32 iterations, RAM[0x60..0x7F] contains pseudo-random values. + +Phase 2: Bubble Sort +--------------------- + +Algorithm Explanation +~~~~~~~~~~~~~~~~~~~~~ + +Bubble sort works by repeatedly stepping through the array, comparing adjacent elements, and swapping them if they're in the wrong order. + +.. code-block:: text + + for i = 0 to n-2: + for j = 0 to n-2: + if array[j] > array[j+1]: + swap(array[j], array[j+1]) + +Outer Loop +~~~~~~~~~~ + +.. code-block:: asm + + ldi r18, 0 ; i = 0 + outer_loop: + ; ... inner loop ... + inc r18 + ldi r23, 31 + cp r18, r23 + breq done + jmp outer_loop - from tiny8 import CPU, Visualizer, assemble_file +Runs 31 times (i = 0 to 30). - prog, labels = assemble_file("examples/bubblesort.asm") - cpu = CPU() - cpu.load_program(prog, labels) - cpu.run(max_cycles=15000) +Inner Loop +~~~~~~~~~~ - print([cpu.read_ram(i) for i in range(100, 132)]) +.. code-block:: asm + + ldi r19, 0 ; j = 0 + inner_loop: + ; load element A = RAM[0x60 + j] + ldi r20, 0x60 + add r20, r19 + ld r21, r20 ; r21 = A + + ; load element B = RAM[0x60 + j + 1] + ldi r22, 0x60 + add r22, r19 + ldi r23, 1 + add r22, r23 + ld r24, r22 ; r24 = B + + ; compare and swap if needed + cp r21, r24 + brcc no_swap + st r20, r24 ; swap + st r22, r21 + + no_swap: + inc r19 + ; ... + +For each j: + +1. Load adjacent elements (j and j+1) +2. Compare them +3. Swap if first > second +4. Advance to next pair + +Register Usage +-------------- + +.. list-table:: Register Allocation + :header-rows: 1 + :widths: 15 85 + + * - Register + - Purpose + * - R16 + - Memory address pointer / base address + * - R17 + - Array index during initialization + * - R18 + - PRNG seed / outer loop counter (i) + * - R19 + - Inner loop counter (j) + * - R20 + - Address of element A + * - R21 + - Value of element A + * - R22 + - Address of element B + * - R23 + - Temporary comparison value + * - R24 + - Value of element B + * - R25 + - PRNG multiplier constant (75) + +Memory Layout +------------- + +.. code-block:: text + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Address โ”‚ Content โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ 0x0060 โ”‚ Random value 1 โ”‚ + โ”‚ 0x0061 โ”‚ Random value 2 โ”‚ + โ”‚ 0x0062 โ”‚ Random value 3 โ”‚ + โ”‚ ... โ”‚ ... โ”‚ + โ”‚ 0x007F โ”‚ Random value 32 โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + After sorting: values in ascending order + +Execution Example +----------------- + +Initial Array (pseudo-random) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - viz = Visualizer(cpu) - base = 100 - viz.animate_combined( - interval=1, - mem_addr_start=base, - mem_addr_end=base + 31, - plot_every=100, - # filename="bubblesort.gif", - # fps=60, - ) +.. code-block:: text + [123, 78, 234, 45, 190, 67, 12, 198, ...] + +After Bubble Sort +~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + [12, 45, 67, 78, 123, 190, 198, 234, ...] + +Bubble sort compares and swaps adjacent elements until the entire array is sorted. + +Running the Example +------------------- + +Interactive Debugger +~~~~~~~~~~~~~~~~~~~~ .. code-block:: bash - :caption: Example Output - [247, 243, 239, 238, 227, 211, 210, 195, 190, 187, 186, 171, 167, 159, 155, 150, 142, 139, 135, 130, 127, 106, 102, 94, 54, 50, 34, 26, 23, 15, 10, 6] \ No newline at end of file + tiny8 examples/bubblesort.asm + +Watch the sorting process step-by-step. Set marks at the beginning of inner_loop to track passes. + +Animation +~~~~~~~~~ + +.. code-block:: bash + + tiny8 examples/bubblesort.asm -m ani -o bubblesort.gif \ + --mem-start 0x60 --mem-end 0x7F + +Creates a visualization showing: + +* Memory heatmap of the array being sorted +* Register changes during comparisons +* Visual pattern of values moving to their correct positions + +Python API +~~~~~~~~~~ + +.. code-block:: python + + from tiny8 import CPU, assemble_file + + cpu = CPU() + cpu.load_program(assemble_file("examples/bubblesort.asm")) + cpu.run(max_steps=50000) # Bubble sort needs many steps! + + # Read sorted array + sorted_array = [cpu.mem.read(0x60 + i) for i in range(32)] + print("Sorted array:", sorted_array) + + # Verify it's sorted + assert sorted_array == sorted(sorted_array) + print("โœ“ Array is correctly sorted!") + +Performance Analysis +-------------------- + +Time Complexity +~~~~~~~~~~~~~~~ + +* **Best case**: O(nยฒ) - 31 ร— 31 = 961 comparisons +* **Worst case**: O(nยฒ) - Same as best (this implementation doesn't optimize) +* **Average case**: O(nยฒ) + +For 32 elements: + +* Outer loop: 31 iterations +* Inner loop: 31 iterations per outer +* Total comparisons: ~961 +* Total swaps: Variable (depends on initial order) + +Instruction Count +~~~~~~~~~~~~~~~~~ + +Each comparison involves approximately: + +* 10 instructions to load addresses and values +* 5 instructions for comparison and potential swap +* 3 instructions for loop control + +Total: ~15,000+ instructions for a complete sort. + +Key Concepts +------------ + +Nested Loops +~~~~~~~~~~~~ + +Two loop counters working together: + +.. code-block:: asm + + outer_loop: + ldi r19, 0 + inner_loop: + ; ... work ... + inc r19 + ; ... inner loop test ... + inc r18 + ; ... outer loop test ... + +Address Calculation +~~~~~~~~~~~~~~~~~~~ + +Computing memory addresses dynamically: + +.. code-block:: asm + + ldi r20, 0x60 ; Base address + add r20, r19 ; Add offset + ld r21, r20 ; Load from computed address + +Conditional Swapping +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: asm + + cp r21, r24 ; Compare values + brcc no_swap ; Skip swap if in order + st r20, r24 ; Perform swap + st r22, r21 + no_swap: + +Exercises +--------- + +1. **Count swaps**: Add a counter to track number of swaps +2. **Optimize**: Add early exit if no swaps occurred in a pass +3. **Different sizes**: Sort 16 or 64 elements instead +4. **Descending order**: Modify to sort in descending order +5. **Different algorithm**: Implement selection sort or insertion sort + +Solutions Hints +~~~~~~~~~~~~~~~ + +**Count swaps**: Add ``inc r26`` in the swap section, initialize R26 to 0. + +**Optimize**: Set a flag when swapping, check it at end of outer loop. + +**Descending order**: Change ``brcc no_swap`` to ``brcs no_swap``. + +Visualization Tips +------------------ + +When viewing the animation: + +* **Memory panel**: Watch values "bubble" to their positions +* **Register panel**: R19 (j) cycles 0-30 repeatedly +* **Register panel**: R18 (i) increments slowly + +Look for: + +* Large values moving right (higher addresses) +* Small values moving left (lower addresses) +* Progressively fewer changes as array becomes sorted + +Comparison with Other Sorts +---------------------------- + +.. list-table:: Sorting Algorithm Comparison + :header-rows: 1 + :widths: 25 25 25 25 + + * - Algorithm + - Time Complexity + - Space + - Stability + * - Bubble Sort + - O(nยฒ) + - O(1) + - Yes + * - Selection Sort + - O(nยฒ) + - O(1) + - No + * - Insertion Sort + - O(nยฒ) + - O(1) + - Yes + * - Quick Sort + - O(n log n) + - O(log n) + - No + +Bubble sort is chosen for this example because it's conceptually simple and demonstrates memory operations clearly. + +Related Examples +---------------- + +* :doc:`linear_search` - Sequential array access +* :doc:`find_max` - Array comparison operations +* :doc:`reverse` - Array manipulation + +Next Steps +---------- + +* Study :doc:`../architecture` for memory model details +* Review :doc:`../instruction_reference` for LD, ST, CP +* Try implementing other sorting algorithms +* Use :doc:`../visualization` to understand algorithm behavior diff --git a/docs/examples/count_bits.rst b/docs/examples/count_bits.rst new file mode 100644 index 0000000..034fb7e --- /dev/null +++ b/docs/examples/count_bits.rst @@ -0,0 +1,10 @@ +Count Bits +========== + +Counts the number of set bits (1s) in a value. + +.. literalinclude:: ../../examples/count_bits.asm + :language: asm + :linenos: + +Demonstrates bit manipulation and counting operations. diff --git a/docs/examples/factorial.rst b/docs/examples/factorial.rst new file mode 100644 index 0000000..68d2620 --- /dev/null +++ b/docs/examples/factorial.rst @@ -0,0 +1,10 @@ +Factorial +========= + +Computes factorial of a number using iterative multiplication. + +.. literalinclude:: ../../examples/factorial.asm + :language: asm + :linenos: + +Calculates 5! = 120 using repeated multiplication and demonstrates loop control. diff --git a/docs/examples/fibonacci.rst b/docs/examples/fibonacci.rst index 2c789c4..b9237f9 100644 --- a/docs/examples/fibonacci.rst +++ b/docs/examples/fibonacci.rst @@ -1,164 +1,243 @@ -Fibonacci -========== +Fibonacci Sequence +================== -This example demonstrates a simple iterative implementation of the Fibonacci sequence in Tiny8 assembly language. The program computes the n-th Fibonacci number, where n is provided in register R17, and returns the result in register R16. +This example calculates the 10th Fibonacci number using an iterative approach. +Overview +-------- + +**Difficulty**: Beginner + +**Concepts**: Loops, register arithmetic, conditional branching + +**Output**: R17 contains F(10) = 55 + +The Program +----------- + +.. literalinclude:: ../../examples/fibonacci.asm + :language: asm + :linenos: + +Algorithm Explanation +--------------------- + +The Fibonacci sequence is defined as: + +* F(0) = 0 +* F(1) = 1 +* F(n) = F(n-1) + F(n-2) for n > 1 + +This program uses an iterative approach with three registers: + +* **R16**: Previous Fibonacci number (F(n-1)) +* **R17**: Current Fibonacci number (F(n)) +* **R18**: Counter (remaining iterations) + +Step-by-Step Walkthrough +------------------------- + +Initialization (Lines 8-10) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: asm - :caption: fibonacci.asm - - ; Simple iterative Fibonacci - ; Purpose: compute fib(n) and leave the result in R16. - ; Registers: - ; R17 - input n (non-negative integer) - ; R16 - 'a' (accumulator / result) - ; R18 - 'b' (next Fibonacci term) - ; R19 - temporary scratch used for moves/subtracts - ; - ; Algorithm (iterative): - ; a = 0 - ; b = 1 - ; if n == 0 -> result = a - ; if n == 1 -> result = b - ; else repeat (n-1) times: (a, b) = (b, a + b) - - start: - ; initialize: a = 0, b = 1 - LDI R16, 0 ; a = 0 - LDI R18, 1 ; b = 1 - - ; quick exit: if n == 0 then result is a (R16 == 0) - CPI R17, 0 - BREQ done - - ; main loop: run until we've advanced n-1 times - main_loop: - ; if n == 1 then the current 'b' is the result - CPI R17, 1 - BREQ from_b - - ; decrement n (n = n - 1) - LDI R19, 1 - SUB R17, R19 - - ; compute a = a + b (new 'sum' temporarily in R16) - ADD R16, R18 - - ; rotate registers: new a = old b, new b = sum (we used R19 as temp) - MOV R19, R16 ; temp = sum - MOV R16, R18 ; a = old b - MOV R18, R19 ; b = sum - - JMP main_loop - - from_b: - ; when n reached 1, result is b - MOV R16, R18 - ; fall through to halt - - done: - ; infinite loop to halt; result in R16 - JMP done -.. code-block:: python - :caption: Running the Fibonacci Example + ldi r16, 0 ; F(0) = 0 + ldi r17, 1 ; F(1) = 1 + ldi r18, 9 ; Counter: 9 more iterations - from tiny8 import CPU, assemble_file +We start with F(0) = 0 and F(1) = 1. Since we want F(10), we need 9 more iterations (F(2) through F(10)). - n = 13 +Main Loop (Lines 12-18) +~~~~~~~~~~~~~~~~~~~~~~~~ - assert n <= 13, "n must be <= 13 to avoid 8-bit overflow" +.. code-block:: asm + + loop: + add r16, r17 ; F(n) = F(n-1) + F(n-2) + mov r19, r16 ; Save result temporarily + mov r16, r17 ; Shift: previous = current + mov r17, r19 ; Shift: current = new result + dec r18 ; Decrement counter + brne loop ; Continue if counter != 0 + +Each iteration: + +1. **Add**: Compute next Fibonacci number (R16 + R17) +2. **Save**: Store result in temporary register R19 +3. **Shift**: Move R17 to R16 (previous โ† current) +4. **Update**: Move R19 to R17 (current โ† new result) +5. **Decrement**: Decrease counter +6. **Branch**: Loop if counter โ‰  0 + +Execution Trace +--------------- + +Here's how the registers evolve: + +.. list-table:: Fibonacci Calculation + :header-rows: 1 + :widths: 10 15 15 15 30 + + * - Iter + - R16 (prev) + - R17 (curr) + - R18 (count) + - Calculation + * - Init + - 0 + - 1 + - 9 + - F(0), F(1) + * - 1 + - 1 + - 1 + - 8 + - F(2) = 0 + 1 = 1 + * - 2 + - 1 + - 2 + - 7 + - F(3) = 1 + 1 = 2 + * - 3 + - 2 + - 3 + - 6 + - F(4) = 1 + 2 = 3 + * - 4 + - 3 + - 5 + - 5 + - F(5) = 2 + 3 = 5 + * - 5 + - 5 + - 8 + - 4 + - F(6) = 3 + 5 = 8 + * - 6 + - 8 + - 13 + - 3 + - F(7) = 5 + 8 = 13 + * - 7 + - 13 + - 21 + - 2 + - F(8) = 8 + 13 = 21 + * - 8 + - 21 + - 34 + - 1 + - F(9) = 13 + 21 = 34 + * - 9 + - 34 + - 55 + - 0 + - F(10) = 21 + 34 = 55 + +Running the Example +------------------- + +Interactive Debugger +~~~~~~~~~~~~~~~~~~~~ - program, labels = assemble_file("examples/fibonacci.asm") - cpu = CPU() - cpu.load_program(program, labels) +.. code-block:: bash - cpu.write_reg(17, n) - cpu.run(max_cycles=1000) + tiny8 examples/fibonacci.asm - print("R16 =", cpu.read_reg(16)) - print("R17 =", cpu.read_reg(17)) - print("PC =", cpu.pc) - print("SP =", cpu.sp) - print("register changes (reg_trace):\n", *[str(reg) + "\n" for reg in cpu.reg_trace]) +Step through with ``j`` and watch registers R16, R17, and R18 evolve. +Animation +~~~~~~~~~ .. code-block:: bash - :caption: Example Output - - R16 = 233 - R17 = 1 - PC = 14 - SP = 2047 - register changes (reg_trace): - (0, 17, 13) - (1, 18, 1) - (6, 19, 1) - (7, 17, 12) - (8, 16, 1) - (16, 17, 11) - (17, 16, 2) - (18, 19, 2) - (19, 16, 1) - (20, 18, 2) - (24, 19, 1) - (25, 17, 10) - (26, 16, 3) - (27, 19, 3) - (28, 16, 2) - (29, 18, 3) - (33, 19, 1) - (34, 17, 9) - (35, 16, 5) - (36, 19, 5) - (37, 16, 3) - (38, 18, 5) - (42, 19, 1) - (43, 17, 8) - (44, 16, 8) - (45, 19, 8) - (46, 16, 5) - (47, 18, 8) - (51, 19, 1) - (52, 17, 7) - (53, 16, 13) - (54, 19, 13) - (55, 16, 8) - (56, 18, 13) - (60, 19, 1) - (61, 17, 6) - (62, 16, 21) - (63, 19, 21) - (64, 16, 13) - (65, 18, 21) - (69, 19, 1) - (70, 17, 5) - (71, 16, 34) - (72, 19, 34) - (73, 16, 21) - (74, 18, 34) - (78, 19, 1) - (79, 17, 4) - (80, 16, 55) - (81, 19, 55) - (82, 16, 34) - (83, 18, 55) - (87, 19, 1) - (88, 17, 3) - (89, 16, 89) - (90, 19, 89) - (91, 16, 55) - (92, 18, 89) - (96, 19, 1) - (97, 17, 2) - (98, 16, 144) - (99, 19, 144) - (100, 16, 89) - (101, 18, 144) - (105, 19, 1) - (106, 17, 1) - (107, 16, 233) - (108, 19, 233) - (109, 16, 144) - (110, 18, 233) - (114, 16, 233) \ No newline at end of file + + tiny8 examples/fibonacci.asm -m ani -o fibonacci.gif + +Visualize the register changes over time. + +Python API +~~~~~~~~~~ + +.. code-block:: python + + from tiny8 import CPU, assemble_file + + cpu = CPU() + cpu.load_program(assemble_file("examples/fibonacci.asm")) + cpu.run(max_steps=100) + + print(f"F(10) = {cpu.read_reg(17)}") # Output: 55 + +Key Concepts +------------ + +Loop Structure +~~~~~~~~~~~~~~ + +The loop pattern is common in assembly: + +.. code-block:: asm + + ldi r18, N ; Initialize counter + loop: + ; ... loop body ... + dec r18 ; Decrement counter + brne loop ; Branch if not equal to zero + +Register Shifting +~~~~~~~~~~~~~~~~~ + +Moving values between registers for state management: + +.. code-block:: asm + + mov r19, r16 ; Temporary save + mov r16, r17 ; Shift values + mov r17, r19 ; Update with new value + +This "register rotation" is a fundamental technique in assembly programming. + +Exercises +--------- + +1. **Modify the counter**: Calculate F(15) instead of F(10) +2. **Different starting values**: Try F(0) = 1, F(1) = 1 (alternative definition) +3. **Store in memory**: Save each Fibonacci number to memory +4. **Detect overflow**: Check when the result exceeds 255 + +Solutions +~~~~~~~~~ + +**F(15) modification**: + +.. code-block:: asm + + ldi r18, 14 ; 14 iterations for F(15) + +Note: F(15) = 610, which exceeds 8-bit range (wraps to 98). + +**Store in memory**: + +.. code-block:: asm + + ldi r20, 0x60 ; Memory base address + loop: + add r16, r17 + sts r20, r17 ; Store current Fibonacci number + inc r20 ; Advance memory pointer + ; ... rest of loop ... + +Related Examples +---------------- + +* :doc:`factorial` - Another iterative calculation +* :doc:`sum_1_to_n` - Similar loop structure +* :doc:`power` - Repeated operation pattern + +Next Steps +---------- + +* Learn about :doc:`../assembly_language` syntax +* Review the :doc:`../instruction_reference` for ADD, MOV, DEC, BRNE +* Try the :doc:`../visualization` tools to see execution flow diff --git a/docs/examples/find_max.rst b/docs/examples/find_max.rst new file mode 100644 index 0000000..02088bf --- /dev/null +++ b/docs/examples/find_max.rst @@ -0,0 +1,10 @@ +Find Maximum +============ + +Finds the maximum value in an array. + +.. literalinclude:: ../../examples/find_max.asm + :language: asm + :linenos: + +Demonstrates comparison operations and conditional logic. diff --git a/docs/examples/gcd.rst b/docs/examples/gcd.rst new file mode 100644 index 0000000..718fc67 --- /dev/null +++ b/docs/examples/gcd.rst @@ -0,0 +1,10 @@ +GCD (Greatest Common Divisor) +============================= + +Computes the GCD of two numbers using Euclidean algorithm. + +.. literalinclude:: ../../examples/gcd.asm + :language: asm + :linenos: + +Demonstrates division, modulo operation, and iterative algorithm. diff --git a/docs/examples/hello_world.rst b/docs/examples/hello_world.rst new file mode 100644 index 0000000..a74bf9e --- /dev/null +++ b/docs/examples/hello_world.rst @@ -0,0 +1,10 @@ +Hello World +=========== + +A minimal Tiny8 program demonstrating basic structure. + +.. literalinclude:: ../../examples/hello_world.asm + :language: asm + :linenos: + +This program demonstrates the simplest possible Tiny8 program structure with initialization and an infinite loop. diff --git a/docs/examples/index.rst b/docs/examples/index.rst index b476c10..bb75b78 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -1,8 +1,160 @@ Examples ======== +This section contains detailed walkthroughs of example programs that demonstrate Tiny8's capabilities and teach assembly programming concepts. + +Overview +-------- + +The examples are organized by complexity and concept: + +**Basic Examples** + Simple programs demonstrating fundamental operations + +**Intermediate Examples** + Programs using loops, conditionals, and memory operations + +**Advanced Examples** + Complex algorithms including sorting and mathematical computations + +All examples are available in the ``examples/`` directory of the Tiny8 repository. + +Running Examples +---------------- + +You can run any example with the interactive debugger: + +.. code-block:: bash + + tiny8 examples/fibonacci.asm + +Or generate an animation: + +.. code-block:: bash + + tiny8 examples/bubblesort.asm -m ani -o bubblesort.gif + +Or use the Python API: + +.. code-block:: python + + from tiny8 import CPU, assemble_file + + cpu = CPU() + cpu.load_program(assemble_file("examples/fibonacci.asm")) + cpu.run(max_steps=1000) + +Example Programs +---------------- + +Basic Examples +~~~~~~~~~~~~~~ + +.. toctree:: + :maxdepth: 1 + + hello_world + fibonacci + factorial + +Intermediate Examples +~~~~~~~~~~~~~~~~~~~~~~ + +.. toctree:: + :maxdepth: 1 + + sum_1_to_n + array_sum + find_max + linear_search + memory_copy + +Advanced Examples +~~~~~~~~~~~~~~~~~ + .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + + bubblesort + gcd + power + multiply_by_shift + count_bits + is_prime + reverse + +Quick Reference +--------------- + +.. list-table:: Example Summary + :header-rows: 1 + :widths: 20 40 40 + + * - Program + - Concept + - Key Instructions + * - Hello World + - Basic structure + - LDI, JMP + * - Fibonacci + - Loops, arithmetic + - ADD, MOV, DEC, BRNE + * - Factorial + - Multiplication + - MUL, DEC, CPI, BRGE + * - Sum 1 to N + - Accumulation + - ADD, INC, CP, BRLO + * - Array Sum + - Memory access + - LDS, ADD, INC + * - Find Max + - Comparison + - CP, BRLO, MOV + * - Linear Search + - Sequential search + - LDS, CP, BREQ + * - Memory Copy + - Data transfer + - LDS, STS, INC + * - Bubble Sort + - Sorting algorithm + - LD, ST, CP, BRCC + * - GCD + - Euclidean algorithm + - DIV, MUL, SUB, MOV + * - Power + - Repeated multiplication + - MUL, DEC, BRNE + * - Multiply by Shift + - Bit manipulation + - LSL, LSR, ADD + * - Count Bits + - Bit operations + - LSR, ANDI + * - Is Prime + - Number theory + - DIV, MUL, CP + * - Reverse Array + - Array manipulation + - LDS, STS, SWAP + +Learning Path +------------- + +Recommended order for learning: + +1. **Start with basics**: hello_world, fibonacci, factorial +2. **Learn memory**: array_sum, memory_copy, find_max +3. **Master control flow**: linear_search, gcd +4. **Tackle algorithms**: bubblesort, power, is_prime +5. **Explore bit operations**: multiply_by_shift, count_bits + +Next Steps +---------- - fibonacci - bubblesort \ No newline at end of file +* Read the detailed :doc:`fibonacci` walkthrough +* Study the :doc:`bubblesort` algorithm implementation +* Review the :doc:`../architecture` for CPU details +* Practice with :doc:`../visualization` tools +* Explore the :doc:`../api/modules` for Python integration diff --git a/docs/examples/is_prime.rst b/docs/examples/is_prime.rst new file mode 100644 index 0000000..cb8576d --- /dev/null +++ b/docs/examples/is_prime.rst @@ -0,0 +1,10 @@ +Is Prime +======== + +Tests whether a number is prime. + +.. literalinclude:: ../../examples/is_prime.asm + :language: asm + :linenos: + +Demonstrates trial division and number theory concepts. diff --git a/docs/examples/linear_search.rst b/docs/examples/linear_search.rst new file mode 100644 index 0000000..a1a3651 --- /dev/null +++ b/docs/examples/linear_search.rst @@ -0,0 +1,10 @@ +Linear Search +============= + +Searches for a value in an array. + +.. literalinclude:: ../../examples/linear_search.asm + :language: asm + :linenos: + +Demonstrates sequential search and early loop exit. diff --git a/docs/examples/memory_copy.rst b/docs/examples/memory_copy.rst new file mode 100644 index 0000000..bcdffde --- /dev/null +++ b/docs/examples/memory_copy.rst @@ -0,0 +1,10 @@ +Memory Copy +=========== + +Copies data from one memory location to another. + +.. literalinclude:: ../../examples/memory_copy.asm + :language: asm + :linenos: + +Demonstrates memory-to-memory data transfer. diff --git a/docs/examples/multiply_by_shift.rst b/docs/examples/multiply_by_shift.rst new file mode 100644 index 0000000..e03427a --- /dev/null +++ b/docs/examples/multiply_by_shift.rst @@ -0,0 +1,10 @@ +Multiply by Shift +================= + +Multiplies numbers using bit shifting instead of MUL instruction. + +.. literalinclude:: ../../examples/multiply_by_shift.asm + :language: asm + :linenos: + +Demonstrates bit manipulation and efficient multiplication. diff --git a/docs/examples/power.rst b/docs/examples/power.rst new file mode 100644 index 0000000..587f3da --- /dev/null +++ b/docs/examples/power.rst @@ -0,0 +1,10 @@ +Power Function +============== + +Computes x^n using repeated multiplication. + +.. literalinclude:: ../../examples/power.asm + :language: asm + :linenos: + +Demonstrates exponentiation through iteration. diff --git a/docs/examples/reverse.rst b/docs/examples/reverse.rst new file mode 100644 index 0000000..041d7c8 --- /dev/null +++ b/docs/examples/reverse.rst @@ -0,0 +1,10 @@ +Reverse Array +============= + +Reverses an array in place. + +.. literalinclude:: ../../examples/reverse.asm + :language: asm + :linenos: + +Demonstrates two-pointer technique and array manipulation. diff --git a/docs/examples/sum_1_to_n.rst b/docs/examples/sum_1_to_n.rst new file mode 100644 index 0000000..cf5f851 --- /dev/null +++ b/docs/examples/sum_1_to_n.rst @@ -0,0 +1,10 @@ +Sum 1 to N +========== + +Calculates the sum of integers from 1 to N. + +.. literalinclude:: ../../examples/sum_1_to_n.asm + :language: asm + :linenos: + +Demonstrates accumulation pattern and loop control. diff --git a/docs/getting_started.rst b/docs/getting_started.rst new file mode 100644 index 0000000..d124b1b --- /dev/null +++ b/docs/getting_started.rst @@ -0,0 +1,272 @@ +Getting Started +=============== + +This guide will help you get started with Tiny8, from installation to running your first programs. + +Installation +------------ + +Requirements +~~~~~~~~~~~~ + +Tiny8 requires Python 3.11 or later. Check your Python version: + +.. code-block:: bash + + python --version + +Installing Tiny8 +~~~~~~~~~~~~~~~~ + +Install Tiny8 using pip: + +.. code-block:: bash + + pip install tiny8 + +This will install the ``tiny8`` command-line tool and make the Python API available for import. + +Verifying Installation +~~~~~~~~~~~~~~~~~~~~~~ + +Verify that Tiny8 is installed correctly: + +.. code-block:: bash + + tiny8 --version + +You should see the version number printed to the terminal. + +Your First Program +------------------ + +Let's write a simple program that adds two numbers. + +Create the Assembly File +~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a new file called ``add.asm`` with the following content: + +.. code-block:: asm + + ; Simple addition program + ; Adds 5 + 3 and stores the result in R16 + + ldi r16, 5 ; Load 5 into R16 + ldi r17, 3 ; Load 3 into R17 + add r16, r17 ; Add R17 to R16 + + done: + jmp done ; Infinite loop + +Understanding the Code +~~~~~~~~~~~~~~~~~~~~~~ + +Let's break down what each line does: + +* ``ldi r16, 5`` - **L** oa **d** **I** mmediate: Loads the value 5 directly into register 16 +* ``ldi r17, 3`` - Loads the value 3 into register 17 +* ``add r16, r17`` - Adds the contents of R17 to R16, storing the result in R16 +* ``jmp done`` - Jumps to the label ``done``, creating an infinite loop + +.. note:: + Lines starting with ``;`` are comments and are ignored by the assembler. + +Running with the Interactive Debugger +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Run your program using the Tiny8 interactive debugger: + +.. code-block:: bash + + tiny8 add.asm + +You'll see a terminal-based interface showing: + +* **Source code** - Your assembly program with the current instruction highlighted +* **Registers** - The 32 general-purpose registers (R0-R31) +* **Status flags** - The SREG flags (I, T, H, S, V, N, Z, C) +* **Memory** - RAM contents +* **Control information** - Program counter (PC), stack pointer (SP), and step count + +Debugger Controls +~~~~~~~~~~~~~~~~~ + +Use these keyboard shortcuts to control execution: + +**Navigation:** + +* ``l`` or ``โ†’`` - Step forward one instruction +* ``h`` or ``โ†`` - Step backward one instruction +* ``w`` - Jump forward 10 steps +* ``b`` - Jump backward 10 steps +* ``0`` - Go to first step +* ``$`` - Go to last step + +**Playback:** + +* ``Space`` - Toggle play/pause +* ``[`` - Slower playback +* ``]`` - Faster playback + +**Display:** + +* ``r`` - Toggle showing all registers +* ``M`` - Toggle showing all memory +* ``=`` - Show detailed step information +* ``j`` - Scroll memory view down +* ``k`` - Scroll memory view up + +**Marks:** + +* ``m`` + letter - Set a mark at current position +* ``'`` + letter - Jump to a saved mark + +**Search/Commands:** + +* ``/`` - Show help screen +* ``:`` - Enter command mode (for advanced navigation and search) + +**Exit:** + +* ``q`` or ``Esc`` - Quit + +Try stepping through your program with ``l`` and watch how the registers change! + +Running from Python +------------------- + +You can also run Tiny8 programs from Python code. + +Basic Execution +~~~~~~~~~~~~~~~ + +.. code-block:: python + + from tiny8 import CPU, assemble_file + + # Assemble the program + asm = assemble_file("add.asm") + + # Create a CPU instance + cpu = CPU() + + # Load the program + cpu.load_program(asm) + + # Run the program (with a safety limit) + cpu.run(max_steps=100) + + # Read the result + print(f"R16 = {cpu.read_reg(16)}") # Should print 8 + +Step-by-Step Execution +~~~~~~~~~~~~~~~~~~~~~~~ + +For more control, you can step through the program manually: + +.. code-block:: python + + from tiny8 import CPU, assemble + + # Parse assembly code directly + code = """ + ldi r16, 10 + ldi r17, 5 + sub r16, r17 + """ + asm = assemble(code) + + # Set up and run + cpu = CPU() + cpu.load_program(asm) + + # Step through manually + while cpu.pc < len(cpu.program): + print(f"Step {cpu.step_count}: PC={cpu.pc}") + cpu.step() + if cpu.step_count > 10: + break + + print(f"Final result: R16 = {cpu.read_reg(16)}") + +Inspecting CPU State +~~~~~~~~~~~~~~~~~~~~ + +You can inspect various aspects of the CPU state: + +.. code-block:: python + + # Read registers + value = cpu.read_reg(16) + + # Read memory + mem_value = cpu.mem.read(0x0100) + + # Check flags + zero_flag = cpu.get_flag(1) # Z flag + carry_flag = cpu.get_flag(0) # C flag + + # Get trace information + reg_changes = cpu.reg_trace + mem_changes = cpu.mem_trace + +Common Assembly Patterns +------------------------- + +Loading Immediate Values +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: asm + + ldi r16, 42 ; Load decimal + ldi r17, 0xFF ; Load hexadecimal + ldi r18, 0b1010 ; Load binary + +Working with Memory +~~~~~~~~~~~~~~~~~~~ + +.. code-block:: asm + + ; Store register to memory + ldi r16, 100 + sts 0x0200, r16 ; Store R16 at address 0x0200 + + ; Load from memory to register + lds r17, 0x0200 ; Load from address 0x0200 into R17 + +Loops +~~~~~ + +.. code-block:: asm + + ; Count down from 10 to 0 + ldi r16, 10 + + loop: + dec r16 ; Decrement R16 + brne loop ; Branch if not equal to zero + + ; Continue execution here + +Conditional Branching +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: asm + + ; Compare two values + ldi r16, 5 + ldi r17, 10 + cp r16, r17 ; Compare R16 with R17 + brlo less_than ; Branch if lower (unsigned) + + ; R16 >= R17 + ldi r18, 1 + jmp done + + less_than: + ; R16 < R17 + ldi r18, 0 + + done: + jmp done diff --git a/docs/index.rst b/docs/index.rst index f845be7..3bc76f1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,151 +1,155 @@ -Tiny8 documentation -===================== +.. Tiny8 documentation master file -.. image:: https://img.shields.io/pypi/v/tiny8 - :target: +Tiny8 Documentation +=================== -.. image:: https://img.shields.io/github/license/sql-hkr/tiny8 - :target: +**An educational 8-bit CPU simulator with interactive visualization** -.. image:: https://img.shields.io/pypi/pyversions/tiny8 - :target: +Tiny8 is a lightweight and educational toolkit for exploring the fundamentals of computer architecture through hands-on assembly programming and real-time visualization. Designed for learning and experimentation, it features an AVR-inspired 8-bit CPU with 32 registers, a rich instruction set, and powerful debugging tools โ€” all with zero heavy dependencies. -.. image:: https://img.shields.io/github/actions/workflow/status/sql-hkr/tiny8/ci.yml?label=CI - :target: +.. image:: https://github.com/user-attachments/assets/6d4f07ba-21b3-483f-a5d4-7603334c40f4 + :alt: Animated bubble sort visualization + :align: center + :width: 600px -Tiny8 is a lightweight toolkit that allows you to explore how computers work at their core through small-scale memory models, handcrafted assembly, and lightweight in-memory data structures. -Designed for rapid experimentation, Tiny8 embraces minimalism with zero unnecessary dependencies, a clean design, and intuitive visualization tools that make learning, debugging, and tinkering enjoyable. +.. centered:: *Real-time visualization of a bubble sort algorithm executing on Tiny8* -.. image:: _static/examples/bubblesort.gif - :alt: Bubble sort +Features +-------- -โญ๏ธ NEW FEATURE! +๐ŸŽฏ Interactive Terminal Debugger +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. image:: https://github.com/user-attachments/assets/cd5a0ae0-8aff-41af-81e0-4ff9c426f617 - :alt: CLI visualizer +.. image:: https://github.com/user-attachments/assets/5317ebcd-53d5-4966-84be-be94b7830899 + :alt: CLI visualizer screenshot + :align: center :width: 600px -Installation ------------- - -Tiny8 supports Python 3.11 and newer. It has no heavy external dependencies and is suitable for inclusion in virtual environments. -Follow the steps below to prepare your environment and install from source or PyPI. +* **Vim-style navigation**: Step through execution with intuitive keyboard controls +* **Change highlighting**: See exactly what changed at each step (registers, flags, memory) +* **Advanced search**: Find instructions, track register/memory changes, locate PC addresses +* **Marks and bookmarks**: Set and jump to important execution points +* **Vertical scrolling**: Handle programs with large memory footprints -Prerequisites +๐ŸŽฌ Graphical Animation +~~~~~~~~~~~~~~~~~~~~~~~ -- Python 3.11+ -- Git (for installing from the repository) -- Recommended: create and use a virtual environment +* Generate high-quality GIF/MP4 videos of program execution +* Visualize register evolution, memory access patterns, and flag changes +* Perfect for presentations, documentation, and learning materials -From source (development) +๐Ÿ—๏ธ Complete 8-bit Architecture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: bash - - git clone https://github.com/sql-hkr/tiny8.git - cd tiny8 - uv venv - source .venv/bin/activate - uv sync +* **32 general-purpose registers** (R0-R31) +* **8-bit ALU** with arithmetic, logical, and bit manipulation operations +* **Status register (SREG)** with 8 condition flags +* **2KB address space** for unified memory and I/O +* **Stack operations** with dedicated stack pointer +* **AVR-inspired instruction set** with 60+ instructions -.. tip:: +๐Ÿ“š Educational Focus +~~~~~~~~~~~~~~~~~~~~ - `uv `_ is an extremely fast Python package and project manager, written in Rust. To install it, run: +* Clean, readable Python implementation +* Comprehensive examples (Fibonacci, bubble sort, factorial, and more) +* Step-by-step execution traces for debugging +* Full API documentation and instruction set reference - .. code-block:: bash +Quick Start +----------- - # On macOS and Linux. - curl -LsSf https://astral.sh/uv/install.sh | sh +Installation +~~~~~~~~~~~~ - # On Windows. - powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +Install Tiny8 using pip: -This flow sets up a development virtual environment, installs development requirements, and prepares the project for local editing and testing. +.. code-block:: bash -From PyPI (stable) + pip install tiny8 -.. code-block:: bash +Your First Program +~~~~~~~~~~~~~~~~~~ - uv add tiny8 +Create a file called ``fibonacci.asm``: -CLI Visualizer --------------- +.. code-block:: asm -Tiny8 includes a lightweight terminal-based visualizer that lets you step through a program's execution trace in your terminal. It shows the status register (SREG), the 32 general-purpose registers, and a compact view of a configurable memory range for each step. + ; Fibonacci Sequence Calculator + ; Calculates the 10th Fibonacci number (F(10) = 55) + + ldi r16, 0 ; F(0) = 0 + ldi r17, 1 ; F(1) = 1 + ldi r18, 9 ; Counter: 9 more iterations + + loop: + add r16, r17 ; F(n) = F(n-1) + F(n-2) + mov r19, r16 ; Save result temporarily + mov r16, r17 ; Shift: previous = current + mov r17, r19 ; Shift: current = new result + dec r18 ; Decrement counter + brne loop ; Continue if counter != 0 + + done: + jmp done ; Infinite loop at end -Key points -~~~~~~~~~~ +Run it with the interactive debugger: -- The CLI visualizer expects the CPU to have a populated ``step_trace`` (run the CPU first with ``cpu.run(...)``). -- Controls are keyboard-driven (play/pause, step forward/back, jump, quit) and work in most POSIX terminals that support curses. -- For higher-fidelity animations (GIFs) and interactive matplotlib views, use the ``Visualizer`` class which requires ``matplotlib``. +.. code-block:: bash -Interactive controls -~~~~~~~~~~~~~~~~~~~~ + tiny8 fibonacci.asm -.. code-block:: text +Or generate an animation: - Space - toggle play/pause - l or > - next step - h or < - previous step - w - jump forward 10 steps - b - jump back 10 steps - 0 - jump to first step - $ - jump to last step - q or ESC - quit +.. code-block:: bash -Programmatic usage ------------------- + tiny8 fibonacci.asm -m ani -o fibonacci.gif -You can invoke the terminal visualizer directly from Python after running the CPU: +Using the Python API +~~~~~~~~~~~~~~~~~~~~ .. code-block:: python from tiny8 import CPU, assemble_file - from tiny8 import run_cli - - prog, labels = assemble_file("examples/bubblesort.asm") + + # Assemble and load program + asm = assemble_file("fibonacci.asm") cpu = CPU() - cpu.load_program(prog, labels) - cpu.run(max_cycles=15000) + cpu.load_program(asm) + + # Run the program + cpu.run(max_steps=1000) + + # Check the result + print(f"Result: R17 = {cpu.read_reg(17)}") # Final Fibonacci number - # Run the curses-based CLI visualizer - run_cli(cpu, mem_addr_start=100, mem_addr_end=131) - -Tiny8 provides a ``tiny8`` console script (see ``pyproject.toml``). You can run the CLI or the animation mode directly: - -.. code-block:: bash - - # Run the curses-based CLI visualizer for an assembly file - uv run tiny8 examples/bubblesort.asm # --mode cli --mem-start 100 --mem-end 131 - - # Produce an animated GIF using matplotlib (requires matplotlib) - uv run tiny8 examples/bubblesort.asm --mode ani -o bubblesort.gif --mem-start 100 --mem-end 131 --plot-every 100 --fps 60 - -.. important:: - - Tiny8 uses Python's built-in curses module (Unix-like systems). On Windows, use an appropriate terminal that supports curses or run via WSL. - -Examples --------- +Documentation Contents +---------------------- .. toctree:: :maxdepth: 2 + :caption: User Guide + + getting_started + architecture + assembly_language + visualization +.. toctree:: + :maxdepth: 2 + :caption: Examples + examples/index -API Reference ---------------- - -The API section documents the public modules, classes, functions, and configuration options. -It includes usage notes, parameter descriptions, and return value details so you can use the library reliably in production code. - .. toctree:: :maxdepth: 2 + :caption: API Reference + + api/modules - api/tiny8 - -License -------- +Indices and Tables +================== -Tiny8 is licensed under the MIT License. See `LICENSE `_ for details. -Contributions, bug reports, and pull requests are welcome; please follow the repository's CONTRIBUTING guidelines. +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/visualization.rst b/docs/visualization.rst new file mode 100644 index 0000000..5e98858 --- /dev/null +++ b/docs/visualization.rst @@ -0,0 +1,438 @@ +Visualization +============= + +Tiny8 provides two powerful visualization tools to help you understand program execution: an **interactive terminal debugger** and a **graphical animation system** for generating videos. + +Terminal Debugger +----------------- + +The interactive terminal debugger provides a comprehensive, real-time view of your program execution with Vim-style keyboard controls. + +Launching the Debugger +~~~~~~~~~~~~~~~~~~~~~~ + +Run any assembly program with the ``tiny8`` command: + +.. code-block:: bash + + tiny8 program.asm + +This opens the terminal-based visualizer showing your program state. + +Interface Overview +~~~~~~~~~~~~~~~~~~ + +The debugger interface is divided into several sections: + +.. code-block:: text + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ SOURCE CODE (with PC) โ”‚ + โ”‚ Shows your assembly with current line โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ REGISTERS โ”‚ + โ”‚ R0-R31 with changed values highlighted โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ STATUS FLAGS (SREG) โ”‚ + โ”‚ I T H S V N Z C - visual indicators โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ MEMORY โ”‚ + โ”‚ RAM contents with addresses and values โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ CONTROL INFO โ”‚ + โ”‚ PC, SP, Step count, Help hint โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +Keyboard Controls +~~~~~~~~~~~~~~~~~ + +Navigation +^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Key + - Action + * - ``l`` or ``โ†’`` + - Step forward one instruction + * - ``h`` or ``โ†`` + - Step backward one instruction + * - ``0`` + - Go to first step + * - ``$`` + - Go to last step + * - ``w`` + - Jump forward 10 steps + * - ``b`` + - Jump backward 10 steps + * - ``Space`` + - Toggle play/pause mode + * - ``[`` / ``]`` + - Slower/faster playback speed + +Auto-Play Mode +^^^^^^^^^^^^^^ + +Press ``Space`` to automatically advance through execution: + +* Program steps forward continuously +* Use ``[`` and ``]`` to adjust playback speed (slower/faster) +* Press ``Space`` again to pause +* Press ``l``/``h`` to take manual control + +Commands and Search +^^^^^^^^^^^^^^^^^^^ + +Press ``:`` to enter command mode for advanced navigation: + +**Command Examples:** + +* ``123`` - Jump to step 123 +* ``+50`` - Jump forward 50 steps +* ``-20`` - Jump backward 20 steps +* ``/add`` - Search forward for "add" instruction +* ``?ldi`` - Search backward for "ldi" instruction +* ``@100`` - Jump to PC address 100 +* ``r10`` - Find next change to register R10 +* ``r10=42`` - Find where R10 equals 42 +* ``m100`` - Find next change to memory[100] +* ``m100=0xFF`` - Find where memory[100] equals 0xFF +* ``fZ`` - Find next change to flag Z +* ``fC=1`` - Find where flag C equals 1 +* ``h`` or ``help`` - Show command help + +Marks and Bookmarks +^^^^^^^^^^^^^^^^^^^ + +Set marks to quickly jump to important execution points: + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Key + - Action + * - ``m`` + letter + - Set mark at current step (e.g., ``ma``, ``mb``) + * - ``'`` + letter + - Jump to mark (e.g., ``'a``, ``'b``) + +Example workflow: + +1. Step to an interesting point: ``ma`` (set mark 'a') +2. Explore elsewhere in execution +3. Return instantly: ``'a`` (jump to mark 'a') + +View Options +^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Key + - Action + * - ``r`` + - Toggle between changed/all registers view + * - ``M`` + - Toggle between non-zero/all memory view + * - ``j`` + - Scroll memory view down + * - ``k`` + - Scroll memory view up + * - ``=`` + - Show detailed step information + +Other Controls +^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Key + - Action + * - ``/`` + - Show help screen + * - ``q`` or ``Esc`` + - Quit debugger + +Visual Indicators +~~~~~~~~~~~~~~~~~ + +Change Highlighting +^^^^^^^^^^^^^^^^^^^ + +The debugger highlights changes at each step: + +* **Registers**: Changed values appear in a different color +* **Flags**: Set flags are highlighted +* **Memory**: Modified memory cells are marked +* **PC indicator**: Shows current instruction with ``โ–บ`` or color + +Understanding the Display +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Registers Display** + +.. code-block:: text + + R0: 000 R1: 000 R2: 000 R3: 000 + R16: 042 R17: 010 R18: 005 R19: 000 + ^^^ ^^^ (highlighted if changed) + +**Status Flags (SREG)** + +.. code-block:: text + + SREG: [I:0] [T:0] [H:0] [S:0] [V:0] [N:0] [Z:1] [C:0] + ^^^ + (set flags highlighted) + +**Memory Display** + +.. code-block:: text + + 0x0200: 42 โ† Changed this step + 0x0201: 00 + 0x0202: FF โ† Changed this step + +Tips and Tricks +~~~~~~~~~~~~~~~ + +Debugging Workflow +^^^^^^^^^^^^^^^^^^ + +1. **Set marks at key points**: Before loops, after calculations +2. **Use search**: Find where registers or memory addresses are accessed +3. **Toggle views**: Show only changed registers when debugging large programs +4. **Auto-play**: Watch algorithm execution in real-time + +Finding Issues +^^^^^^^^^^^^^^ + +* **Infinite loops**: Watch step counter; if PC stops changing, you're stuck +* **Wrong results**: Step through and watch registers; mark where values diverge +* **Memory issues**: Search for memory addresses; check reads/writes +* **Flag problems**: Watch SREG; verify flags after comparisons + +Graphical Animation +------------------- + +Generate high-quality GIF or MP4 videos showing program execution with register and memory evolution. + +Basic Animation +~~~~~~~~~~~~~~~ + +Create an animation from your program: + +.. code-block:: bash + + tiny8 program.asm -m ani -o output.gif + +This generates ``output.gif`` showing: + +* SREG flag evolution over time +* All 32 registers as a heatmap +* Memory contents in a specified range + +Advanced Options +~~~~~~~~~~~~~~~~ + +Command-Line Arguments +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + tiny8 program.asm -m ani \ + --output animation.gif \ + --mem-start 0x0200 \ + --mem-end 0x02FF \ + --interval 200 \ + --fps 30 + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Option + - Description + * - ``-m ani`` + - Enable animation mode + * - ``-o FILE`` + - Output filename (.gif or .mp4) + * - ``--mem-start ADDR`` + - Start address for memory visualization + * - ``--mem-end ADDR`` + - End address for memory visualization + * - ``--interval MS`` + - Milliseconds per frame + * - ``--fps N`` + - Frames per second (for video) + +Animation Layout +~~~~~~~~~~~~~~~~ + +The generated animation has three panels: + +**Top Panel: SREG Flags** + 8 rows showing each flag (I, T, H, S, V, N, Z, C) over time + * Bright = flag set (1) + * Dark = flag clear (0) + +**Middle Panel: Registers** + 32 rows (R0-R31) showing register values over time + * Color intensity = value (0-255) + * Bright = high values + * Dark = low values + +**Bottom Panel: Memory** + Selected memory range showing contents over time + * Same color scheme as registers + * Track data structure evolution + +Reading the Animation +~~~~~~~~~~~~~~~~~~~~~ + +**Time Axis (Horizontal)** + Each column represents one execution step + * Left side: Program start + * Right side: Program end + * Watch values change across time + +**Value Axis (Vertical)** + Each row is a register or memory location + * Track individual items vertically + * See patterns and data flow + +Example Use Cases +~~~~~~~~~~~~~~~~~ + +Visualizing Algorithms +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + # Visualize bubble sort + tiny8 examples/bubblesort.asm -m ani -o bubblesort.gif + +Watch the sorting algorithm: + +* Registers holding array indices +* Memory showing array elements being swapped +* Flags changing during comparisons + +Debugging Data Flow +^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + # Track Fibonacci sequence generation + tiny8 examples/fibonacci.asm -m ani -o fib.gif + +Observe: + +* R16 and R17 alternating as Fibonacci numbers grow +* Loop counter (R18) decrementing +* Flag changes at each iteration + +Presentations and Education +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Animations are perfect for: + +* Teaching computer architecture concepts +* Explaining algorithms visually +* Documenting program behavior +* Creating engaging educational content + +Python API for Visualization +----------------------------- + +You can also create visualizations programmatically using the Python API. + +Using the Visualizer Class +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from tiny8 import CPU, assemble, Visualizer + + # Create and run program + code = """ + ldi r16, 0 + ldi r17, 10 + loop: + add r16, r17 + dec r17 + brne loop + """ + + asm = assemble(code) + cpu = CPU() + cpu.load_program(asm) + cpu.run(max_steps=100) + + # Create visualization + viz = Visualizer(cpu) + viz.animate_execution( + mem_addr_start=0x0200, + mem_addr_end=0x02FF, + filename="my_animation.gif", + interval=200, # ms between frames + fps=30, + cmap="viridis" # matplotlib colormap + ) + +Customization Options +~~~~~~~~~~~~~~~~~~~~~ + +The ``animate_execution`` method accepts several parameters: + +.. code-block:: python + + viz.animate_execution( + mem_addr_start=0x60, # Start of memory range + mem_addr_end=0x7F, # End of memory range + filename="output.gif", # Output file + interval=200, # Frame interval (ms) + fps=30, # Frames per second + fontsize=9, # Label font size + cmap="inferno", # Color map name + plot_every=1 # Downsample: plot every N steps + ) + +Available colormaps: ``viridis``, ``plasma``, ``inferno``, ``magma``, ``cividis``, ``cool``, ``hot``, and many more from Matplotlib. + +Visualization Best Practices +----------------------------- + +For Terminal Debugging +~~~~~~~~~~~~~~~~~~~~~~ + +1. **Start simple**: Run through once to understand program flow +2. **Mark key points**: Set marks at loop starts, branches, important calculations +3. **Use search**: Find where specific registers/memory are modified +4. **Toggle views**: Hide unchanged registers for clarity in large programs +5. **Auto-play for overview**: Watch execution flow at high level + +For Animations +~~~~~~~~~~~~~~ + +1. **Choose memory range carefully**: Show only relevant data +2. **Adjust frame rate**: Slower for detailed analysis, faster for overview +3. **Pick good colormaps**: High contrast for presentations, perceptually uniform for analysis +4. **Downsample long programs**: Use ``plot_every`` to skip steps +5. **Combine with comments**: Explain what viewers should watch for + +Performance Tips +~~~~~~~~~~~~~~~~ + +For very long-running programs: + +* Use ``max_steps`` parameter to limit execution +* Enable ``plot_every`` downsampling for animations +* Focus memory range on active data regions +* Consider using the CLI debugger instead of generating full animation diff --git a/examples/array_sum.asm b/examples/array_sum.asm new file mode 100644 index 0000000..018eda5 --- /dev/null +++ b/examples/array_sum.asm @@ -0,0 +1,56 @@ +; Array sum: compute sum of array values [5, 10, 15, 20, 25, 30, 35, 40] +; Demonstrates array initialization and iteration +; Registers: R16 = sum, R17 = address, R18 = count, R19 = temp, R20-R21 = init +; Output: R16 = 180 (sum of all array elements) + +start: + ; initialize array at RAM[0x60..0x67] with values [5, 10, 15, 20, 25, 30, 35, 40] + ldi r20, 0x60 + ldi r21, 5 + st r20, r21 ; RAM[0x60] = 5 + + ldi r20, 0x61 + ldi r21, 10 + st r20, r21 ; RAM[0x61] = 10 + + ldi r20, 0x62 + ldi r21, 15 + st r20, r21 ; RAM[0x62] = 15 + + ldi r20, 0x63 + ldi r21, 20 + st r20, r21 ; RAM[0x63] = 20 + + ldi r20, 0x64 + ldi r21, 25 + st r20, r21 ; RAM[0x64] = 25 + + ldi r20, 0x65 + ldi r21, 30 + st r20, r21 ; RAM[0x65] = 30 + + ldi r20, 0x66 + ldi r21, 35 + st r20, r21 ; RAM[0x66] = 35 + + ldi r20, 0x67 + ldi r21, 40 + st r20, r21 ; RAM[0x67] = 40 + + ; compute sum by iterating through array + ldi r16, 0 ; sum = 0 (accumulator) + ldi r17, 0x60 ; address = 0x60 (start of array) + ldi r18, 8 ; count = 8 (number of elements) + +sum_loop: + ld r19, r17 ; load current array element into r19 + add r16, r19 ; add element to sum: sum += array[i] + inc r17 ; advance to next address: address++ + dec r18 ; decrement counter: count-- + + ; continue if more elements remain + cpi r18, 0 + brne sum_loop ; branch if count != 0 + +done: + jmp done ; infinite loop (halt) diff --git a/examples/bubblesort.asm b/examples/bubblesort.asm index 837a1e8..01d6f35 100644 --- a/examples/bubblesort.asm +++ b/examples/bubblesort.asm @@ -1,67 +1,71 @@ -; Bubble sort using RAM (addresses 100..131) - 32 elements -; Purpose: fill RAM[100..131] with pseudo-random bytes and sort them -; Registers (use R16..R31 for LDI immediates): -; R16 - base address (start = 100) -; R17 - index / loop counter for initialization -; R18 - PRNG state (seed) -; R19..R24 - temporary registers used in loops and swaps -; R25 - PRNG multiplier (kept aside to avoid clobber in MUL) -; -; The code below is split into two phases: -; 1) init_loop: generate and store 32 pseudo-random bytes at RAM[100..131] -; 2) outer/inner loops: perform a simple bubble sort over those 32 bytes +; Bubble sort: fill RAM[0x60..0x80] with random values and sort ascending +; Uses PRNG (pseudo-random number generator) to create test data +; Then performs bubble sort by comparing adjacent elements +; Registers: R16 = address, R17 = index/i, R18 = seed/i, R19 = j +; R20-R24 = temp values, R25 = PRNG multiplier +; Output: Sorted array at RAM[0x60..0x80] (32 bytes) - ; initialize pointers and PRNG - ldi r16, 100 ; base address - ldi r17, 0 ; index = 0 - ldi r18, 123 ; PRNG seed - ldi r25, 75 ; PRNG multiplier (kept in r25 so mul doesn't clobber it) + ; initialize PRNG and loop counters + ldi r16, 0x60 ; base address + ldi r17, 0 ; index = 0 + ldi r18, 123 ; PRNG seed (starting value) + ldi r25, 75 ; PRNG multiplier (constant for random generation) init_loop: - ; PRNG step: r2 := lowbyte(r2 * 75), then tweak - mul r18, r25 ; r18 = low byte of (r18 * 75) - inc r18 ; small increment to avoid repeating patterns - ; store generated byte into memory at base + index - st r16, r18 ; RAM[base] = r18 + ; generate pseudo-random byte: seed = (seed * 75) + 1 + mul r18, r25 ; multiply seed by 75 (low byte only) + inc r18 ; add 1 to avoid zero cycles + + ; store generated value at RAM[base + index] + st r16, r18 ; RAM[0x60 + index] = random value inc r16 ; advance base pointer inc r17 ; increment index + + ; check if we've generated 32 values ldi r23, 32 - cp r17, r23 - brne init_loop + cp r17, r23 ; compare index with 32 + brne init_loop ; continue if not done + + ; bubble sort: 32 elements (outer loop runs 31 times) + ldi r18, 0 ; i = 0 (outer loop counter) -; Bubble sort for 32 elements (perform passes until i == 31) - ldi r18, 0 ; i = 0 (outer loop counter) outer_loop: - ldi r19, 0 ; j = 0 (inner loop counter) + ldi r19, 0 ; j = 0 (inner loop counter - element index) + inner_loop: - ; compute address of element A = base + j - ldi r20, 100 + ; load element A = RAM[0x60 + j] + ldi r20, 0x60 ; compute address of element A add r20, r19 - ld r21, r20 ; r21 = A - ; compute address of element B = base + j + 1 - ldi r22, 100 + ld r21, r20 ; r21 = value of element A + + ; load element B = RAM[0x60 + j + 1] + ldi r22, 0x60 ; compute address of element B add r22, r19 ldi r23, 1 - add r22, r23 - ld r24, r22 ; r24 = B - ; compare A and B (we'll swap if A < B) - cp r21, r24 ; sets carry if r21 < r24 - brcc no_swap - ; swap A and B: store B into A's address, A into B's address - st r20, r24 - st r22, r21 + add r22, r23 ; address = 0x60 + j + 1 + ld r24, r22 ; r24 = value of element B + + ; compare and swap if A > B (ascending order) + cp r21, r24 ; compare A with B + brcc no_swap ; skip swap if A < B (carry clear) + st r20, r24 ; RAM[addr_A] = B + st r22, r21 ; RAM[addr_B] = A + no_swap: - inc r19 - ldi r23, 31 + ; advance to next pair of elements + inc r19 ; j++ + ldi r23, 31 ; check if j < 31 (last valid pair) cp r19, r23 - breq end_inner + breq end_inner ; exit inner loop if j == 31 jmp inner_loop + end_inner: - inc r18 - ldi r23, 31 + ; advance outer loop counter + inc r18 ; i++ + ldi r23, 31 ; check if i < 31 (need 31 passes) cp r18, r23 - breq done + breq done ; exit if all passes complete jmp outer_loop done: - jmp done + jmp done ; infinite loop (halt) diff --git a/examples/bubblesort.py b/examples/bubblesort.py deleted file mode 100644 index eca38b3..0000000 --- a/examples/bubblesort.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Example runner that executes the `examples/bubblesort.asm` program. - -This script assembles and runs the bubblesort example and prints the -contents of RAM[100..131]. Optionally it can create an animation using -matplotlib if available. -""" - -from tiny8 import CPU, Visualizer, assemble_file - -prog, labels = assemble_file("examples/bubblesort.asm") -cpu = CPU() -cpu.load_program(prog, labels) -cpu.run(max_cycles=15000) - -print([cpu.read_ram(i) for i in range(100, 132)]) - -viz = Visualizer(cpu) -base = 100 -viz.animate_combined( - interval=1, - mem_addr_start=base, - mem_addr_end=base + 31, - plot_every=100, - # filename="bubblesort.gif", - # fps=60, -) diff --git a/examples/count_bits.asm b/examples/count_bits.asm new file mode 100644 index 0000000..59fc4cc --- /dev/null +++ b/examples/count_bits.asm @@ -0,0 +1,30 @@ +; Count bits: count set bits in 0b10110101 (181) +; Output: R16 = 5 + +start: + ldi r17, 181 ; input = 0b10110101 (5 set bits) + ldi r16, 0 ; count = 0 + mov r18, r17 ; working copy + +loop: + ; check if value is zero + cpi r18, 0 + breq done + + ; if LSB is 1, increment count + mov r19, r18 + ldi r20, 1 + and r19, r20 + cpi r19, 1 + brne no_inc + inc r16 + +no_inc: + ; shift right (divide by 2) + ldi r20, 2 + div r18, r20 + + jmp loop + +done: + jmp done diff --git a/examples/factorial.asm b/examples/factorial.asm new file mode 100644 index 0000000..7ba28af --- /dev/null +++ b/examples/factorial.asm @@ -0,0 +1,24 @@ +; Factorial: compute 5! +; Output: R16 = 120 + +start: + ldi r17, 5 ; n = 5 + ldi r16, 1 ; result = 1 + + ; if n <= 1, done + cpi r17, 2 + brlt done + +loop: + ; result *= n + mul r16, r17 + + ; n-- + dec r17 + + ; continue if n > 1 + cpi r17, 2 + brge loop + +done: + jmp done diff --git a/examples/fibonacci.asm b/examples/fibonacci.asm index 7205574..70f36e0 100644 --- a/examples/fibonacci.asm +++ b/examples/fibonacci.asm @@ -1,52 +1,21 @@ -; Simple iterative Fibonacci -; Purpose: compute fib(n) and leave the result in R16. -; Registers: -; R17 - input n (non-negative integer) -; R16 - 'a' (accumulator / result) -; R18 - 'b' (next Fibonacci term) -; R19 - temporary scratch used for moves/subtracts +; Fibonacci Sequence Calculator +; Calculates the 10th Fibonacci number (F(10) = 55) +; F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2) ; -; Algorithm (iterative): -; a = 0 -; b = 1 -; if n == 0 -> result = a -; if n == 1 -> result = b -; else repeat (n-1) times: (a, b) = (b, a + b) +; Results stored in registers: +; R16 and R17 hold the two most recent Fibonacci numbers -start: - ; initialize: a = 0, b = 1 - LDI R16, 0 ; a = 0 - LDI R18, 1 ; b = 1 + ldi r16, 0 ; F(0) = 0 + ldi r17, 1 ; F(1) = 1 + ldi r18, 9 ; Counter: 9 more iterations to reach F(10) - ; quick exit: if n == 0 then result is a (R16 == 0) - CPI R17, 0 - BREQ done - - ; main loop: run until we've advanced n-1 times -main_loop: - ; if n == 1 then the current 'b' is the result - CPI R17, 1 - BREQ from_b - - ; decrement n (n = n - 1) - LDI R19, 1 - SUB R17, R19 - - ; compute a = a + b (new 'sum' temporarily in R16) - ADD R16, R18 - - ; rotate registers: new a = old b, new b = sum (we used R19 as temp) - MOV R19, R16 ; temp = sum - MOV R16, R18 ; a = old b - MOV R18, R19 ; b = sum - - JMP main_loop - -from_b: - ; when n reached 1, result is b - MOV R16, R18 - ; fall through to halt +loop: + add r16, r17 ; F(n) = F(n-1) + F(n-2) + mov r19, r16 ; Save result temporarily + mov r16, r17 ; Shift: previous = current + mov r17, r19 ; Shift: current = new result + dec r18 ; Decrement counter + brne loop ; Continue if counter != 0 done: - ; infinite loop to halt; result in R16 - JMP done + jmp done ; Infinite loop at end diff --git a/examples/fibonacci.py b/examples/fibonacci.py deleted file mode 100644 index 3e26fcf..0000000 --- a/examples/fibonacci.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Example runner that executes the `examples/fibonacci.asm` program. - -This script assembles and runs the Fibonacci example using the tiny8 CPU -model. It writes input ``n`` into register R17 and runs the program, then -prints selected CPU state. -""" - -from tiny8 import CPU, assemble_file - -n = 13 - -assert n <= 13, "n must be <= 13 to avoid 8-bit overflow" - -program, labels = assemble_file("examples/fibonacci.asm") -cpu = CPU() -cpu.load_program(program, labels) - -cpu.write_reg(17, n) -cpu.run(max_cycles=1000) - -print("R16 =", cpu.read_reg(16)) -print("R17 =", cpu.read_reg(17)) -print("PC =", cpu.pc) -print("SP =", cpu.sp) -print("register changes (reg_trace):\n", *[str(reg) + "\n" for reg in cpu.reg_trace]) diff --git a/examples/find_max.asm b/examples/find_max.asm new file mode 100644 index 0000000..3851302 --- /dev/null +++ b/examples/find_max.asm @@ -0,0 +1,81 @@ +; Find max: find maximum value and its index in array [12, 45, 7, 89, 23, 56, 34, 78] +; Uses linear search to track both maximum value and position +; Registers: R16 = max value, R17 = max index, R18 = address, R19 = count +; R20 = current index, R21 = current value +; Output: R16 = 89 (maximum value), R17 = 3 (index of maximum) + +start: + ; initialize array at RAM[0x60..0x67] with test data + ldi r20, 0x60 + ldi r21, 12 + st r20, r21 ; RAM[0x60] = 12 + + ldi r20, 0x61 + ldi r21, 45 + st r20, r21 ; RAM[0x61] = 45 + + ldi r20, 0x62 + ldi r21, 7 + st r20, r21 ; RAM[0x62] = 7 + + ldi r20, 0x63 + ldi r21, 89 + st r20, r21 ; RAM[0x63] = 89 (this is the maximum) + + ldi r20, 0x64 + ldi r21, 23 + st r20, r21 ; RAM[0x64] = 23 + + ldi r20, 0x65 + ldi r21, 56 + st r20, r21 ; RAM[0x65] = 56 + + ldi r20, 0x66 + ldi r21, 34 + st r20, r21 ; RAM[0x66] = 34 + + ldi r20, 0x67 + ldi r21, 78 + st r20, r21 ; RAM[0x67] = 78 + + ; find maximum value and its index + ldi r16, 0 ; max = 0 (current maximum value) + ldi r17, 0 ; max_index = 0 (index of maximum) + ldi r18, 0x60 ; address = 0x60 (current array position) + ldi r19, 8 ; count = 8 (elements remaining) + ldi r20, 0 ; current_index = 0 + +find_loop: + ld r21, r18 ; load current array element + + ; compare current value with max: if value > max, update both + cp r16, r21 ; compare max with current value + brge no_update ; skip update if max >= value + mov r16, r21 ; update max = current value + mov r17, r20 ; update max_index = current_index + +no_update: + inc r18 ; advance to next address + inc r20 ; increment current index + dec r19 ; decrement count + + ; continue if more elements remain + cpi r19, 0 + brne find_loop ; branch if count != 0 + +done: + jmp done ; infinite loop (halt) + + mov r16, r21 ; max = value + mov r17, r20 ; max_index = current_index + +no_update: + inc r18 ; next address + inc r20 ; next index + dec r19 ; count-- + + cpi r19, 0 + brne find_loop + +done: + jmp done diff --git a/examples/gcd.asm b/examples/gcd.asm new file mode 100644 index 0000000..80bb959 --- /dev/null +++ b/examples/gcd.asm @@ -0,0 +1,29 @@ +; GCD: compute gcd(48, 18) +; Output: R16 = 6 + +start: + ldi r16, 48 ; a = 48 + ldi r17, 18 ; b = 18 + + ; if b == 0, done + cpi r17, 0 + breq done + +loop: + ; compute remainder: a % b + mov r18, r16 ; save a + div r16, r17 ; r16 = a / b + mul r16, r17 ; r16 = (a / b) * b + mov r19, r18 + sub r19, r16 ; r19 = a % b + + ; shift: a = b, b = remainder + mov r16, r17 + mov r17, r19 + + ; continue if b != 0 + cpi r17, 0 + brne loop + +done: + jmp done diff --git a/examples/hello_world.asm b/examples/hello_world.asm new file mode 100644 index 0000000..70d0aa7 --- /dev/null +++ b/examples/hello_world.asm @@ -0,0 +1,33 @@ +; Hello World: store ASCII string "HELLO" in memory +; Demonstrates basic memory storage operations +; Registers: R16 = address pointer, R17 = ASCII character value +; Output: RAM[0x60..0x64] contains bytes [72, 69, 76, 76, 79] ("HELLO") + +start: + ; store 'H' (ASCII 72 = 0x48) + ldi r16, 0x60 ; address = 0x60 + ldi r17, 72 ; ASCII code for 'H' + st r16, r17 ; RAM[0x60] = 'H' + + ; store 'E' (ASCII 69 = 0x45) + ldi r16, 0x61 ; address = 0x61 + ldi r17, 69 ; ASCII code for 'E' + st r16, r17 ; RAM[0x61] = 'E' + + ; store first 'L' (ASCII 76 = 0x4C) + ldi r16, 0x62 ; address = 0x62 + ldi r17, 76 ; ASCII code for 'L' + st r16, r17 ; RAM[0x62] = 'L' + + ; store second 'L' (ASCII 76 = 0x4C) + ldi r16, 0x63 ; address = 0x63 + ldi r17, 76 ; ASCII code for 'L' + st r16, r17 ; RAM[0x63] = 'L' + + ; store 'O' (ASCII 79 = 0x4F) + ldi r16, 0x64 ; address = 0x64 + ldi r17, 79 ; ASCII code for 'O' + st r16, r17 ; RAM[0x64] = 'O' + +done: + jmp done ; infinite loop (halt) diff --git a/examples/is_prime.asm b/examples/is_prime.asm new file mode 100644 index 0000000..bc36f07 --- /dev/null +++ b/examples/is_prime.asm @@ -0,0 +1,51 @@ +; Is prime: check if 17 is a prime number using trial division +; Tests divisibility by all numbers from 2 to n/2 +; Registers: R16 = result (1=prime, 0=not prime), R17 = n, R18 = divisor, R19-R20 = temp +; Output: R16 = 1 (17 is prime) + +start: + ldi r17, 17 ; n = 17 (number to test for primality) + + ; special cases: numbers <= 1 are not prime + cpi r17, 2 + brlt not_prime ; branch if n < 2 + + ; special case: 2 is prime (smallest prime) + cpi r17, 2 + breq is_prime + + ; test divisibility: check divisors from 2 to n/2 + ldi r18, 2 ; divisor = 2 (start with smallest prime) + +check_loop: + ; optimization: if divisor * 2 > n, no need to continue + mov r19, r18 + ldi r20, 2 + mul r19, r20 ; r19 = divisor * 2 + cp r19, r17 ; compare with n + brge is_prime ; if divisor * 2 > n, number is prime + + ; check if n is divisible by current divisor: n % divisor == 0 + mov r19, r17 + div r19, r18 ; r19 = n / divisor (quotient) + mul r19, r18 ; r19 = quotient * divisor + mov r20, r17 + sub r20, r19 ; r20 = n - (quotient * divisor) = remainder + + ; if remainder == 0, n is divisible (not prime) + cpi r20, 0 + breq not_prime + + ; try next divisor + inc r18 ; divisor++ + jmp check_loop + +is_prime: + ldi r16, 1 ; result = 1 (number is prime) + jmp done + +not_prime: + ldi r16, 0 ; result = 0 (number is not prime) + +done: + jmp done ; infinite loop (halt) diff --git a/examples/linear_search.asm b/examples/linear_search.asm new file mode 100644 index 0000000..5bda6cd --- /dev/null +++ b/examples/linear_search.asm @@ -0,0 +1,66 @@ +; Linear search: find 56 in array [12, 45, 7, 89, 23, 56, 34, 78] +; Output: R16 = 5 (index), or 255 if not found + +start: + ; store array at RAM[0x60..0x67] + ldi r20, 0x60 + ldi r21, 12 + st r20, r21 + + ldi r20, 0x61 + ldi r21, 45 + st r20, r21 + + ldi r20, 0x62 + ldi r21, 7 + st r20, r21 + + ldi r20, 0x63 + ldi r21, 89 + st r20, r21 + + ldi r20, 0x64 + ldi r21, 23 + st r20, r21 + + ldi r20, 0x65 + ldi r21, 56 + st r20, r21 + + ldi r20, 0x66 + ldi r21, 34 + st r20, r21 + + ldi r20, 0x67 + ldi r21, 78 + st r20, r21 + + ; search for target = 56 + ldi r17, 56 ; target + ldi r18, 0x60 ; address + ldi r19, 8 ; count + ldi r20, 0 ; index + +search_loop: + ld r21, r18 ; load value + + ; if value == target, found + cp r21, r17 + breq found + + inc r18 ; next address + inc r20 ; next index + dec r19 ; count-- + + cpi r19, 0 + brne search_loop + +not_found: + ldi r16, 255 + jmp done + +found: + mov r16, r20 + +done: + jmp done diff --git a/examples/memory_copy.asm b/examples/memory_copy.asm new file mode 100644 index 0000000..da4cd7c --- /dev/null +++ b/examples/memory_copy.asm @@ -0,0 +1,47 @@ +; Memory copy: copy 6 bytes from RAM[0x60..0x65] to RAM[0x70..0x75] +; Output: RAM[0x70..0x75] = copy of RAM[0x60..0x65] + +start: + ; store source data at RAM[0x60..0x65] + ldi r20, 0x60 + ldi r21, 10 + st r20, r21 + + ldi r20, 0x61 + ldi r21, 20 + st r20, r21 + + ldi r20, 0x62 + ldi r21, 30 + st r20, r21 + + ldi r20, 0x63 + ldi r21, 40 + st r20, r21 + + ldi r20, 0x64 + ldi r21, 50 + st r20, r21 + + ldi r20, 0x65 + ldi r21, 60 + st r20, r21 + + ; copy to destination + ldi r16, 0x60 ; src + ldi r17, 0x70 ; dst + ldi r18, 6 ; count + +copy_loop: + ld r19, r16 ; load from src + st r17, r19 ; store to dst + + inc r16 ; next src + inc r17 ; next dst + dec r18 ; count-- + + cpi r18, 0 + brne copy_loop + +done: + jmp done diff --git a/examples/multiply_by_shift.asm b/examples/multiply_by_shift.asm new file mode 100644 index 0000000..9404688 --- /dev/null +++ b/examples/multiply_by_shift.asm @@ -0,0 +1,33 @@ +; Multiply by shift: compute 7 * 8 using shifts +; Output: R16 = 56 + +start: + ldi r17, 7 ; multiplicand + ldi r18, 8 ; multiplier + ldi r16, 0 ; result = 0 + +loop: + ; if multiplier is zero, done + cpi r18, 0 + breq done + + ; if multiplier LSB is 1, add multiplicand + mov r19, r18 + ldi r20, 1 + and r19, r20 + cpi r19, 1 + brne no_add + add r16, r17 + +no_add: + ; shift multiplicand left (*2) + ldi r20, 2 + mul r17, r20 + + ; shift multiplier right (/2) + div r18, r20 + + jmp loop + +done: + jmp done diff --git a/examples/power.asm b/examples/power.asm new file mode 100644 index 0000000..3f23f1b --- /dev/null +++ b/examples/power.asm @@ -0,0 +1,39 @@ +; Power: compute 3^4 +; Output: R16 = 81 + +start: + ldi r16, 3 ; base = 3 + ldi r17, 4 ; exp = 4 + + ; save base + mov r18, r16 + + ; if exp == 0, result = 1 + cpi r17, 0 + breq exp_zero + + ; if exp == 1, done + cpi r17, 1 + breq done + + ; counter = 1 + ldi r19, 1 + +loop: + ; result *= base + mul r16, r18 + + ; counter++ + inc r19 + + ; continue if counter < exp + cp r19, r17 + brlt loop + + jmp done + +exp_zero: + ldi r16, 1 + +done: + jmp done diff --git a/examples/reverse.asm b/examples/reverse.asm new file mode 100644 index 0000000..dc609f0 --- /dev/null +++ b/examples/reverse.asm @@ -0,0 +1,52 @@ +; Reverse: reverse array [10, 20, 30, 40, 50, 60] +; Output: RAM[0x60..0x65] = [60, 50, 40, 30, 20, 10] + +start: + ; store array at RAM[0x60..0x65] + ldi r20, 0x60 + ldi r21, 10 + st r20, r21 + + ldi r20, 0x61 + ldi r21, 20 + st r20, r21 + + ldi r20, 0x62 + ldi r21, 30 + st r20, r21 + + ldi r20, 0x63 + ldi r21, 40 + st r20, r21 + + ldi r20, 0x64 + ldi r21, 50 + st r20, r21 + + ldi r20, 0x65 + ldi r21, 60 + st r20, r21 + + ; reverse: swap from both ends + ldi r16, 0x60 ; left = 0x60 + ldi r17, 0x65 ; right = 0x65 + +reverse_loop: + ; check if left >= right + cp r16, r17 + brge done + + ; swap RAM[left] and RAM[right] + ld r18, r16 ; temp = RAM[left] + ld r19, r17 ; RAM[left] = RAM[right] + st r16, r19 + st r17, r18 ; RAM[right] = temp + + ; move pointers + inc r16 ; left++ + dec r17 ; right-- + + jmp reverse_loop + +done: + jmp done diff --git a/examples/sum_1_to_n.asm b/examples/sum_1_to_n.asm new file mode 100644 index 0000000..ae6f923 --- /dev/null +++ b/examples/sum_1_to_n.asm @@ -0,0 +1,24 @@ +; Sum: compute 1 + 2 + 3 + ... + 20 using loop +; Demonstrates accumulation pattern with counter +; Registers: R16 = sum (accumulator), R17 = n (limit), R18 = i (loop counter) +; Output: R16 = 210 (sum of integers from 1 to 20) + +start: + ldi r17, 20 ; n = 20 (upper limit) + ldi r16, 0 ; sum = 0 (accumulator starts at zero) + ldi r18, 1 ; i = 1 (loop counter starts at 1) + +loop: + ; add current counter value to sum: sum += i + add r16, r18 + + ; increment counter: i = i + 1 + inc r18 + + ; continue loop if i <= n + cp r18, r17 ; compare i with n + brlt loop ; branch if i < n + breq loop ; also branch if i == n (include n in sum) + +done: + jmp done ; infinite loop (halt) diff --git a/pyproject.toml b/pyproject.toml index 0eed59a..7e52568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "tiny8" version = "0.1.1" -description = "Simulator for an 8-bit CPU with registers, memory, and a stack." +description = "An educational 8-bit CPU simulator with interactive visualization and assembly programming tools" readme = "README.md" authors = [ { name = "sql-hkr", email = "sql.hkr@gmail.com" } @@ -23,6 +23,8 @@ build-backend = "uv_build" [dependency-groups] dev = [ + "pytest>=8.4.2", + "pytest-cov>=6.0.0", "ruff>=0.14.1", "shibuya>=2025.10.20", "sphinx>=8.2.3", diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..eee0408 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,36 @@ +[pytest] +minversion = 8.0 +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +# Output options +addopts = + -ra + --strict-markers + --strict-config + --showlocals + -v + +# Pytest markers +markers = + slow: marks tests as slow (deselect with '-m "not slow"') + integration: marks tests as integration tests + parametrize: marks parametrized tests + +# Coverage options +[coverage:run] +source = src/tiny8 +omit = + */tests/* + */test_*.py + */__pycache__/* + +[coverage:report] +precision = 2 +show_missing = true +skip_covered = false + +[coverage:html] +directory = htmlcov diff --git a/src/tiny8/__init__.py b/src/tiny8/__init__.py index 2b422cf..770e129 100644 --- a/src/tiny8/__init__.py +++ b/src/tiny8/__init__.py @@ -4,11 +4,20 @@ for experimentation and teaching. """ -from .assembler import assemble, assemble_file +from .assembler import AsmResult, assemble, assemble_file from .cli import run_cli from .cpu import CPU +from .utils import ProgressBar from .visualizer import Visualizer -__all__ = ["CPU", "assemble", "assemble_file", "Visualizer", "run_cli"] +__all__ = [ + "CPU", + "AsmResult", + "assemble", + "assemble_file", + "Visualizer", + "run_cli", + "ProgressBar", +] __version__ = "0.1.1" diff --git a/src/tiny8/assembler.py b/src/tiny8/assembler.py index ce4c4af..96917d9 100644 --- a/src/tiny8/assembler.py +++ b/src/tiny8/assembler.py @@ -5,7 +5,24 @@ """ import re -from typing import Dict, List, Tuple +from dataclasses import dataclass, field + + +@dataclass +class AsmResult: + """Result of assembling source code. + + Attributes: + program: List of instruction tuples (mnemonic, operands). + labels: Mapping from label names to program counter addresses. + pc_to_line: Mapping from program counter to original source line number. + source_lines: Original source text split into lines for display. + """ + + program: list[tuple[str, tuple]] = field(default_factory=list) + labels: dict[str, int] = field(default_factory=dict) + pc_to_line: dict[int, int] = field(default_factory=dict) + source_lines: list[str] = field(default_factory=list) def _parse_number(token: str) -> int: @@ -47,135 +64,105 @@ def _parse_number(token: str) -> int: raise ValueError(f"Unable to parse numeric token: {token}") -def parse_asm(text: str) -> Tuple[List[Tuple[str, Tuple]], Dict[str, int]]: +def parse_asm(text: str) -> AsmResult: """Parse assembly source text into a program listing and a label table. - The function scans the given assembly text line-by-line and produces two - values: - - - program: a list of instructions, where each instruction is a tuple - (MNEMONIC, OPERANDS_TUPLE). MNEMONIC is stored in uppercase for - readability (the CPU/runtime looks up handlers using mnemonic.lower()). - OPERANDS_TUPLE is an immutable tuple of operand values in the order they - appear. - - labels: a mapping from label name (str) to program counter (int), where - the program counter is the zero-based index of the instruction in the - produced program list. + The function scans the given assembly text line-by-line and produces + an AsmResult containing the parsed program, labels, source line + mapping, and original source text. Args: text: Assembly source code as a single string. May contain comments, blank lines and labels. Returns: - A pair (program, labels) as described above. + AsmResult object containing program, labels, pc_to_line mapping, + and source_lines. - Notes: - - Comments begin with ';' and extend to the end of the line and are - removed before parsing. + Note: + - Comments begin with ';' and extend to the end of the line. - Labels may appear on a line by themselves or before an instruction - with the form "label: instr ...". A label associates the name - with the current instruction index. + with the form "label: instr ...". - Registers are encoded as ("reg", N) where N is the register number. - Numeric operands are parsed using _parse_number; non-numeric tokens are preserved as strings (symbols) for later resolution. """ - program: List[Tuple[str, Tuple]] = [] - labels: Dict[str, int] = {} + result = AsmResult() pc = 0 lines = text.splitlines() - for line in lines: - # remove comments + result.source_lines = lines.copy() + for line_num, line in enumerate(lines): line = line.split(";", 1)[0].strip() if not line: continue - # label on same line: "label: instr ops" if ":" in line: left, right = line.split(":", 1) lbl = left.strip() - labels[lbl] = pc + result.labels[lbl] = pc line = right.strip() if not line: continue - # tokenization by commas and whitespace parts = [p for p in re.split(r"[\s,]+", line) if p != ""] - # store mnemonic in uppercase for readability; handlers are looked up - # using lower() by the CPU when dispatching. instr = parts[0].upper() ops = [] for p in parts[1:]: pl = p.lower() - # register like r0: mark as a register operand to preserve formatting if pl.startswith("r") and pl[1:].isdigit(): ops.append(("reg", int(pl[1:]))) else: - # try numeric parse, otherwise keep as label string try: n = _parse_number(p) ops.append(n) except ValueError: ops.append(p) - program.append((instr, tuple(ops))) + result.program.append((instr, tuple(ops))) + result.pc_to_line[pc] = line_num pc += 1 - return program, labels + return result -def assemble(text: str) -> Tuple[List[Tuple[str, Tuple]], Dict[str, int]]: +def assemble(text: str) -> AsmResult: """Parse assembly source text and return parsed instructions and label map. Args: - text (str): Assembly source code as a single string. May contain + text: Assembly source code as a single string. May contain multiple lines, labels and comments. Returns: - Tuple[List[Tuple[str, Tuple]], Dict[str, int]]: A pair (instructions, labels): - - instructions: list of parsed instructions in source order. Each - instruction is a tuple (mnemonic, operands) where `mnemonic` is a - string and `operands` is a tuple of operand values as produced by - the assembler. - - labels: mapping from label names (str) to integer addresses - (instruction indices). + AsmResult object containing program, labels, source line mapping, + and original source text. Raises: - Exception: Propagates parsing errors from the underlying parser - (for example, syntax or operand errors). - - Notes: - This function is a thin wrapper around parse_asm(...) and forwards any - exceptions raised by the parser. + Exception: Propagates parsing errors from the underlying parser. Example: >>> src = "start: MOV R1, 5\\nJMP start" - >>> instructions, labels = assemble(src) - >>> labels + >>> result = assemble(src) + >>> result.labels {'start': 0} """ return parse_asm(text) -def assemble_file(path: str): +def assemble_file(path: str) -> AsmResult: """Assemble the contents of a source file. - Reads the entire file at `path` and passes its contents to assemble(...). - Args: - path (str): Path to the source file to assemble. + path: Path to the source file to assemble. Returns: - Any: The result produced by calling assemble(source_text). The exact - type depends on the implementation of assemble(...). + The result produced by calling assemble(source_text). Raises: FileNotFoundError: If the specified file does not exist. OSError: For other I/O related errors when opening or reading the file. Exception: Any exception raised by assemble(...) will be propagated. - Notes: - The file is opened in text mode and read entirely into memory; for very - large files this may be inefficient. + Note: + The file is opened in text mode and read entirely into memory. Example: >>> result = assemble_file("program.asm") - >>> # result now holds the assembled output produced by assemble(...) """ with open(path, "r") as f: return assemble(f.read()) diff --git a/src/tiny8/cli.py b/src/tiny8/cli.py index f42f5e4..165a138 100644 --- a/src/tiny8/cli.py +++ b/src/tiny8/cli.py @@ -1,267 +1,1190 @@ -"""Terminal based visualizer for tiny8 CPU step traces. +"""Terminal-based visualizer for tiny8 CPU step traces. -Provides a simple UI to inspect SREG, registers and a memory range -for a selected step from ``cpu.step_trace``. Interactive keyboard controls -allow play/pause and single-stepping. - -Controls: - .. code-block:: text - - Space - toggle play/pause - l or > - next step - k or < - previous step - w - jump forward 10 steps - b - jump back 10 steps - 0 - jump to first step - $ - jump to last step - q or ESC - quit - -Usage: - .. code-block:: python - - from tiny8.cli_visualizer import run_cli - run_cli(cpu, mem_addr_start=0, mem_addr_end=127) +Simplified and enhanced CLI with Vim-style controls, marks, search, and more. """ from __future__ import annotations import argparse import curses -import math import time +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any +# Key handler registry +_key_handlers: dict[int | str, Callable] = {} -def _format_byte(b: int) -> str: - return f"{b:02X}" +def key_handler(*keys: int | str): + """Decorator to register a function as a handler for specific key(s). -def run_cli(cpu, mem_addr_start: int = 0, mem_addr_end: int = 31, delay: float = 0.15): - """Run the curses-based CLI visualizer for the given CPU. + Args: + *keys: One or more keys (as int or string) to bind to this handler. + + Example: + @key_handler(ord('q'), 27) # q and ESC + def quit_handler(state, ...): + return True # Signal to exit + """ + + def decorator(func: Callable) -> Callable: + for key in keys: + _key_handlers[key] = func + return func + + return decorator + + +@dataclass +class ViewState: + """State container for the CLI visualizer. + + Attributes: + step_idx: Current step index in trace. + scroll_offset: Vertical scroll offset for memory view. + playing: Whether auto-play is active. + last_advance_time: Timestamp of last auto-advance. + delay: Delay between auto-advance steps in seconds. + show_all_regs: Show all 32 registers vs only changed. + show_all_mem: Show non-zero memory vs all memory. + command_mode: Whether in command input mode. + command_buffer: Current command being typed. + marks: Dictionary of named position marks. + status_msg: Current status message to display. + status_time: Timestamp when status message was set. + """ + + step_idx: int = 0 + scroll_offset: int = 0 + playing: bool = False + last_advance_time: float = 0.0 + delay: float = 0.15 + show_all_regs: bool = True + show_all_mem: bool = False + command_mode: bool = False + command_buffer: str = "" + marks: dict[str, int] = field(default_factory=dict) + status_msg: str = "" + status_time: float = 0.0 + + +@dataclass +class KeyContext: + """Context passed to key handlers.""" + + state: ViewState + scr: Any + traces: list + cpu: Any + mem_addr_start: int + mem_addr_end: int + source_lines: list[str] | None + n: int # total steps + + def redraw(self) -> int: + """Redraw screen and return height.""" + return draw_step( + self.scr, + self.state, + self.traces, + self.cpu, + self.mem_addr_start, + self.mem_addr_end, + self.source_lines, + ) + + def set_status(self, msg: str) -> None: + """Set status message.""" + self.state.status_msg = msg + self.state.status_time = time.time() + + +def format_byte(value: int) -> str: + """Format byte value as two-digit hex string. Args: - cpu: CPU instance with a populated `step_trace` list. - mem_addr_start: start address for memory display. - mem_addr_end: end address for memory display. - delay: seconds between automatic steps when playing. + value: Byte value to format (0-255). + + Returns: + Two-character uppercase hex string. """ + return f"{value:02X}" - traces = getattr(cpu, "step_trace", None) - if not traces: - raise RuntimeError( - "cpu.step_trace is empty โ€” run the CPU to populate step_trace first" + +def safe_add( + scr, y: int, x: int, text: str, attr: int = 0, max_x: int | None = None +) -> None: + """Safely add text to screen, handling boundary conditions. + + Args: + scr: Curses screen object. + y: Y coordinate (row). + x: X coordinate (column). + text: Text to display. + attr: Display attributes (e.g., curses.A_BOLD). + max_x: Maximum x coordinate, or None to use screen width. + """ + if max_x is None: + _, max_x = scr.getmaxyx() + if y < 0 or y >= scr.getmaxyx()[0]: + return + # Truncate text to fit within screen bounds + text = text[: max_x - x - 1] if x < max_x else "" + try: + scr.addstr(y, x, text, attr) + except curses.error: + pass + + +def draw_step( + scr, + state: ViewState, + traces: list, + cpu, + mem_start: int, + mem_end: int, + source_lines: list[str] | None = None, +) -> int: + """Draw the current execution step to screen. + + Displays header, SREG flags, assembly source code, registers, and memory for the current step. + Highlights changes from previous step and the currently executing source line. + + Args: + scr: Curses screen object. + state: Current view state. + traces: List of execution trace entries. + cpu: CPU instance (unused, for consistency). + mem_start: Start address for memory display. + mem_end: End address for memory display. + source_lines: Optional list of original assembly source lines for display. + + Returns: + Total number of lines drawn (for scroll calculations). + """ + max_y, max_x = scr.getmaxyx() + scr.erase() + + idx, mem_scroll, n = state.step_idx, state.scroll_offset, len(traces) + entry = traces[idx] + prev = traces[idx - 1] if idx > 0 else None + + pc, sp = entry.get("pc", 0), entry.get("sp", 0) + instr, sreg = entry.get("instr", ""), entry.get("sreg", 0) + prev_sreg = prev.get("sreg", 0) if prev else 0 + regs, prev_regs = entry.get("regs", []), prev.get("regs", []) if prev else [] + mem, prev_mem = entry.get("mem", {}), prev.get("mem", {}) if prev else {} + + line = 0 + + # Header (fixed) - White bold for high visibility + safe_add( + scr, + line, + 0, + f"Step {idx}/{n - 1} PC:0x{pc:04X} SP:0x{sp:04X} {instr}", + curses.color_pair(3) | curses.A_BOLD, + max_x, + ) + line += 2 + + # Assembly Source Code (if available) + if source_lines: + source_line_num = entry.get("source_line", -1) + safe_add( + scr, + line, + 0, + "Assembly Source:", + curses.color_pair(3) | curses.A_BOLD, + max_x, ) + line += 1 - n_steps = len(traces) - - def draw_step(stdscr, idx: int): - stdscr.erase() - entry = traces[idx] - - pc = entry.get("pc", getattr(cpu, "pc", 0)) - sp = entry.get("sp", getattr(cpu, "sp", 0)) - instr = entry.get("instr", "") - - # Header - header = f"Step {idx:{len(str(n_steps - 1))}}/{n_steps - 1} PC:0x{pc:04X} SP:0x{sp:04X}" - if instr: - header += f" RUN: {instr}" - stdscr.addstr(0, 0, header) - - # SREG - s = entry.get("sreg", 0) - # flag_names = "I T H S V N Z C".split() - bits = [(s >> b) & 1 for b in reversed(range(8))] - sstr = " ".join(str(bit) for bit in bits) - stdscr.addstr(2, 0, f"SREG: {sstr} 0x{s:02X}") - - # Registers (compact grid) - regs = entry.get("regs", []) - reg_count = 32 - reg_cols = min(8, int(math.ceil(math.sqrt(reg_count)))) - reg_rows = int(math.ceil(reg_count / reg_cols)) - - stdscr.addstr(4, 0, "Registers:") - for r in range(reg_rows): - row_vals = [] - row_addr = r * reg_cols - for c in range(reg_cols): - i = row_addr + c - if i >= reg_count: - break - val = regs[i] if i < len(regs) else 0 - row_vals.append(_format_byte(val)) - # show register row reference (like memory rows) using hex index - stdscr.addstr(5 + r, 2, f"0x{row_addr:02X}: " + " ".join(row_vals)) - - # Memory (compact grid) - memsnap = entry.get("mem", {}) - mem_count = max(0, mem_addr_end - mem_addr_start + 1) - mem_cols = min(32, int(math.ceil(math.sqrt(mem_count)))) if mem_count > 0 else 1 - mem_rows = int(math.ceil(mem_count / mem_cols)) if mem_count > 0 else 1 - - mem_top = 6 + reg_rows - stdscr.addstr(mem_top, 0, f"Memory {hex(mem_addr_start)}..{hex(mem_addr_end)}:") - for r in range(mem_rows): - row_addr = mem_addr_start + r * mem_cols - row_vals = [] - for c in range(mem_cols): - a = row_addr + c - if a > mem_addr_end: + context_lines = 5 + start_line = max(0, source_line_num - context_lines) + end_line = min(len(source_lines), source_line_num + context_lines + 1) + + lines_before = source_line_num - start_line + lines_after = end_line - source_line_num - 1 + + padding_before = max(0, context_lines - lines_before) + padding_after = max(0, context_lines - lines_after) + + for _ in range(padding_before): + safe_add(scr, line, 2, "", 0, max_x) + line += 1 + + for src_idx in range(start_line, end_line): + if src_idx < len(source_lines): + prefix = ">>>" if src_idx == source_line_num else " " + src_text = source_lines[src_idx].rstrip() + display_text = f"{prefix} {src_idx + 1:3d}: {src_text}" + if src_idx == source_line_num: + attr = curses.color_pair(1) | curses.A_BOLD + else: + attr = 0 + safe_add(scr, line, 2, display_text, attr, max_x) + line += 1 + + for _ in range(padding_after): + safe_add(scr, line, 2, "", 0, max_x) + line += 1 + + line += 1 + + flags = ["I", "T", "H", "S", "V", "N", "Z", "C"] + safe_add(scr, line, 0, "SREG: ", curses.color_pair(3) | curses.A_BOLD, max_x) + x = 6 + for i, name in enumerate(flags): + bit_pos = 7 - i + bit, pbit = (sreg >> bit_pos) & 1, (prev_sreg >> bit_pos) & 1 + if bit != pbit and prev: + # Changed flag: green background + attr = curses.color_pair(1) | curses.A_BOLD + elif bit == 1: + # Set flag: green text to indicate active + attr = curses.color_pair(2) | curses.A_BOLD + else: + # Cleared flag: normal text + attr = 0 + safe_add(scr, line, x, f"{name}:{bit} ", attr, max_x) + x += 4 + safe_add(scr, line, x + 2, f"0x{sreg:02X}", curses.color_pair(2), max_x) + line += 2 + + # Registers (fixed) + safe_add( + scr, + line, + 0, + f"Registers ({'all' if state.show_all_regs else 'changed'}):", + curses.color_pair(3) | curses.A_BOLD, + max_x, + ) + line += 1 + + if state.show_all_regs: + for row in range(2): + base = row * 16 + safe_add( + scr, + line, + 2, + f"R{base:02d}-{base + 15:02d}: ", + curses.color_pair(3), + max_x, + ) + for col in range(16): + r = base + col + if r >= 32: break - if a in memsnap: - val = memsnap[a] + val = regs[r] if r < len(regs) else 0 + pval = prev_regs[r] if r < len(prev_regs) else 0 + if val != pval and prev: + # Changed register: green background + attr = curses.color_pair(1) | curses.A_BOLD + elif val != 0: + # Non-zero register: green text + attr = curses.color_pair(2) else: - try: - val = cpu.read_ram(a) - except Exception: - val = 0 - row_vals.append(_format_byte(val)) - stdscr.addstr( - mem_top + 1 + r, 2, f"0x{row_addr:04X}: " + " ".join(row_vals) + # Zero register: normal text + attr = 0 + safe_add(scr, line, 12 + col * 3, format_byte(val), attr, max_x) + line += 1 + else: + changed = [ + ( + r, + prev_regs[r] if r < len(prev_regs) else 0, + regs[r] if r < len(regs) else 0, ) + for r in range(32) + if (regs[r] if r < len(regs) else 0) + != (prev_regs[r] if r < len(prev_regs) else 0) + and prev + ] + if changed: + for r, old, new in changed: + safe_add( + scr, + line, + 2, + f"R{r:02d}: {format_byte(old)} โ†’ {format_byte(new)}", + curses.color_pair(1) | curses.A_BOLD, + max_x, + ) + line += 1 + else: + safe_add(scr, line, 2, "(no changes)", 0, max_x) + line += 1 + line += 1 + + # Memory header (fixed) + safe_add( + scr, + line, + 0, + f"Memory {hex(mem_start)}..{hex(mem_end)} ({'non-zero' if state.show_all_mem else 'all'}):", + curses.color_pair(3) | curses.A_BOLD, + max_x, + ) + line += 1 + + # Calculate memory viewport + mem_start_line = line + mem_available_lines = max_y - line - 1 # Reserve 1 line for status + + # Build memory lines + mem_lines = [] + if state.show_all_mem: + nz = [(a, v) for a, v in mem.items() if mem_start <= a <= mem_end] + nz.sort() + if nz: + for addr, val in nz: + pval = prev_mem.get(addr, 0) + if val != pval and prev: + # Changed memory: green background + attr = curses.color_pair(1) | curses.A_BOLD + else: + # Non-zero memory: green text + attr = curses.color_pair(2) + ch = chr(val) if 32 <= val <= 126 else "." + mem_lines.append((f"0x{addr:04X}: {format_byte(val)} '{ch}'", attr)) + else: + mem_lines.append(("(all zero)", 0)) + else: + for row in range((mem_end - mem_start + 16) // 16): + row_addr = mem_start + row * 16 + if row_addr > mem_end: + break + line_text = f"0x{row_addr:04X}: " + ascii_parts = [] + highlights = [] + for col in range(16): + addr = row_addr + col + if addr > mem_end: + break + val, pval = mem.get(addr, 0), prev_mem.get(addr, 0) if prev else 0 + if val != pval and prev: + highlights.append((12 + col * 3, format_byte(val))) + ascii_parts.append(chr(val) if 32 <= val <= 126 else ".") + mem_lines.append((line_text, 0, highlights, ascii_parts)) - # Footer - footer_y = mem_top + 2 + mem_rows - stdscr.addstr( - footer_y, + # Render visible memory lines + total_mem_lines = len(mem_lines) + for i in range(mem_available_lines): + mem_line_idx = mem_scroll + i + if mem_line_idx >= total_mem_lines: + break + scr_line = mem_start_line + i + if scr_line >= max_y - 1: + break + + mem_line_data = mem_lines[mem_line_idx] + if state.show_all_mem: + text, attr = mem_line_data + safe_add(scr, scr_line, 2, text, attr, max_x) + else: + line_text, attr, highlights, ascii_parts = mem_line_data + safe_add(scr, scr_line, 2, line_text, curses.color_pair(3), max_x) + # Draw hex bytes + row_addr = mem_start + mem_line_idx * 16 + for col in range(16): + addr = row_addr + col + if addr > mem_end: + break + val = mem.get(addr, 0) + pval = prev_mem.get(addr, 0) if prev else 0 + if val != pval and prev: + # Changed memory: green background + attr = curses.color_pair(1) | curses.A_BOLD + elif val != 0: + # Non-zero memory: green text + attr = curses.color_pair(2) + else: + # Zero memory: normal text + attr = 0 + safe_add(scr, scr_line, 12 + col * 3, format_byte(val), attr, max_x) + # Draw ASCII + safe_add(scr, scr_line, 12 + 48, " " + "".join(ascii_parts), 0, max_x) + + # Status/Command line + status_line = max_y - 1 + scr.move(status_line, 0) + scr.clrtoeol() + + if state.command_mode: + # Command mode: white bold for visibility + safe_add( + scr, + status_line, 0, - "Controls: space: play/pause l: next h: back w: +10 b: -10\n" - + " " * 10 - + "0: start $: end q: quit", + f":{state.command_buffer}", + curses.color_pair(3) | curses.A_BOLD, + max_x, ) - stdscr.refresh() + else: + # Normal mode: show temporary status message or footer + if state.status_msg and time.time() - state.status_time < 0.5: + # Status messages in green + safe_add( + scr, + status_line, + 0, + state.status_msg, + curses.color_pair(2) | curses.A_BOLD, + max_x, + ) + else: + play = "[PLAY]" if state.playing else "[PAUSE]" + info = f" Speed:{state.delay:.2f}s" + if mem_scroll > 0: + info += f" MemScroll:{mem_scroll}/{max(0, total_mem_lines - mem_available_lines)}" + info += " | / for help | q to quit" + # Play/pause in green when active, white otherwise + play_attr = ( + curses.color_pair(2) | curses.A_BOLD + if state.playing + else curses.color_pair(3) | curses.A_BOLD + ) + safe_add(scr, status_line, 0, play, play_attr, max_x) + safe_add(scr, status_line, len(play), info, 0, max_x) + + scr.refresh() + return total_mem_lines + + +def show_help(scr) -> None: + """Display help screen with all available commands and controls. + + Shows comprehensive documentation of keyboard shortcuts, commands, + and features. Waits for user keypress before returning. + + Args: + scr: Curses screen object. + """ + scr.clear() + help_text = [ + "Tiny8 CLI - Help", + "", + "Navigation: l/h: next/prev w/b: ยฑ10 0/$: first/last j/k: scroll", + "Playback: Space: play/pause [/]: slower/faster", + "Display: r: toggle regs M: toggle mem =: step info", + "Marks: ma: set mark 'a: goto mark", + "", + "Commands (press : to enter):", + " 123 - Jump to step 123", + " +50, -20 - Relative jump forward/backward", + " /add - Search forward for instruction containing 'add'", + " ?ldi - Search backward for instruction", + " @100 - Jump to PC address (decimal or 0x hex)", + " r10 - Find next change to register R10", + " r10=42 - Find where R10 equals 42 (decimal or 0x hex)", + " m100 - Find next change to memory address 100", + " m100=0xFF - Find where memory[100] equals 0xFF", + " fZ - Find next change to flag Z (I,T,H,S,V,N,Z,C)", + " fC=1 - Find where flag C equals 1", + " h, help - Show command help", + "", + "Other: /: this help q: quit", + "", + "Press any key...", + ] + for i, line in enumerate(help_text): + try: + scr.addstr(i, 2, line) + except Exception: + pass + scr.refresh() + scr.nodelay(False) + scr.getch() + scr.nodelay(True) + + +def show_info(scr, entry: dict, idx: int) -> None: + """Display detailed information about a specific step. + + Shows PC, SP, instruction, SREG, and all non-zero registers and memory + for the given step. Waits for user keypress before returning. + + Args: + scr: Curses screen object. + entry: Trace entry dictionary containing step data. + idx: Step index number. + """ + scr.clear() + lines = [ + f"Step {idx} Details", + "", + f"PC: 0x{entry.get('pc', 0):04X} SP: 0x{entry.get('sp', 0):04X}", + f"Instruction: {entry.get('instr', 'N/A')}", + f"SREG: 0x{entry.get('sreg', 0):02X}", + "", + "Non-zero registers:", + ] + for i, v in enumerate(entry.get("regs", [])): + if v: + lines.append(f" R{i:02d} = 0x{v:02X} ({v})") + lines.append("") + lines.append("Non-zero memory:") + for a in sorted(entry.get("mem", {}).keys()): + v = entry["mem"][a] + ch = chr(v) if 32 <= v <= 126 else "." + lines.append(f" 0x{a:04X} = 0x{v:02X} ({v}) '{ch}'") + lines.append("") + lines.append("Press any key...") + + for i, line in enumerate(lines): + try: + scr.addstr(i, 2, line) + except Exception: + pass + scr.refresh() + scr.nodelay(False) + scr.getch() + scr.nodelay(True) + + +def run_command(state: ViewState, traces: list[dict]) -> str: + """Execute a command and return status message. + + Handles all command types including navigation, search, and tracking. + Updates state.step_idx and state.scroll_offset as needed. + + Args: + state: Current ViewState object to modify. + traces: List of trace entry dictionaries. + + Returns: + Status message string describing the result. + + Command Types: + - Numeric (123): Jump to step 123 + - Relative (ยฑ50): Jump forward/backward 50 steps + - Forward search (/add): Find instruction containing "add" + - Backward search (?ldi): Search backward for "ldi" + - PC jump (@100): Jump to PC address 0x64 + - Register track (r10): Find next change to R10 + - Register search (r10=42): Find where R10 equals 42 + - Memory track (m100): Find next change to memory[100] + - Memory search (m100=0xFF): Find where memory[100] equals 0xFF + - Flag track (fZ): Find next change to flag Z + - Flag search (fC=1): Find where flag C equals 1 + - Help (h, help): Show command documentation + """ + cmd = state.command_buffer.strip() + n = len(traces) + + # Jump to absolute step number + if cmd.isdigit(): + t = int(cmd) + if 0 <= t < n: + state.step_idx, state.scroll_offset = t, 0 + return f"โ†’ step {t}" + return f"Invalid: {t}" + + # Relative jump (+50, -20) + if cmd and cmd[0] in "+-" and cmd[1:].isdigit(): + new_idx = state.step_idx + int(cmd) + if 0 <= new_idx < n: + state.step_idx, state.scroll_offset = new_idx, 0 + return f"โ†’ step {new_idx}" + return f"Invalid: {new_idx}" + + # Search forward for instruction (/add, /ldi r16) + if cmd.startswith("/"): + search = cmd[1:].lower().strip() + if not search: + return "Empty search" + for i in range(state.step_idx + 1, n): + instr = traces[i].get("instr", "").lower() + if search in instr: + state.step_idx, state.scroll_offset = i, 0 + return f"Found at step {i}: {traces[i].get('instr', '')}" + return f"Not found: {search}" + + # Search backward (?add, ?ldi r16) + if cmd.startswith("?"): + search = cmd[1:].lower().strip() + if not search: + return "Empty search" + for i in range(state.step_idx - 1, -1, -1): + instr = traces[i].get("instr", "").lower() + if search in instr: + state.step_idx, state.scroll_offset = i, 0 + return f"Found at step {i}: {traces[i].get('instr', '')}" + return f"Not found: {search}" + + # Jump to PC address (@100, @0x64) + if cmd.startswith("@"): + try: + addr_str = cmd[1:].strip() + target_pc = ( + int(addr_str, 16) if addr_str.startswith("0x") else int(addr_str) + ) + for i in range(n): + if traces[i].get("pc", -1) == target_pc: + state.step_idx, state.scroll_offset = i, 0 + return f"โ†’ step {i} (PC=0x{target_pc:04X})" + return f"PC 0x{target_pc:04X} not found" + except ValueError: + return f"Invalid address: {cmd}" + + # Find register change (r10, r16=42) + if cmd.startswith("r") and len(cmd) >= 2: + try: + parts = cmd[1:].split("=") + reg_num = int(parts[0]) + if not (0 <= reg_num <= 31): + return f"Invalid register: R{reg_num}" - def _curses_main(stdscr): + if len(parts) == 1: + # Find next change to this register + current_val = ( + traces[state.step_idx].get("regs", [])[reg_num] + if reg_num < len(traces[state.step_idx].get("regs", [])) + else 0 + ) + for i in range(state.step_idx + 1, n): + regs = traces[i].get("regs", []) + if reg_num < len(regs) and regs[reg_num] != current_val: + state.step_idx, state.scroll_offset = i, 0 + return f"R{reg_num} changed at step {i}: 0x{regs[reg_num]:02X}" + return f"R{reg_num} doesn't change" + else: + # Find where register equals value + target_val = ( + int(parts[1], 16) if parts[1].startswith("0x") else int(parts[1]) + ) + for i in range(state.step_idx + 1, n): + regs = traces[i].get("regs", []) + if reg_num < len(regs) and regs[reg_num] == target_val: + state.step_idx, state.scroll_offset = i, 0 + return f"R{reg_num}=0x{target_val:02X} at step {i}" + return f"R{reg_num}=0x{target_val:02X} not found" + except (ValueError, IndexError): + return f"Invalid: {cmd}" + + # Memory search (m100, m0x64=42) + if cmd.startswith("m") and len(cmd) >= 2: + try: + parts = cmd[1:].split("=") + addr = int(parts[0], 16) if parts[0].startswith("0x") else int(parts[0]) + + if len(parts) == 1: + # Find next change to this memory address + current_val = traces[state.step_idx].get("mem", {}).get(addr, 0) + for i in range(state.step_idx + 1, n): + mem = traces[i].get("mem", {}) + new_val = mem.get(addr, 0) + if new_val != current_val: + state.step_idx, state.scroll_offset = i, 0 + return f"Mem[0x{addr:04X}] changed at step {i}: 0x{new_val:02X}" + return f"Mem[0x{addr:04X}] doesn't change" + else: + # Find where memory equals value + target_val = ( + int(parts[1], 16) if parts[1].startswith("0x") else int(parts[1]) + ) + for i in range(state.step_idx + 1, n): + mem = traces[i].get("mem", {}) + if mem.get(addr, 0) == target_val: + state.step_idx, state.scroll_offset = i, 0 + return f"Mem[0x{addr:04X}]=0x{target_val:02X} at step {i}" + return f"Mem[0x{addr:04X}]=0x{target_val:02X} not found" + except (ValueError, IndexError): + return f"Invalid: {cmd}" + + # Flag search (fZ, fC=1) + if cmd.startswith("f") and len(cmd) >= 2: + flag_map = {"I": 7, "T": 6, "H": 5, "S": 4, "V": 3, "N": 2, "Z": 1, "C": 0} + try: + parts = cmd[1:].split("=") + flag_name = parts[0].upper() + if flag_name not in flag_map: + return f"Invalid flag: {flag_name}" + + bit_pos = flag_map[flag_name] + + if len(parts) == 1: + # Find next change to this flag + current_sreg = traces[state.step_idx].get("sreg", 0) + current_bit = (current_sreg >> bit_pos) & 1 + for i in range(state.step_idx + 1, n): + sreg = traces[i].get("sreg", 0) + bit = (sreg >> bit_pos) & 1 + if bit != current_bit: + state.step_idx, state.scroll_offset = i, 0 + return f"Flag {flag_name} changed at step {i}: {bit}" + return f"Flag {flag_name} doesn't change" + else: + # Find where flag equals value + target_val = int(parts[1]) + if target_val not in [0, 1]: + return "Flag value must be 0 or 1" + for i in range(state.step_idx + 1, n): + sreg = traces[i].get("sreg", 0) + bit = (sreg >> bit_pos) & 1 + if bit == target_val: + state.step_idx, state.scroll_offset = i, 0 + return f"Flag {flag_name}={target_val} at step {i}" + return f"Flag {flag_name}={target_val} not found" + except (ValueError, KeyError): + return f"Invalid: {cmd}" + + # Help command + if cmd in ["h", "help"]: + return "Commands: NUM, ยฑNUM, /instr, ?instr, @addr, rN[=val], mADDR[=val], fFLAG[=val]" + + return f"Unknown: {cmd}" + + +# Key handler functions using decorator pattern +@key_handler(ord("q"), 27) # q and ESC +def handle_quit(ctx: KeyContext) -> bool: + """Quit the visualizer.""" + return True # Signal to exit + + +@key_handler(ord(" ")) +def handle_play_pause(ctx: KeyContext) -> int: + """Toggle play/pause.""" + ctx.state.playing = not ctx.state.playing + if ctx.state.playing: + ctx.state.last_advance_time = time.time() + return ctx.redraw() + + +@key_handler(ord("l"), curses.KEY_RIGHT) +def handle_step_forward(ctx: KeyContext) -> int: + """Step forward.""" + ctx.state.step_idx = min(ctx.n - 1, ctx.state.step_idx + 1) + ctx.state.scroll_offset = 0 + return ctx.redraw() + + +@key_handler(ord("h"), curses.KEY_LEFT) +def handle_step_backward(ctx: KeyContext) -> int: + """Step backward.""" + ctx.state.step_idx = max(0, ctx.state.step_idx - 1) + ctx.state.scroll_offset = 0 + return ctx.redraw() + + +@key_handler(ord("w")) +def handle_jump_forward(ctx: KeyContext) -> int: + """Jump forward 10 steps.""" + ctx.state.step_idx = min(ctx.n - 1, ctx.state.step_idx + 10) + ctx.state.scroll_offset = 0 + return ctx.redraw() + + +@key_handler(ord("b")) +def handle_jump_backward(ctx: KeyContext) -> int: + """Jump backward 10 steps.""" + ctx.state.step_idx = max(0, ctx.state.step_idx - 10) + ctx.state.scroll_offset = 0 + return ctx.redraw() + + +@key_handler(ord("0")) +def handle_goto_first(ctx: KeyContext) -> int: + """Go to first step.""" + ctx.state.step_idx = 0 + ctx.state.scroll_offset = 0 + return ctx.redraw() + + +@key_handler(ord("$")) +def handle_goto_last(ctx: KeyContext) -> int: + """Go to last step.""" + ctx.state.step_idx = ctx.n - 1 + ctx.state.scroll_offset = 0 + return ctx.redraw() + + +@key_handler(ord("r")) +def handle_toggle_regs(ctx: KeyContext) -> int: + """Toggle showing all registers.""" + ctx.state.show_all_regs = not ctx.state.show_all_regs + return ctx.redraw() + + +@key_handler(ord("M")) +def handle_toggle_mem(ctx: KeyContext) -> int: + """Toggle showing all memory.""" + ctx.state.show_all_mem = not ctx.state.show_all_mem + return ctx.redraw() + + +@key_handler(ord("[")) +def handle_slower(ctx: KeyContext) -> int: + """Decrease playback speed.""" + ctx.state.delay = min(2.0, ctx.state.delay + 0.05) + ctx.set_status(f"Speed: {ctx.state.delay:.2f}s") + return ctx.redraw() + + +@key_handler(ord("]")) +def handle_faster(ctx: KeyContext) -> int: + """Increase playback speed.""" + ctx.state.delay = max(0.05, ctx.state.delay - 0.05) + ctx.set_status(f"Speed: {ctx.state.delay:.2f}s") + return ctx.redraw() + + +@key_handler(ord("=")) +def handle_show_info(ctx: KeyContext) -> int: + """Show detailed step information.""" + show_info(ctx.scr, ctx.traces[ctx.state.step_idx], ctx.state.step_idx) + return ctx.redraw() + + +@key_handler(ord("/")) +def handle_show_help(ctx: KeyContext) -> int: + """Show help screen.""" + show_help(ctx.scr) + return ctx.redraw() + + +@key_handler(ord("j")) +def handle_scroll_down(ctx: KeyContext, h: int) -> int: + """Scroll memory view down.""" + ctx.state.scroll_offset = min(max(0, h - 1), ctx.state.scroll_offset + 1) + return ctx.redraw() + + +@key_handler(ord("k")) +def handle_scroll_up(ctx: KeyContext, h: int) -> int: + """Scroll memory view up.""" + ctx.state.scroll_offset = max(0, ctx.state.scroll_offset - 1) + return ctx.redraw() + + +def run_cli( + cpu, + mem_addr_start: int = 0, + mem_addr_end: int = 31, + delay: float = 0.15, + source_lines: list[str] | None = None, +) -> None: + """Run interactive CLI visualizer in terminal. + + Displays CPU state, registers, memory, and assembly source in a curses-based interface + with keyboard navigation and playback controls. + + Args: + cpu: CPU instance with step_trace attribute containing execution history. + mem_addr_start: Starting memory address to display (default: 0). + mem_addr_end: Ending memory address to display (default: 31). + delay: Initial playback delay in seconds (default: 0.15). + source_lines: Optional list of original assembly source lines for display. + + Raises: + RuntimeError: If cpu.step_trace is empty or missing. + """ + traces = getattr(cpu, "step_trace", None) + if not traces: + raise RuntimeError("cpu.step_trace empty") + + n = len(traces) + + def main(scr): curses.curs_set(0) - stdscr.nodelay(True) - idx = 0 - playing = False + scr.nodelay(True) + + # Initialize color pairs for usability-focused design + curses.start_color() + curses.use_default_colors() + curses.init_pair( + 1, curses.COLOR_BLACK, curses.COLOR_GREEN + ) # Black on green (highlights) + curses.init_pair(2, curses.COLOR_GREEN, -1) # Green text (active/positive) + curses.init_pair(3, curses.COLOR_WHITE, -1) # White text (important info) - draw_step(stdscr, idx) + state = ViewState(delay=delay) + ctx = KeyContext( + state=state, + scr=scr, + traces=traces, + cpu=cpu, + mem_addr_start=mem_addr_start, + mem_addr_end=mem_addr_end, + source_lines=source_lines, + n=n, + ) + h = ctx.redraw() while True: - ch = stdscr.getch() + ch = scr.getch() + + if ch == curses.KEY_RESIZE: + h = ctx.redraw() + continue + if ch != -1: - # handle keys - if ch in (ord("q"), 27): - break - elif ch == ord(" "): - playing = not playing - elif ch in (ord("l"), curses.KEY_RIGHT): - idx = min(n_steps - 1, idx + 1) - draw_step(stdscr, idx) - elif ch in (ord("h"), curses.KEY_LEFT): - idx = max(0, idx - 1) - draw_step(stdscr, idx) - elif ch == ord("w"): - idx = min(n_steps - 1, idx + 10) - draw_step(stdscr, idx) - elif ch == ord("b"): - idx = max(0, idx - 10) - draw_step(stdscr, idx) - elif ch == ord("0"): - idx = 0 - draw_step(stdscr, idx) - elif ch == ord("$"): - idx = n_steps - 1 - draw_step(stdscr, idx) - - if playing: - time.sleep(delay) - if idx < n_steps - 1: - idx += 1 - draw_step(stdscr, idx) - else: - playing = False + # Handle command mode + if state.command_mode: + if ch == 27: + state.command_mode, state.command_buffer = False, "" + h = ctx.redraw() + elif ch in (curses.KEY_ENTER, 10, 13): + state.status_msg = run_command(state, traces) + state.status_time = time.time() + state.command_mode, state.command_buffer = False, "" + h = ctx.redraw() + elif ch in (curses.KEY_BACKSPACE, 127, 8): + state.command_buffer = state.command_buffer[:-1] + my, mx = scr.getmaxyx() + safe_add( + scr, my - 1, 0, f":{state.command_buffer}" + " " * 20, 0, mx + ) + scr.refresh() + elif 32 <= ch <= 126: + state.command_buffer += chr(ch) + my, mx = scr.getmaxyx() + safe_add(scr, my - 1, 0, f":{state.command_buffer}", 0, mx) + scr.refresh() + continue + + # Handle : for command mode + if ch == ord(":"): + state.command_mode, state.command_buffer, state.playing = ( + True, + "", + False, + ) + h = ctx.redraw() + continue + + # Handle marks (m and ') + if ch == ord("m"): + scr.nodelay(False) + mc = scr.getch() + scr.nodelay(True) + if 97 <= mc <= 122: + mn = chr(mc) + state.marks[mn] = state.step_idx + ctx.set_status(f"Mark '{mn}' set") + h = ctx.redraw() + continue + + if ch == ord("'"): + scr.nodelay(False) + mc = scr.getch() + scr.nodelay(True) + if 97 <= mc <= 122: + mn = chr(mc) + if mn in state.marks: + state.step_idx, state.scroll_offset = state.marks[mn], 0 + ctx.set_status(f"โ†’ mark '{mn}'") + else: + ctx.set_status(f"Mark '{mn}' not set") + h = ctx.redraw() + continue + + # Dispatch to registered key handlers + handler = _key_handlers.get(ch) + if handler: + # Handle scroll handlers that need h parameter + if ch in (ord("j"), ord("k")): + result = handler(ctx, h) + else: + result = handler(ctx) + + if result is True: # Quit signal + break + if isinstance(result, int): # New height + h = result + + # Handle auto-play + if state.playing: + t = time.time() + if t - state.last_advance_time >= state.delay: + if state.step_idx < n - 1: + state.step_idx += 1 + h = ctx.redraw() + state.last_advance_time = t + else: + state.playing = False + time.sleep(0.01) else: + # Check if status message expired and needs redraw + if state.status_msg: + elapsed = time.time() - state.status_time + if elapsed >= 0.5: + state.status_msg = "" + h = ctx.redraw() time.sleep(0.05) - curses.wrapper(_curses_main) + curses.wrapper(main) + + +def main() -> None: + """Entry point for CLI command-line interface. + Parses command-line arguments, assembles the input file, runs the CPU, + and launches either CLI or animation mode visualization. + + The function supports two modes: + - CLI mode: Interactive terminal-based step-through debugger + - Animation mode: Generate video/GIF visualization of execution + + Command-line arguments are organized into groups for better clarity: + - Execution options: Control CPU behavior (max-steps) + - Memory display: Configure memory address range (supports hex notation) + - CLI mode: Interactive playback settings + - Animation mode: Video generation parameters + """ + from tiny8 import CPU, __version__, assemble_file + + parser = argparse.ArgumentParser( + prog="tiny8", + description="Tiny8 8-bit CPU simulator with interactive CLI and visualization", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s program.asm # Run in interactive CLI mode + %(prog)s program.asm -m ani -o output.mp4 # Generate animation video + %(prog)s program.asm --mem-start 0x100 --mem-end 0x11F # Custom memory range + %(prog)s program.asm -d 0.5 # Slower playback (0.5s per step) + +Interactive CLI Controls: + l/h or arrows - Navigate forward/backward + w/b - Jump ยฑ10 steps + 0/$ - Jump to first/last step + j/k - Scroll memory view + Space - Play/pause execution + [/] - Adjust playback speed + r - Toggle register display + M - Toggle memory display mode + :123 - Jump to step 123 + /add - Search forward for instruction + q - Quit + """, + ) -def main(): - from tiny8 import CPU, assemble_file + # Version + parser.add_argument( + "--version", + "-v", + action="version", + version=f"%(prog)s {__version__}", + ) - parser = argparse.ArgumentParser(description="Tiny8 CLI Visualizer") + # Required arguments parser.add_argument( "asm_file", - type=str, - help="Path to the assembly file to simulate", + metavar="FILE", + help="path to Tiny8 assembly file to execute", ) + + # Mode selection parser.add_argument( "--mode", "-m", - type=str, + choices=["cli", "ani"], default="cli", - help="Mode to run the simulator in (default: cli)", + help="visualization mode: 'cli' for interactive terminal (default), 'ani' for animation video", ) - parser.add_argument( - "--max_cycles", type=int, default=15000, help="Maximum CPU cycles to run" + + # Execution options + exec_group = parser.add_argument_group("execution options") + exec_group.add_argument( + "--max-steps", + type=int, + default=15000, + metavar="N", + help="maximum number of CPU steps to execute (default: 15000)", ) - parser.add_argument( + + # Memory display options + mem_group = parser.add_argument_group("memory display options") + mem_group.add_argument( "--mem-start", "-ms", - type=int, - default=100, - help="Start address for memory display (default: 100)", + type=lambda x: int(x, 0), # Support 0x hex notation + default=0x00, + metavar="ADDR", + help="starting memory address to display (decimal or 0xHEX, default: 0x00)", ) - parser.add_argument( + mem_group.add_argument( "--mem-end", "-me", - type=int, - default=131, - help="End address for memory display (default: 131)", + type=lambda x: int(x, 0), # Support 0x hex notation + default=0xFF, + metavar="ADDR", + help="ending memory address to display (decimal or 0xHEX, default: 0xFF)", ) - parser.add_argument( + + # CLI mode options + cli_group = parser.add_argument_group("CLI mode options") + cli_group.add_argument( "--delay", "-d", type=float, default=0.15, - help="Delay in seconds between automatic steps when playing (default: 0.15)", + metavar="SEC", + help="initial playback delay between steps in seconds (default: 0.15)", ) - parser.add_argument( + + # Animation mode options + ani_group = parser.add_argument_group("animation mode options") + ani_group.add_argument( "--interval", "-i", type=int, default=1, - help="Interval in milliseconds between frames for animation mode (default: 1)", + metavar="MS", + help="animation update interval in milliseconds (default: 1)", ) - parser.add_argument( + ani_group.add_argument( "--fps", "-f", type=int, default=60, - help="Frames per second for animation mode (default: 60)", + metavar="FPS", + help="frames per second for animation output (default: 60)", ) - parser.add_argument( + ani_group.add_argument( "--plot-every", "-pe", type=int, default=100, - help="Plot every N steps in animation mode (default: 100)", + metavar="N", + help="update plot every N steps for performance (default: 100)", ) - parser.add_argument( + ani_group.add_argument( "--output", "-o", - default=None, - help="Output filename for animation mode (e.g., bubblesort.gif)", + metavar="FILE", + help="output filename for animation (e.g., output.mp4, output.gif)", ) + args = parser.parse_args() - prog, labels = assemble_file(args.asm_file) + + asm = assemble_file(args.asm_file) cpu = CPU() - cpu.load_program(prog, labels) - cpu.run(max_cycles=args.max_cycles) + cpu.load_program(asm) + cpu.run(max_steps=args.max_steps) + if args.mode == "cli": - run_cli(cpu, mem_addr_start=args.mem_start, mem_addr_end=args.mem_end) + run_cli(cpu, args.mem_start, args.mem_end, args.delay, asm.source_lines) elif args.mode == "ani": from tiny8 import Visualizer viz = Visualizer(cpu) - viz.animate_combined( + viz.animate_execution( interval=args.interval, mem_addr_start=args.mem_start, mem_addr_end=args.mem_end, plot_every=args.plot_every, - output_file=args.output, + filename=args.output, fps=args.fps, ) + + +if __name__ == "__main__": + main() diff --git a/src/tiny8/cpu.py b/src/tiny8/cpu.py index f4437e1..82aa267 100644 --- a/src/tiny8/cpu.py +++ b/src/tiny8/cpu.py @@ -2,13 +2,17 @@ This module provides a lightweight CPU model inspired by the ATmega family. The :class:`CPU` class is the primary export and implements a small, extensible instruction-dispatch model. -The implementation favors readability over cycle-accurate emulation. Add +The implementation favors readability over step-accurate emulation. Add instruction handlers by defining methods named ``op_`` on ``CPU``. """ -from typing import Optional +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from .assembler import AsmResult from .memory import Memory +from .utils import ProgressBar # SREG flag bit positions and short descriptions. SREG_I = 7 # Global Interrupt Enable @@ -35,47 +39,37 @@ class CPU: :class:`tiny8.memory.Memory`). sreg (int): Status register bits stored in a single integer (I, T, H, S, V, N, Z, C). - cycle (int): Instruction execution counter. - reg_trace (list[tuple[int, int, int]]): Per-cycle register change - trace entries of the form ``(cycle, reg, new_value)``. - mem_trace (list[tuple[int, int, int]]): Per-cycle memory change - trace entries of the form ``(cycle, addr, new_value)``. + step_count (int): Instruction execution counter. + reg_trace (list[tuple[int, int, int]]): Per-step register change + trace entries of the form ``(step, reg, new_value)``. + mem_trace (list[tuple[int, int, int]]): Per-step memory change + trace entries of the form ``(step, addr, new_value)``. step_trace (list[dict]): Full per-step snapshots useful for visualization and debugging. Note: This implementation simplifies many AVR specifics (flag semantics, - exact cycle counts, IO mapping) in favor of clarity. Extend or + exact step counts, IO mapping) in favor of clarity. Extend or replace individual ``op_`` handlers to increase fidelity. """ def __init__(self, memory: Optional[Memory] = None): self.mem = memory or Memory() - # 32 general purpose 8-bit registers R0-R31 self.regs: list[int] = [0] * 32 - # Program Counter (word-addressable for AVR) - we use byte addressing for simplicity self.pc: int = 0 - # Stack Pointer - point into RAM self.sp: int = self.mem.ram_size - 1 - # Status register (SREG) flags: I T H S V N Z C - store as bits in an int self.sreg: int = 0 - # Cycle counter - self.cycle: int = 0 - # Simple interrupt vector table (addr->enabled) + self.step_count: int = 0 self.interrupts: dict[int, bool] = {} - # Execution trace of register/memory changes per cycle - self.reg_trace: list[tuple[int, int, int]] = [] # (cycle, reg, newval) - self.mem_trace: list[tuple[int, int, int]] = [] # (cycle, addr, newval) - # Full step trace: list of dicts with cycle, pc, instr_text, regs snapshot, optional mem snapshot + self.reg_trace: list[tuple[int, int, int]] = [] + self.mem_trace: list[tuple[int, int, int]] = [] self.step_trace: list[dict] = [] - # Program area - list of instructions as tuples: (mnemonic, *operands) self.program: list[tuple[str, tuple]] = [] - # Labels -> pc self.labels: dict[str, int] = {} - # Running state + self.pc_to_line: dict[int, int] = {} + self.source_lines: list[str] = [] self.running = False - # Helper SREG flags accessors def set_flag(self, bit: int, value: bool) -> None: """Set or clear a specific SREG flag bit. @@ -103,17 +97,16 @@ def get_flag(self, bit: int) -> bool: def _set_flags_add(self, a: int, b: int, carry_in: int, result: int) -> None: """Set flags for ADD/ADC (AVR semantics). - a, b are 0..255 values, carry_in is 0/1, result is integer sum. + Args: + a: First operand (0..255). + b: Second operand (0..255). + carry_in: Carry input (0 or 1). + result: Integer sum result. """ r = result & 0xFF - # Carry (C): bit 8 c = (result >> 8) & 1 - # Half carry (H): carry from bit3 h = (((a & 0x0F) + (b & 0x0F) + carry_in) >> 4) & 1 - # Negative (N): bit7 of result n = (r >> 7) & 1 - # Two's complement overflow (V): when signs of a and b are same and sign of result differs - # Use 8-bit masking to avoid Python's infinite-bit ~ behavior. v = 1 if (((~(a ^ b) & 0xFF) & (a ^ r) & 0x80) != 0) else 0 s = n ^ v z = 1 if r == 0 else 0 @@ -128,15 +121,16 @@ def _set_flags_add(self, a: int, b: int, carry_in: int, result: int) -> None: def _set_flags_sub(self, a: int, b: int, borrow_in: int, result: int) -> None: """Set flags for SUB/CP/CPI (AVR semantics). - Borrow_in is 0/1; result is signed difference (a - b - borrow_in). + Args: + a: Minuend (0..255). + b: Subtrahend (0..255). + borrow_in: Borrow input (0 or 1). + result: Signed difference (a - b - borrow_in). """ r = result & 0xFF - # Carry (C) indicates borrow in subtraction c = 1 if (a - b - borrow_in) < 0 else 0 - # Half-carry (H): borrow from bit4 h = 1 if ((a & 0x0F) - (b & 0x0F) - borrow_in) < 0 else 0 n = (r >> 7) & 1 - # Two's complement overflow (V): if signs of a and b differ and sign of result differs from sign of a v = 1 if ((((a ^ b) & (a ^ r)) & 0x80) != 0) else 0 s = n ^ v z = 1 if r == 0 else 0 @@ -151,24 +145,31 @@ def _set_flags_sub(self, a: int, b: int, borrow_in: int, result: int) -> None: def _set_flags_logical(self, result: int) -> None: """Set flags for logical operations (AND, OR, EOR) per AVR semantics. - Logical ops clear C and V, set N and Z, S = N ^ V, and clear H. + Args: + result: Operation result. + + Note: + Logical ops clear C and V, set N and Z, S = N ^ V, and clear H. """ r = result & 0xFF n = (r >> 7) & 1 z = 1 if r == 0 else 0 - v = 0 - c = 0 - s = n ^ v + s = n # v is 0, so s = n ^ 0 = n - self.set_flag(SREG_C, bool(c)) - self.set_flag(SREG_V, bool(v)) + self.set_flag(SREG_C, False) + self.set_flag(SREG_V, False) self.set_flag(SREG_N, bool(n)) self.set_flag(SREG_S, bool(s)) self.set_flag(SREG_Z, bool(z)) self.set_flag(SREG_H, False) def _set_flags_inc(self, old: int, new: int) -> None: - """Set flags for INC (affects V, N, Z, S). Does not affect C or H.""" + """Set flags for INC (affects V, N, Z, S). Does not affect C or H. + + Args: + old: Value before increment. + new: Value after increment. + """ n = (new >> 7) & 1 v = 1 if old == 0x7F else 0 z = 1 if new == 0 else 0 @@ -181,14 +182,15 @@ def _set_flags_inc(self, old: int, new: int) -> None: def _set_flags_add16(self, a: int, b: int, carry_in: int, result: int) -> None: """Set flags for 16-bit add (ADIW semantics approximation). - a, b are 0..0xFFFF, carry_in is 0/1, result is full integer sum. + Args: + a: First operand (0..0xFFFF). + b: Second operand (0..0xFFFF). + carry_in: Carry input (0 or 1). + result: Full integer sum. """ r = result & 0xFFFF - # Carry out of bit15 c = (result >> 16) & 1 - # Negative (N) is bit15 of result n = (r >> 15) & 1 - # Two's complement overflow (V): when signs of a and b are same and sign of result differs v = 1 if (((~(a ^ b) & 0xFFFF) & (a ^ r) & 0x8000) != 0) else 0 s = n ^ v z = 1 if r == 0 else 0 @@ -198,19 +200,20 @@ def _set_flags_add16(self, a: int, b: int, carry_in: int, result: int) -> None: self.set_flag(SREG_V, bool(v)) self.set_flag(SREG_S, bool(s)) self.set_flag(SREG_Z, bool(z)) - # H is undefined for word ops on AVR; clear conservatively self.set_flag(SREG_H, False) def _set_flags_sub16(self, a: int, b: int, borrow_in: int, result: int) -> None: """Set flags for 16-bit subtraction (SBIW semantics approximation). - Borrow_in is 0/1; result is integer difference a - b - borrow_in. + Args: + a: Minuend (0..0xFFFF). + b: Subtrahend (0..0xFFFF). + borrow_in: Borrow input (0 or 1). + result: Integer difference a - b - borrow_in. """ r = result & 0xFFFF - # Carry indicates borrow out of bit15 c = 1 if (a - b - borrow_in) < 0 else 0 n = (r >> 15) & 1 - # Two's complement overflow (V) for subtraction v = 1 if ((((a ^ b) & (a ^ r)) & 0x8000) != 0) else 0 s = n ^ v z = 1 if r == 0 else 0 @@ -223,7 +226,12 @@ def _set_flags_sub16(self, a: int, b: int, borrow_in: int, result: int) -> None: self.set_flag(SREG_H, False) def _set_flags_dec(self, old: int, new: int) -> None: - """Set flags for DEC (affects V, N, Z, S). Does not affect C or H.""" + """Set flags for DEC (affects V, N, Z, S). Does not affect C or H. + + Args: + old: Value before decrement. + new: Value after decrement. + """ n = (new >> 7) & 1 v = 1 if old == 0x80 else 0 z = 1 if new == 0 else 0 @@ -259,7 +267,7 @@ def write_reg(self, r: int, val: int) -> None: newv = val & 0xFF if self.regs[r] != newv: self.regs[r] = newv - self.reg_trace.append((self.cycle, r, newv)) + self.reg_trace.append((self.step_count, r, newv)) # Memory access wrappers def read_ram(self, addr: int) -> int: @@ -282,33 +290,56 @@ def write_ram(self, addr: int, val: int) -> None: Note: The underlying :class:`Memory` object stores the value; a - ``(cycle, addr, val)`` tuple is appended to ``mem_trace`` for + ``(step, addr, val)`` tuple is appended to ``mem_trace`` for visualizers/tests. """ - self.mem.write_ram(addr, val, self.cycle) - self.mem_trace.append((self.cycle, addr, val & 0xFF)) + self.mem.write_ram(addr, val, self.step_count) + self.mem_trace.append((self.step_count, addr, val & 0xFF)) # Program loading - def load_program(self, program: list[tuple[str, tuple]], labels: dict[str, int]): + def load_program( + self, + program: "list[tuple[str, tuple]] | AsmResult", + labels: Optional[dict[str, int]] = None, + pc_to_line: Optional[dict[int, int]] = None, + source_lines: Optional[list[str]] = None, + ): """Load an assembled program into the CPU. Args: - program: list of ``(mnemonic, operands)`` tuples returned by the - assembler. - labels: Mapping of label strings to instruction indices. + program: Either a list of ``(mnemonic, operands)`` tuples or an + AsmResult object. If AsmResult, other params are ignored. + labels: Mapping of label strings to instruction indices (ignored if + program is AsmResult). + pc_to_line: Optional mapping from PC to source line number for tracing + (ignored if program is AsmResult). + source_lines: Optional original assembly source lines for display + (ignored if program is AsmResult). Note: After loading the program, the program counter is reset to zero. """ - self.program = program - self.labels = labels + # Check if program is an AsmResult + if hasattr(program, "program") and hasattr(program, "labels"): + # It's an AsmResult + asm = program + self.program = asm.program + self.labels = asm.labels + self.pc_to_line = asm.pc_to_line + self.source_lines = asm.source_lines + else: + # Legacy tuple-based format + self.program = program + self.labels = labels or {} + self.pc_to_line = pc_to_line or {} + self.source_lines = source_lines or [] self.pc = 0 # Instruction execution def step(self) -> bool: """Execute a single instruction at the current program counter. - Performs one fetch-decode-execute cycle. A pre-step snapshot of + Performs one fetch-decode-execute step. A pre-step snapshot of registers and non-zero RAM is recorded, the instruction handler (``op_``) is invoked, and a post-step trace entry is appended to ``step_trace``. @@ -364,43 +395,57 @@ def fmt_op(o): handler(*tuple(decoded_ops)) # record step trace after execution (post-state) - self.cycle += 1 + self.step_count += 1 + source_line = self.pc_to_line.get(self.pc, -1) self.step_trace.append( { - "cycle": self.cycle, + "step": self.step_count, "pc": self.pc, "instr": instr_text, "regs": regs_snapshot, "mem": mem_snapshot, "sreg": self.sreg, "sp": self.sp, + "source_line": source_line, } ) self.pc += 1 return True - def run(self, max_cycles: int = 100000) -> None: - """Run instructions until program end or ``max_cycles`` is reached. + def run(self, max_steps: int = 100000, show_progress: bool = True) -> None: + """Run instructions until program end or ``max_steps`` is reached. Args: - max_cycles: Maximum number of instruction cycles to execute + max_steps: Maximum number of instruction steps to execute (default 100000). + show_progress: If True, display a progress bar during execution + (default True). Note: This repeatedly calls :meth:`step` until it returns False or the - maximum cycle count is reached. + maximum step count is reached. """ self.running = True - cycles = 0 - while self.running and cycles < max_cycles: - ok = self.step() - if not ok: - break - cycles += 1 - - # Minimal instruction implementations + steps = 0 + + if show_progress: + pb = ProgressBar(total=max_steps, desc="CPU execution") + + try: + while self.running and steps < max_steps: + ok = self.step() + if not ok: + break + steps += 1 + + if show_progress: + pb.update(1) + finally: + if show_progress: + pb.close() + def op_nop(self): - """No-operation: does nothing for one cycle.""" + """No-operation: does nothing for one step.""" pass def op_ldi(self, reg_idx: int, imm: int): @@ -428,7 +473,12 @@ def op_mov(self, rd: int, rr: int): def op_add(self, rd: int, rr: int): """Add register ``rr`` to ``rd`` (Rd := Rd + Rr) and update flags. - Sets C, H, N, V, S, Z per AVR semantics. + Args: + rd: Destination register index. + rr: Source register index. + + Note: + Sets C, H, N, V, S, Z per AVR semantics. """ a = self.read_reg(rd) b = self.read_reg(rr) @@ -437,25 +487,45 @@ def op_add(self, rd: int, rr: int): self._set_flags_add(a, b, 0, res) def op_and(self, rd: int, rr: int): - """Logical AND (Rd := Rd & Rr) โ€” updates N, Z, V=0, C=0, H=0, S.""" + """Logical AND (Rd := Rd & Rr) โ€” updates N, Z, V=0, C=0, H=0, S. + + Args: + rd: Destination register index. + rr: Source register index. + """ res = self.read_reg(rd) & self.read_reg(rr) self.write_reg(rd, res) self._set_flags_logical(res) def op_or(self, rd: int, rr: int): - """Logical OR (Rd := Rd | Rr) โ€” updates N, Z, V=0, C=0, H=0, S.""" + """Logical OR (Rd := Rd | Rr) โ€” updates N, Z, V=0, C=0, H=0, S. + + Args: + rd: Destination register index. + rr: Source register index. + """ res = self.read_reg(rd) | self.read_reg(rr) self.write_reg(rd, res) self._set_flags_logical(res) def op_eor(self, rd: int, rr: int): - """Exclusive OR (Rd := Rd ^ Rr) โ€” updates N, Z, V=0, C=0, H=0, S.""" + """Exclusive OR (Rd := Rd ^ Rr) โ€” updates N, Z, V=0, C=0, H=0, S. + + Args: + rd: Destination register index. + rr: Source register index. + """ res = self.read_reg(rd) ^ self.read_reg(rr) self.write_reg(rd, res) self._set_flags_logical(res) def op_sub(self, rd: int, rr: int): - """Subtract (Rd := Rd - Rr) and set flags C,H,N,V,S,Z.""" + """Subtract (Rd := Rd - Rr) and set flags C,H,N,V,S,Z. + + Args: + rd: Destination register index. + rr: Source register index. + """ a = self.read_reg(rd) b = self.read_reg(rr) res_full = a - b @@ -463,21 +533,37 @@ def op_sub(self, rd: int, rr: int): self._set_flags_sub(a, b, 0, res_full) def op_inc(self, rd: int): - """Increment (Rd := Rd + 1) โ€” updates V,N,S,Z; does not change C/H.""" + """Increment (Rd := Rd + 1) โ€” updates V,N,S,Z; does not change C/H. + + Args: + rd: Destination register index. + """ old = self.read_reg(rd) new = (old + 1) & 0xFF self.write_reg(rd, new) self._set_flags_inc(old, new) def op_dec(self, rd: int): - """Decrement (Rd := Rd - 1) โ€” updates V,N,S,Z; does not change C/H.""" + """Decrement (Rd := Rd - 1) โ€” updates V,N,S,Z; does not change C/H. + + Args: + rd: Destination register index. + """ old = self.read_reg(rd) new = (old - 1) & 0xFF self.write_reg(rd, new) self._set_flags_dec(old, new) def op_mul(self, rd: int, rr: int): - """Multiply 8x8 -> 16: store low in Rd, high in Rd+1. Update Z and C conservatively.""" + """Multiply 8x8 -> 16: store low in Rd, high in Rd+1. + + Args: + rd: Destination register index for low byte. + rr: Source register index. + + Note: + Updates Z and C flags. Z set if product == 0; C set if high != 0. + """ a = self.read_reg(rd) b = self.read_reg(rr) prod = a * b @@ -486,13 +572,17 @@ def op_mul(self, rd: int, rr: int): self.write_reg(rd, low) if rd + 1 < 32: self.write_reg(rd + 1, high) - # Update flags: Z set if product == 0; C set if high != 0; H undefined -> clear self.set_flag(SREG_Z, prod == 0) self.set_flag(SREG_C, high != 0) self.set_flag(SREG_H, False) def op_adc(self, rd: int, rr: int): - """Add with carry (Rd := Rd + Rr + C) and update flags.""" + """Add with carry (Rd := Rd + Rr + C) and update flags. + + Args: + rd: Destination register index. + rr: Source register index. + """ a = self.read_reg(rd) b = self.read_reg(rr) carry_in = 1 if self.get_flag(SREG_C) else 0 @@ -501,7 +591,11 @@ def op_adc(self, rd: int, rr: int): self._set_flags_add(a, b, carry_in, res) def op_clr(self, rd: int): - """Clear register (Rd := 0). Behaves like EOR Rd,Rd for flags.""" + """Clear register (Rd := 0). Behaves like EOR Rd,Rd for flags. + + Args: + rd: Destination register index. + """ self.write_reg(rd, 0) self.set_flag(SREG_N, False) self.set_flag(SREG_V, False) @@ -511,7 +605,11 @@ def op_clr(self, rd: int): self.set_flag(SREG_H, False) def op_ser(self, rd: int): - """Set register all ones (Rd := 0xFF). Update flags conservatively.""" + """Set register all ones (Rd := 0xFF). Update flags conservatively. + + Args: + rd: Destination register index. + """ self.write_reg(rd, 0xFF) self.set_flag(SREG_N, True) self.set_flag(SREG_V, False) @@ -521,12 +619,19 @@ def op_ser(self, rd: int): self.set_flag(SREG_H, False) def op_div(self, rd: int, rr: int): - """Unsigned divide convenience instruction: quotient -> Rd, remainder -> Rd+1.""" + """Unsigned divide convenience instruction: quotient -> Rd, remainder -> Rd+1. + + Args: + rd: Destination register index for quotient. + rr: Divisor register index. + + Note: + If divisor is zero, sets C and Z flags to indicate error. + """ a = self.read_reg(rd) b = self.read_reg(rr) if b == 0: self.write_reg(rd, 0) - # indicate error self.set_flag(SREG_C, True) self.set_flag(SREG_Z, True) return @@ -535,33 +640,43 @@ def op_div(self, rd: int, rr: int): self.write_reg(rd, q) if rd + 1 < 32: self.write_reg(rd + 1, r) - # update flags: Z if quotient zero; clear others conservatively self.set_flag(SREG_Z, q == 0) self.set_flag(SREG_C, False) self.set_flag(SREG_H, False) self.set_flag(SREG_V, False) def op_in(self, rd: int, port: int): + """Read from I/O port into register. + + Args: + rd: Destination register index. + port: Port address to read from. + """ val = self.read_ram(port) self.write_reg(rd, val) def op_out(self, port: int, rr: int): + """Write register value to I/O port. + + Args: + port: Port address to write to. + rr: Source register index. + """ val = self.read_reg(rr) self.write_ram(port, val) def op_jmp(self, label: str | int): """Jump to a given label or numeric address by updating the program counter. - This operation sets the CPU's program counter (self.pc) to the target address minus one. - The subtraction of one accounts for the fact that the instruction dispatcher will typically - increment the program counter after the current instruction completes. - Args: - label (str | int): The jump target. If a string, it is treated as a symbolic label - and looked up in self.labels to obtain its numeric address. If an int (or any - value convertible to int), it is used directly as the numeric address. - """ + label: The jump target. If a string, it is treated as a symbolic label + and looked up in self.labels. If an int, it is used directly as the + numeric address. + Note: + Sets PC to target - 1 because the instruction dispatcher will + increment PC after the current instruction completes. + """ if isinstance(label, str): if label not in self.labels: raise KeyError(f"Label {label} not found") @@ -570,49 +685,71 @@ def op_jmp(self, label: str | int): self.pc = int(label) - 1 def op_cpi(self, rd: int, imm: int): + """Compare register with immediate (sets flags but doesn't modify register). + + Args: + rd: Register index to compare. + imm: Immediate value to compare against. + """ a = self.read_reg(rd) b = imm & 0xFF res = a - b self._set_flags_sub(a, b, 0, res) def op_cp(self, rd: int, rr: int): + """Compare two registers (sets flags but doesn't modify registers). + + Args: + rd: First register index. + rr: Second register index. + """ a = self.read_reg(rd) b = self.read_reg(rr) res = a - b self._set_flags_sub(a, b, 0, res) def op_lsl(self, rd: int): + """Logical shift left (Rd := Rd << 1). + + Args: + rd: Destination register index. + """ v = self.read_reg(rd) carry = (v >> 7) & 1 nv = (v << 1) & 0xFF self.write_reg(rd, nv) - # C from old MSB self.set_flag(SREG_C, bool(carry)) n = (nv >> 7) & 1 - # V = N xor C per AVR for LSL - vflag = 1 if (n ^ carry) else 0 - s = n ^ vflag + vflag = n ^ carry self.set_flag(SREG_N, bool(n)) self.set_flag(SREG_V, bool(vflag)) - self.set_flag(SREG_S, bool(s)) + self.set_flag(SREG_S, bool(n ^ vflag)) self.set_flag(SREG_Z, nv == 0) self.set_flag(SREG_H, False) def op_lsr(self, rd: int): + """Logical shift right (Rd := Rd >> 1). + + Args: + rd: Destination register index. + """ v = self.read_reg(rd) carry = v & 1 nv = (v >> 1) & 0xFF self.write_reg(rd, nv) self.set_flag(SREG_C, bool(carry)) - # N becomes 0 after logical shift right self.set_flag(SREG_N, False) - # V = N xor C -> 0 xor C self.set_flag(SREG_V, bool(carry)) - self.set_flag(SREG_S, bool(False ^ carry)) + self.set_flag(SREG_S, bool(carry)) self.set_flag(SREG_Z, nv == 0) self.set_flag(SREG_H, False) def op_rol(self, rd: int): + """Rotate left through carry. + + Args: + rd: Destination register index. + """ v = self.read_reg(rd) carry_in = 1 if self.get_flag(SREG_C) else 0 carry_out = (v >> 7) & 1 @@ -622,10 +759,15 @@ def op_rol(self, rd: int): self.set_flag(SREG_N, bool((nv >> 7) & 1)) self.set_flag(SREG_Z, nv == 0) self.set_flag(SREG_V, False) - self.set_flag(SREG_S, bool(((nv >> 7) & 1) ^ 0)) + self.set_flag(SREG_S, bool((nv >> 7) & 1)) self.set_flag(SREG_H, False) def op_ror(self, rd: int): + """Rotate right through carry. + + Args: + rd: Destination register index. + """ v = self.read_reg(rd) carry_in = 1 if self.get_flag(SREG_C) else 0 carry_out = v & 1 @@ -635,63 +777,97 @@ def op_ror(self, rd: int): self.set_flag(SREG_N, bool((nv >> 7) & 1)) self.set_flag(SREG_Z, nv == 0) self.set_flag(SREG_V, False) - self.set_flag(SREG_S, bool(((nv >> 7) & 1) ^ 0)) + self.set_flag(SREG_S, bool((nv >> 7) & 1)) self.set_flag(SREG_H, False) def op_com(self, rd: int): - """One's complement: Rd := ~Rd. Updates N,V,S,Z,C per AVR-ish semantics.""" + """One's complement: Rd := ~Rd. Updates N,V,S,Z,C per AVR-ish semantics. + + Args: + rd: Destination register index. + """ v = self.read_reg(rd) nv = (~v) & 0xFF self.write_reg(rd, nv) n = (nv >> 7) & 1 - vflag = 0 - s = n ^ vflag - z = 1 if nv == 0 else 0 - # COM sets Carry in AVR self.set_flag(SREG_N, bool(n)) - self.set_flag(SREG_V, bool(vflag)) - self.set_flag(SREG_S, bool(s)) - self.set_flag(SREG_Z, bool(z)) + self.set_flag(SREG_V, False) + self.set_flag(SREG_S, bool(n)) + self.set_flag(SREG_Z, nv == 0) self.set_flag(SREG_C, True) self.set_flag(SREG_H, False) def op_neg(self, rd: int): - """Two's complement (negate): Rd := 0 - Rd. Flags as subtraction from 0.""" + """Two's complement (negate): Rd := 0 - Rd. Flags as subtraction from 0. + + Args: + rd: Destination register index. + """ a = self.read_reg(rd) res_full = 0 - a self.write_reg(rd, res_full & 0xFF) - # use subtraction helper (0 - a) self._set_flags_sub(0, a, 0, res_full) def op_swap(self, rd: int): - """Swap nibbles in register: Rd[7:4] <-> Rd[3:0]. Does not affect SREG.""" + """Swap nibbles in register: Rd[7:4] <-> Rd[3:0]. Does not affect SREG. + + Args: + rd: Destination register index. + """ v = self.read_reg(rd) nv = ((v & 0x0F) << 4) | ((v >> 4) & 0x0F) self.write_reg(rd, nv) def op_tst(self, rd: int): - """Test: perform AND Rd,Rd and update flags but do not store result.""" + """Test: perform AND Rd,Rd and update flags but do not store result. + + Args: + rd: Register index to test. + """ v = self.read_reg(rd) res = v & v - # logical helper clears C/V/H and sets N/Z/S self._set_flags_logical(res) def op_andi(self, rd: int, imm: int): + """Logical AND with immediate (Rd := Rd & K). + + Args: + rd: Destination register index. + imm: Immediate value. + """ res = self.read_reg(rd) & (imm & 0xFF) self.write_reg(rd, res) self._set_flags_logical(res) def op_ori(self, rd: int, imm: int): + """Logical OR with immediate (Rd := Rd | K). + + Args: + rd: Destination register index. + imm: Immediate value. + """ res = self.read_reg(rd) | (imm & 0xFF) self.write_reg(rd, res) self._set_flags_logical(res) def op_eori(self, rd: int, imm: int): + """Logical EOR with immediate (Rd := Rd ^ K). + + Args: + rd: Destination register index. + imm: Immediate value. + """ res = self.read_reg(rd) ^ (imm & 0xFF) self.write_reg(rd, res) self._set_flags_logical(res) def op_subi(self, rd: int, imm: int): + """Subtract immediate (Rd := Rd - K). + + Args: + rd: Destination register index. + imm: Immediate value. + """ a = self.read_reg(rd) b = imm & 0xFF res_full = a - b @@ -699,6 +875,12 @@ def op_subi(self, rd: int, imm: int): self._set_flags_sub(a, b, 0, res_full) def op_sbc(self, rd: int, rr: int): + """Subtract with carry (Rd := Rd - Rr - C). + + Args: + rd: Destination register index. + rr: Source register index. + """ a = self.read_reg(rd) b = self.read_reg(rr) borrow_in = 1 if self.get_flag(SREG_C) else 0 @@ -707,7 +889,12 @@ def op_sbc(self, rd: int, rr: int): self._set_flags_sub(a, b, borrow_in, res_full) def op_sbci(self, rd: int, imm: int): - """Subtract immediate with carry: Rd := Rd - K - C""" + """Subtract immediate with carry: Rd := Rd - K - C. + + Args: + rd: Destination register index. + imm: Immediate value. + """ a = self.read_reg(rd) b = imm & 0xFF borrow_in = 1 if self.get_flag(SREG_C) else 0 @@ -724,35 +911,58 @@ def op_cli(self): self.set_flag(SREG_I, False) def op_cpse(self, rd: int, rr: int): - """Compare and Skip if Equal: compare Rd,Rr; if equal, skip next instruction.""" + """Compare and Skip if Equal: compare Rd,Rr; if equal, skip next instruction. + + Args: + rd: First register index. + rr: Second register index. + """ a = self.read_reg(rd) b = self.read_reg(rr) - # set compare flags like CP self._set_flags_sub(a, b, 0, a - b) if a == b: - # skip next instruction by advancing PC by one (step() will add one more) self.pc += 1 def op_sbrs(self, rd: int, bit: int): - """Skip next if bit in register is set.""" + """Skip next if bit in register is set. + + Args: + rd: Register index. + bit: Bit position to test. + """ v = self.read_reg(rd) if ((v >> (bit & 7)) & 1) == 1: self.pc += 1 def op_sbrc(self, rd: int, bit: int): - """Skip next if bit in register is clear.""" + """Skip next if bit in register is clear. + + Args: + rd: Register index. + bit: Bit position to test. + """ v = self.read_reg(rd) if ((v >> (bit & 7)) & 1) == 0: self.pc += 1 def op_sbis(self, io_addr: int, bit: int): - """Skip if bit in IO/RAM-mapped address is set.""" + """Skip if bit in IO/RAM-mapped address is set. + + Args: + io_addr: I/O or RAM address. + bit: Bit position to test. + """ v = self.read_ram(io_addr) if ((v >> (bit & 7)) & 1) == 1: self.pc += 1 def op_sbic(self, io_addr: int, bit: int): - """Skip if bit in IO/RAM-mapped address is clear.""" + """Skip if bit in IO/RAM-mapped address is clear. + + Args: + io_addr: I/O or RAM address. + bit: Bit position to test. + """ v = self.read_ram(io_addr) if ((v >> (bit & 7)) & 1) == 0: self.pc += 1 @@ -760,8 +970,9 @@ def op_sbic(self, io_addr: int, bit: int): def op_sbiw(self, rd_word_low: int, imm_word: int): """Subtract immediate from word register pair (Rd:Rd+1) โ€” simplified. - rd_word_low is the low register of the pair (even register index). - imm_word is a 16-bit immediate to subtract. + Args: + rd_word_low: Low register of the pair (even register index). + imm_word: 16-bit immediate to subtract. """ lo = self.read_reg(rd_word_low) hi = self.read_reg(rd_word_low + 1) if (rd_word_low + 1) < 32 else 0 @@ -772,14 +983,14 @@ def op_sbiw(self, rd_word_low: int, imm_word: int): self.write_reg(rd_word_low, new_lo) if rd_word_low + 1 < 32: self.write_reg(rd_word_low + 1, new_hi) - # precise flags for 16-bit subtraction self._set_flags_sub16(word, imm_word & 0xFFFF, 0, word - (imm_word & 0xFFFF)) def op_adiw(self, rd_word_low: int, imm_word: int): """Add immediate to word register pair (Rd:Rd+1) - simplified. - rd_word_low is the low register of the pair (even register index). - imm_word is a 16-bit immediate to add. + Args: + rd_word_low: Low register of the pair (even register index). + imm_word: 16-bit immediate to add. """ lo = self.read_reg(rd_word_low) hi = self.read_reg(rd_word_low + 1) if (rd_word_low + 1) < 32 else 0 @@ -790,29 +1001,31 @@ def op_adiw(self, rd_word_low: int, imm_word: int): self.write_reg(rd_word_low, new_lo) if rd_word_low + 1 < 32: self.write_reg(rd_word_low + 1, new_hi) - # precise flags for 16-bit addition self._set_flags_add16(word, imm_word & 0xFFFF, 0, word + (imm_word & 0xFFFF)) def op_rjmp(self, label: str): - """Relative jump โ€” label may be an int or string label.""" - # reuse op_jmp behavior (op_jmp sets PC to label-1). - # If label is a relative offset integer, set pc accordingly. + """Relative jump โ€” label may be an int or string label. + + Args: + label: Jump target (label name or relative offset). + """ if isinstance(label, int): self.pc = self.pc + int(label) else: self.op_jmp(label) def op_rcall(self, label: str): - """Relative call โ€” push return address and jump relatively or to label.""" + """Relative call โ€” push return address and jump relatively or to label. + + Args: + label: Call target (label name or relative offset). + """ ret = self.pc + 1 self.write_ram(self.sp, (ret >> 8) & 0xFF) self.sp -= 1 self.write_ram(self.sp, ret & 0xFF) self.sp -= 1 if isinstance(label, int): - # For numeric relative offsets, behave like op_jmp which sets - # pc = target - 1 (step() will increment after execution). - # Calculate absolute target and adjust similarly. target = self.pc + int(label) self.pc = int(target) - 1 else: @@ -906,31 +1119,27 @@ def op_brcc(self, label: str): self.op_jmp(label) def op_brge(self, label: str | int): - """BRGE - Branch if Greater or Equal (Signed) + """Branch if Greater or Equal (Signed). Args: label: Destination label or address to jump to if the condition is met. """ - s = self.get_flag(SREG_S) if not s: self.op_jmp(label) def op_brlt(self, label: str | int): - """BRLT - Branch if Less Than (Signed). + """Branch if Less Than (Signed). Args: label: Destination label or address to jump to if the condition is met. """ - s = self.get_flag(SREG_S) if s: self.op_jmp(label) def op_brmi(self, label: str | int): - """BRMI - Branch if Minus (Negative flag set). - - Branches when the N flag is set (negative result). + """Branch if Minus (Negative flag set). Args: label: Destination label or address to jump to if the condition is met. @@ -940,9 +1149,7 @@ def op_brmi(self, label: str | int): self.op_jmp(label) def op_brpl(self, label: str | int): - """BRPL - Branch if Plus (Negative flag clear). - - Branches when the N flag is clear (non-negative result). + """Branch if Plus (Negative flag clear). Args: label: Destination label or address to jump to if the condition is met. @@ -954,11 +1161,12 @@ def op_brpl(self, label: str | int): def op_push(self, rr: int): """Push a register value onto the stack. - The value of register ``rr`` is written to the RAM at the current - stack pointer, and the stack pointer is then decremented. - Args: - rr (int): Source register index to push. + rr: Source register index to push. + + Note: + The value of register ``rr`` is written to RAM at the current + stack pointer, and the stack pointer is then decremented. """ val = self.read_reg(rr) self.write_ram(self.sp, val) @@ -967,11 +1175,12 @@ def op_push(self, rr: int): def op_pop(self, rd: int): """Pop a value from the stack into a register. - The stack pointer is incremented, the byte at the new stack pointer - is read from RAM, and the value is written into register ``rd``. - Args: - rd (int): Destination register index to receive the popped value. + rd: Destination register index to receive the popped value. + + Note: + The stack pointer is incremented, the byte at the new stack pointer + is read from RAM, and the value is written into register ``rd``. """ self.sp += 1 val = self.read_ram(self.sp) @@ -980,14 +1189,13 @@ def op_pop(self, rd: int): def op_call(self, label: str): """Call a subroutine by pushing the return address and jumping to label. - The return address (pc+1) is pushed as two bytes (high then low) onto - the stack, decrementing the stack pointer after each write. Control - then jumps to ``label``. - Args: - label (str): Label to call. + label: Label to call. + + Note: + The return address (pc+1) is pushed as two bytes (high then low) onto + the stack, decrementing the stack pointer after each write. """ - # push return address (pc+1) ret = self.pc + 1 self.write_ram(self.sp, (ret >> 8) & 0xFF) self.sp -= 1 @@ -998,11 +1206,10 @@ def op_call(self, label: str): def op_ret(self): """Return from subroutine by popping the return address and setting PC. - Two bytes are popped from the stack (low then high) to reconstruct the - return address, which is then loaded into the program counter - (adjusted because step() will increment PC after execution). + Note: + Two bytes are popped from the stack (low then high) to reconstruct the + return address, which is then loaded into the program counter. """ - # pop return address self.sp += 1 low = self.read_ram(self.sp) self.sp += 1 @@ -1012,7 +1219,6 @@ def op_ret(self): def op_reti(self): """Return from interrupt: pop return address and set I flag.""" - # similar to ret, but also set Global Interrupt Enable self.sp += 1 low = self.read_ram(self.sp) self.sp += 1 @@ -1021,23 +1227,19 @@ def op_reti(self): self.set_flag(SREG_I, True) self.pc = ret - 1 - # Interrupt handling (very simple) def trigger_interrupt(self, vector_addr: int): """Trigger an interrupt vector if it is enabled. - If the interrupt vector is enabled in ``self.interrupts``, the current - PC+1 is pushed onto the stack (high then low byte) and control jumps - to ``vector_addr``. - Args: - vector_addr (int): Interrupt vector address to jump to. + vector_addr: Interrupt vector address to jump to. - Returns: - None + Note: + If the interrupt vector is enabled in ``self.interrupts``, the current + PC+1 is pushed onto the stack (high then low byte) and control jumps + to ``vector_addr``. """ if not self.interrupts.get(vector_addr, False): return - # push PC and jump to vector ret = self.pc + 1 self.write_ram(self.sp, (ret >> 8) & 0xFF) self.sp -= 1 diff --git a/src/tiny8/memory.py b/src/tiny8/memory.py index 01d335f..a4b8ecc 100644 --- a/src/tiny8/memory.py +++ b/src/tiny8/memory.py @@ -2,13 +2,11 @@ class Memory: - """ - Memory class for a simple byte-addressable RAM/ROM model. + """Memory class for a simple byte-addressable RAM/ROM model. This class provides a minimal memory subsystem with separate RAM and ROM regions, change-logging for writes/loads, and convenience snapshot - methods. All stored values are maintained as 8-bit unsigned bytes - (0-255). + methods. All stored values are maintained as 8-bit unsigned bytes (0-255). Args: ram_size: Number of bytes in RAM (default 2048). @@ -18,13 +16,13 @@ class Memory: ram: Mutable list representing RAM contents (each element 0-255). rom: Mutable list representing ROM contents (each element 0-255). ram_changes: Change log for RAM writes. Each entry is a tuple - (addr, old_value, new_value, cycle) appended only when a write + (addr, old_value, new_value, step) appended only when a write changes the stored byte. rom_changes: Change log for ROM loads. Each entry is a tuple - (addr, old_value, new_value, cycle) appended when load_rom + (addr, old_value, new_value, step) appended when load_rom changes bytes. - Notes: + Note: - All write/load operations mask values with 0xFF so stored values are always in the range 0..255. - ram_changes and rom_changes record only actual changes (old != new). @@ -39,7 +37,6 @@ def __init__(self, ram_size: int = 2048, rom_size: int = 2048): self.rom_size = rom_size self.ram = [0] * ram_size self.rom = [0] * rom_size - # change logs: list of (addr, old, new, cycle) self.ram_changes: list[tuple[int, int, int, int]] = [] self.rom_changes: list[tuple[int, int, int, int]] = [] @@ -61,42 +58,40 @@ def read_ram(self, addr: int) -> int: raise IndexError("RAM address out of range") return self.ram[addr] - def write_ram(self, addr: int, value: int, cycle: int = 0) -> None: + def write_ram(self, addr: int, value: int, step: int = 0) -> None: """Write a byte to RAM at the specified address. Args: addr: Target RAM address. Must be in the range [0, self.ram_size). value: Value to write; only the low 8 bits are stored (value & 0xFF). - cycle: Optional cycle/timestamp associated with this write; defaults - to 0. + step: Optional step/timestamp associated with this write; defaults to 0. Raises: IndexError: If addr is out of the valid RAM range. - Notes: + Note: The provided value is masked to a single byte before storing. If the stored byte changes, a record (addr, old_value, new_value, - cycle) is appended to self.ram_changes to track the modification. + step) is appended to self.ram_changes to track the modification. """ if addr < 0 or addr >= self.ram_size: raise IndexError("RAM address out of range") old = self.ram[addr] self.ram[addr] = value & 0xFF if old != self.ram[addr]: - self.ram_changes.append((addr, old, self.ram[addr], cycle)) + self.ram_changes.append((addr, old, self.ram[addr], step)) def load_rom(self, data: list[int]) -> None: """Load a ROM image into the emulator's ROM buffer. Args: data: Sequence of integer byte values (expected 0-255) comprising - the ROM image. Values outside 0-255 will be truncated to 8 - bits. + the ROM image. Values outside 0-255 will be truncated to 8 bits. Raises: ValueError: If len(data) is greater than self.rom_size. - Notes: + Note: Overwrites self.rom[i] for i in range(len(data)) with (data[i] & 0xFF). Appends (index, old_value, new_value, 0) to self.rom_changes for each address where the value actually changed. @@ -131,8 +126,7 @@ def snapshot_ram(self) -> list[int]: Returns: A new list containing the current contents of RAM. Each element - represents a byte (typically in the range 0-255). The returned list - is a shallow copy of the internal RAM, so modifying it will not + represents a byte (0-255). Modifying the returned list will not affect the emulator's internal state. """ return list(self.ram) diff --git a/src/tiny8/utils.py b/src/tiny8/utils.py new file mode 100644 index 0000000..804f822 --- /dev/null +++ b/src/tiny8/utils.py @@ -0,0 +1,200 @@ +"""Utility functions for Tiny8, including tqdm-like progress bar. + +The ProgressBar class provides a simple, tqdm-like progress indicator that +can be used to visualize long-running CPU executions or other iterative +processes. The progress bar automatically adapts to the terminal width. + +Example usage with CPU: + >>> from tiny8 import CPU, assemble + >>> asm = assemble("ldi r16, 10\\nloop:\\ndec r16\\njmp loop") + >>> cpu = CPU() + >>> cpu.load_program(asm) + >>> cpu.run(max_steps=1000, show_progress=True) + CPU execution: 100.0%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 1000/1000 [00:00<00:00, 5000.00it/s] + +Example usage standalone (auto-detect width): + >>> from tiny8 import ProgressBar + >>> with ProgressBar(total=100, desc="Processing") as pb: + ... for i in range(100): + ... # do work + ... pb.update(1) + +Example usage with custom width: + >>> with ProgressBar(total=100, desc="Processing", ncols=60) as pb: + ... for i in range(100): + ... pb.update(1) +""" + +import shutil +import sys +import time +from typing import Optional + + +class ProgressBar: + """A simple tqdm-like progress bar for Tiny8 CPU execution. + + Usage: + with ProgressBar(total=1000, desc="Running") as pb: + for i in range(1000): + # do work + pb.update(1) + + Or: + pb = ProgressBar(total=1000) + for i in range(1000): + # do work + pb.update(1) + pb.close() + """ + + def __init__( + self, + total: Optional[int] = None, + desc: str = "", + disable: bool = False, + ncols: Optional[int] = None, + mininterval: float = 0.1, + ): + """Initialize progress bar. + + Args: + total: Total number of iterations (None for indeterminate) + desc: Description prefix for the progress bar + disable: If True, disable the progress bar completely + ncols: Width of the progress bar in characters (None for auto-detect) + mininterval: Minimum time between updates in seconds + """ + self.total = total + self.desc = desc + self.disable = disable + self.ncols = ncols + self.mininterval = mininterval + self.n = 0 + self.start_time = time.time() + self.last_print_time = 0 + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + self.close() + return False + + def update(self, n: int = 1): + """Update progress by n steps. + + Args: + n: Number of steps to increment + """ + if self.disable: + return + + self.n += n + current_time = time.time() + + if current_time - self.last_print_time < self.mininterval: + if self.total is not None and self.n >= self.total: + self._print_bar() + return + + self.last_print_time = current_time + self._print_bar() + + def _get_terminal_width(self) -> int: + """Get the current terminal width. + + Returns: + Terminal width in characters, defaults to 80 if unable to detect + """ + if self.ncols is not None: + return self.ncols + + try: + return shutil.get_terminal_size(fallback=(80, 24)).columns + except Exception: + return 80 + + def _print_bar(self): + """Print the progress bar to stderr.""" + if self.disable: + return + + terminal_width = self._get_terminal_width() + elapsed = time.time() - self.start_time + + if self.total is not None and self.total > 0: + percent = min(100, (self.n / self.total) * 100) + + rate = self.n / elapsed if elapsed > 0 else 0 + eta = (self.total - self.n) / rate if rate > 0 else 0 + + prefix = f"{self.desc}: {percent:>5.1f}%|" + suffix = f"| {self.n}/{self.total} [{self._format_time(elapsed)}<{self._format_time(eta)}, {rate:.2f}it/s]" + + fixed_width = len(prefix) + len(suffix) + 1 + bar_width = max(10, terminal_width - fixed_width) + + filled = int(bar_width * self.n / self.total) + bar = "โ–ˆ" * filled + "โ–‘" * (bar_width - filled) + + output = f"\r{prefix}{bar}{suffix}" + else: + spinner = ["โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "] + spin_char = spinner[self.n % len(spinner)] + rate = self.n / elapsed if elapsed > 0 else 0 + + output = f"\r{self.desc}: {spin_char} {self.n} [{self._format_time(elapsed)}, {rate:.2f}it/s]" + + if len(output) > terminal_width: + output = output[: terminal_width - 3] + "..." + + sys.stderr.write(output) + sys.stderr.flush() + + def _format_time(self, seconds: float) -> str: + """Format seconds as MM:SS or HH:MM:SS. + + Args: + seconds: Time in seconds + + Returns: + Formatted time string + """ + if seconds < 0 or seconds != seconds: + return "??:??" + + seconds = int(seconds) + if seconds < 3600: + return f"{seconds // 60:02d}:{seconds % 60:02d}" + else: + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + secs = seconds % 60 + return f"{hours:02d}:{minutes:02d}:{secs:02d}" + + def set_description(self, desc: str): + """Update the description. + + Args: + desc: New description string + """ + self.desc = desc + self._print_bar() + + def close(self): + """Close the progress bar and print newline.""" + if self.disable: + return + + self._print_bar() + sys.stderr.write("\n") + sys.stderr.flush() + + def reset(self): + """Reset the progress bar to initial state.""" + self.n = 0 + self.start_time = time.time() + self.last_print_time = 0 diff --git a/src/tiny8/visualizer.py b/src/tiny8/visualizer.py index 1d02f59..3ba9ae5 100644 --- a/src/tiny8/visualizer.py +++ b/src/tiny8/visualizer.py @@ -5,20 +5,19 @@ memory range. """ -try: - import matplotlib.pyplot as plt -except Exception: - plt = None +import matplotlib.pyplot as plt +import numpy as np +from matplotlib import animation class Visualizer: def __init__(self, cpu): self.cpu = cpu - def animate_combined( + def animate_execution( self, - mem_addr_start: int = 0, - mem_addr_end: int = 31, + mem_addr_start: int = 0x60, + mem_addr_end: int = 0x7F, filename: str | None = None, interval: int = 200, fps: int = 30, @@ -29,195 +28,318 @@ def animate_combined( """Animate SREG bits, registers (R0..R31), and a memory range as three stacked subplots. Args: - mem_addr_start: Start memory address for memory subplot (default 0). - mem_addr_end: End memory address for memory subplot (default 31). - filename: Optional output filename for saving an animation (GIF). If - not provided, show the plot interactively. - interval: Milliseconds between frames in the animation. + mem_addr_start: Start memory address for memory subplot. + mem_addr_end: End memory address for memory subplot. + filename: Optional output filename for saving animation. + interval: Milliseconds between frames. fps: Frames per second for saving output. fontsize: Font size for labels and ticks. cmap: Matplotlib colormap name for the heatmaps. - plot_every: Plot every N cycles (downsampling of frames). - - Notes: - Requires numpy and matplotlib. If Matplotlib's animation or ffmpeg - is not available, the method will attempt to show a static figure. + plot_every: Plot every N steps (downsampling). """ - try: - import numpy as np - except Exception: - print("numpy is required for animation") - return - if plt is None: - print("matplotlib not available - cannot animate") - return - - # Build time dimension from step_trace or reg_trace fallback - if plot_every <= 0: - plot_every = 1 - if self.cpu.step_trace: - max_cycle = max(e["cycle"] for e in self.cpu.step_trace) - cols = max_cycle - else: - traces = self.cpu.reg_trace - if not traces: - print("No activity to animate") - return - cols = max((c for (c, _, _) in traces), default=0) + 1 + num_steps = len(self.cpu.step_trace) - # SREG matrix: 8 flags x cols flag_names = ["I", "T", "H", "S", "V", "N", "Z", "C"] - sreg_mat = np.zeros((8, cols), dtype=float) - # registers matrix - rows = 32 - reg_mat = np.zeros((rows, cols), dtype=float) - # memory matrix + sreg_mat = np.zeros((8, num_steps)) + reg_mat = np.zeros((32, num_steps)) mem_rows = mem_addr_end - mem_addr_start + 1 - mem_mat = np.zeros((mem_rows, cols), dtype=float) - - # initialize with NaN for proper forward-fill - sreg_mat[:] = np.nan - reg_mat[:] = np.nan - mem_mat[:] = np.nan - - # populate from step_trace entries - for e in self.cpu.step_trace: - cidx = max(0, e["cycle"] - 1) - # sreg bits - s = e.get("sreg", 0) + mem_mat = np.zeros((mem_rows, num_steps)) + + for idx, entry in enumerate(self.cpu.step_trace): + s = entry.get("sreg", 0) for b in range(8): - sreg_mat[7 - b, cidx] = 1.0 if ((s >> b) & 1) else 0.0 - # regs snapshot (post-exec uses pre-exec regs snapshot stored as 'regs') - regs = e.get("regs", []) - for r in range(min(rows, len(regs))): - reg_mat[r, cidx] = regs[r] - # mem snapshot: only filled where present - memsnap = e.get("mem", {}) + sreg_mat[7 - b, idx] = 1.0 if ((s >> b) & 1) else 0.0 + + regs = entry.get("regs", []) + for r in range(min(32, len(regs))): + reg_mat[r, idx] = regs[r] + + memsnap = entry.get("mem", {}) for a, v in memsnap.items(): if mem_addr_start <= a <= mem_addr_end: - mem_mat[a - mem_addr_start, cidx] = v - - # forward-fill along time axis - for c in range(1, cols): - for arr in (sreg_mat, reg_mat, mem_mat): - mask = np.isnan(arr[:, c]) - arr[mask, c] = arr[mask, c - 1] - sreg_mat = np.nan_to_num(sreg_mat, nan=0.0) - reg_mat = np.nan_to_num(reg_mat, nan=0.0) - mem_mat = np.nan_to_num(mem_mat, nan=0.0) + mem_mat[a - mem_addr_start, idx] = v plt.style.use("dark_background") - - # create subplots with tight spacing fig, axes = plt.subplots( 3, 1, figsize=(15, 10), gridspec_kw={"height_ratios": [1, 4, 4]} ) im_sreg = axes[0].imshow( - sreg_mat, aspect="auto", cmap=cmap, interpolation="nearest", vmin=0, vmax=1 + sreg_mat[:, :1], + aspect="auto", + cmap=cmap, + interpolation="nearest", + vmin=0, + vmax=1, ) axes[0].set_yticks(range(8)) axes[0].set_yticklabels(flag_names, fontsize=fontsize) axes[0].set_ylabel("SREG", fontsize=fontsize) im_regs = axes[1].imshow( - reg_mat, aspect="auto", cmap=cmap, interpolation="nearest", vmin=0, vmax=255 + reg_mat[:, :1], + aspect="auto", + cmap=cmap, + interpolation="nearest", + vmin=0, + vmax=255, ) - axes[1].set_yticks(range(rows)) - axes[1].set_yticklabels([f"R{i}" for i in range(rows)], fontsize=fontsize) + axes[1].set_yticks(range(32)) + axes[1].set_yticklabels([f"R{i}" for i in range(32)], fontsize=fontsize) axes[1].set_ylabel("Registers", fontsize=fontsize) im_mem = axes[2].imshow( - mem_mat, aspect="auto", cmap=cmap, interpolation="nearest", vmin=0, vmax=255 + mem_mat[:, :1], + aspect="auto", + cmap=cmap, + interpolation="nearest", + vmin=0, + vmax=255, ) axes[2].set_yticks(range(mem_rows)) axes[2].set_yticklabels( [hex(a) for a in range(mem_addr_start, mem_addr_end + 1)], fontsize=fontsize ) axes[2].set_ylabel("Memory", fontsize=fontsize) - axes[2].set_xlabel("cycle", fontsize=fontsize) + axes[2].set_xlabel("Step", fontsize=fontsize) - # Reduce tick padding and label sizes to minimize margins for ax in axes: - ax.tick_params(axis="x", which="both", pad=2, labelsize=fontsize) - ax.tick_params(axis="y", which="both", pad=2) + ax.tick_params(axis="x", labelsize=fontsize) + ax.tick_params(axis="y", labelsize=fontsize) - # smaller colorbars to avoid expanding figure margins - fig.colorbar( - im_sreg, ax=axes[0], orientation="vertical", fraction=0.015, pad=0.01 - ) - fig.colorbar( - im_regs, ax=axes[1], orientation="vertical", fraction=0.015, pad=0.01 - ) - fig.colorbar( - im_mem, ax=axes[2], orientation="vertical", fraction=0.015, pad=0.01 - ) - - # Apply tight layout and then manually nudge subplot bounds to minimize margins - try: - plt.tight_layout(pad=0.2) - except Exception: - pass - # shrink margins as much as reasonable - fig.subplots_adjust(top=0.96, bottom=0.035, hspace=0.02) + fig.colorbar(im_sreg, ax=axes[0], fraction=0.015, pad=0.01) + fig.colorbar(im_regs, ax=axes[1], fraction=0.015, pad=0.01) + fig.colorbar(im_mem, ax=axes[2], fraction=0.015, pad=0.01) - try: - from matplotlib import animation - except Exception: - animation = None + plt.tight_layout(pad=0.2) + fig.subplots_adjust(top=0.96, bottom=0.05, hspace=0.02) def update(frame): - # frame will be the actual column index (cycle index) when frames is an iterable - fidx = frame - - # update images to show up to fidx (inclusive) - def mask_after(arr): - disp = arr.copy() - if fidx + 1 < disp.shape[1]: - disp[:, fidx + 1 :] = 0 - return disp - - im_sreg.set_data(mask_after(sreg_mat)) - im_regs.set_data(mask_after(reg_mat)) - im_mem.set_data(mask_after(mem_mat)) - - # instruction text - instr_text = "" - pc_val = getattr(self.cpu, "pc", None) - sp_val = getattr(self.cpu, "sp", None) - try: - entry = next( - (e for e in self.cpu.step_trace if e["cycle"] - 1 == fidx), None - ) - if entry: - instr_text = entry.get("instr", "") - pc_val = entry.get("pc", pc_val) - sp_val = entry.get("sp", sp_val) - except Exception: - instr_text = "" - - left = ( - f"Cycle: {fidx:5d}, PC: 0x{pc_val:04x}, SP: 0x{sp_val:04x}, Run: {instr_text}" - if instr_text - else f"Cycle {fidx}" - ) - fig.suptitle(left, x=0, ha="left") - return (im_sreg, im_regs, im_mem) + im_sreg.set_data(sreg_mat[:, : frame + 1]) + im_regs.set_data(reg_mat[:, : frame + 1]) + im_mem.set_data(mem_mat[:, : frame + 1]) - if animation is None: - print("matplotlib.animation not available; showing static figure") - plt.show() - return + im_sreg.set_extent([0, frame + 1, 8, 0]) + im_regs.set_extent([0, frame + 1, 32, 0]) + im_mem.set_extent([0, frame + 1, mem_rows, 0]) - frame_iter = range(0, cols, plot_every) + entry = self.cpu.step_trace[frame] + instr = entry.get("instr", "") + pc = entry.get("pc", 0) + sp = entry.get("sp", 0) + + fig.suptitle( + f"Step: {frame}, PC: 0x{pc:04x}, SP: 0x{sp:04x}, {instr}", + x=0.01, + ha="left", + fontsize=fontsize, + ) + return im_sreg, im_regs, im_mem + + frames = range(0, num_steps, plot_every) anim = animation.FuncAnimation( - fig, update, frames=frame_iter, interval=interval, blit=False + fig, update, frames=frames, interval=interval, blit=False ) + if filename: - try: - anim.save(filename, fps=fps) - except Exception as e: - print("Failed to save animation:", e) - plt.show() + anim.save(filename, fps=fps) else: plt.show() + + def show_register_history(self, registers: list[int] = None, figsize=(14, 8)): + """Plot timeline of register value changes over execution. + + Args: + registers: List of register indices to plot (default: R0-R7). + figsize: Figure size as (width, height). + """ + if registers is None: + registers = list(range(8)) + + num_steps = len(self.cpu.step_trace) + reg_data = {r: np.zeros(num_steps) for r in registers} + + for idx, entry in enumerate(self.cpu.step_trace): + regs = entry.get("regs", []) + for r in registers: + if r < len(regs): + reg_data[r][idx] = regs[r] + + plt.style.use("dark_background") + fig, ax = plt.subplots(figsize=figsize) + + for r in registers: + ax.plot(reg_data[r], label=f"R{r}", linewidth=1.5, marker="o", markersize=3) + + ax.set_xlabel("Step", fontsize=12) + ax.set_ylabel("Register Value", fontsize=12) + ax.set_title("Register Values Over Time", fontsize=14, pad=20) + ax.legend(loc="best", ncol=4, fontsize=10) + ax.grid(True, alpha=0.3) + plt.tight_layout() + plt.show() + + def show_memory_access( + self, mem_addr_start: int = 0, mem_addr_end: int = 255, figsize=(12, 8) + ): + """Plot a heatmap showing memory access patterns over time. + + Args: + mem_addr_start: Start memory address. + mem_addr_end: End memory address. + figsize: Figure size as (width, height). + """ + num_steps = len(self.cpu.step_trace) + mem_rows = mem_addr_end - mem_addr_start + 1 + mem_mat = np.zeros((mem_rows, num_steps)) + + for idx, entry in enumerate(self.cpu.step_trace): + memsnap = entry.get("mem", {}) + for a, v in memsnap.items(): + if mem_addr_start <= a <= mem_addr_end: + mem_mat[a - mem_addr_start, idx] = v + + plt.style.use("dark_background") + fig, ax = plt.subplots(figsize=figsize) + + im = ax.imshow(mem_mat, aspect="auto", cmap="viridis", interpolation="nearest") + ax.set_xlabel("Step", fontsize=12) + ax.set_ylabel("Memory Address", fontsize=12) + ax.set_title( + f"Memory Access Pattern (0x{mem_addr_start:04x} - 0x{mem_addr_end:04x})", + fontsize=14, + pad=20, + ) + + num_ticks = min(20, mem_rows) + tick_positions = np.linspace(0, mem_rows - 1, num_ticks, dtype=int) + ax.set_yticks(tick_positions) + ax.set_yticklabels([hex(mem_addr_start + pos) for pos in tick_positions]) + + fig.colorbar(im, ax=ax, label="Value") + plt.tight_layout() + plt.show() + + def show_flag_history(self, figsize=(14, 6)): + """Plot SREG flag changes over execution time. + + Args: + figsize: Figure size as (width, height). + """ + flag_names = ["C", "Z", "N", "V", "S", "H", "T", "I"] + num_steps = len(self.cpu.step_trace) + flag_data = {name: np.zeros(num_steps) for name in flag_names} + + for idx, entry in enumerate(self.cpu.step_trace): + s = entry.get("sreg", 0) + for bit, name in enumerate(flag_names): + flag_data[name][idx] = 1 if ((s >> bit) & 1) else 0 + + plt.style.use("dark_background") + fig, ax = plt.subplots(figsize=figsize) + + offset = 0 + colors = plt.cm.Set3(np.linspace(0, 1, 8)) + + for idx, name in enumerate(flag_names): + values = flag_data[name] + offset + ax.fill_between( + range(num_steps), + offset, + values, + alpha=0.7, + label=name, + color=colors[idx], + ) + offset += 1.2 + + ax.set_xlabel("Step", fontsize=12) + ax.set_ylabel("Flags", fontsize=12) + ax.set_title("SREG Flag Activity", fontsize=14, pad=20) + ax.set_yticks([i * 1.2 + 0.5 for i in range(8)]) + ax.set_yticklabels(flag_names) + ax.legend(loc="upper right", ncol=8, fontsize=10) + ax.grid(True, alpha=0.2, axis="x") + plt.tight_layout() + plt.show() + + def show_statistics(self, top_n: int = 10): + """Plot execution summary statistics. + + Args: + top_n: Number of top memory addresses to show in access frequency. + """ + num_steps = len(self.cpu.step_trace) + + # Count instruction types + instr_counts = {} + for entry in self.cpu.step_trace: + instr = entry.get("instr", "").split()[0] + instr_counts[instr] = instr_counts.get(instr, 0) + 1 + + # Track memory access frequency + mem_access = {} + for entry in self.cpu.step_trace: + memsnap = entry.get("mem", {}) + for addr in memsnap.keys(): + mem_access[addr] = mem_access.get(addr, 0) + 1 + + plt.style.use("dark_background") + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + + # Instruction frequency + sorted_instrs = sorted(instr_counts.items(), key=lambda x: x[1], reverse=True)[ + :top_n + ] + if sorted_instrs: + instrs, counts = zip(*sorted_instrs) + axes[0, 0].barh(instrs, counts, color="skyblue") + axes[0, 0].set_xlabel("Count", fontsize=11) + axes[0, 0].set_title("Top Instructions", fontsize=12) + axes[0, 0].invert_yaxis() + + # Memory access frequency + sorted_mem = sorted(mem_access.items(), key=lambda x: x[1], reverse=True)[ + :top_n + ] + if sorted_mem: + addrs, counts = zip(*sorted_mem) + addr_labels = [f"0x{a:04x}" for a in addrs] + axes[0, 1].barh(addr_labels, counts, color="lightcoral") + axes[0, 1].set_xlabel("Access Count", fontsize=11) + axes[0, 1].set_title("Top Memory Accesses", fontsize=12) + axes[0, 1].invert_yaxis() + + # Register usage + reg_changes = [0] * 32 + for entry in self.cpu.step_trace: + regs = entry.get("regs", []) + for r in range(min(32, len(regs))): + if r < len(regs) and regs[r] != 0: + reg_changes[r] += 1 + + axes[1, 0].bar(range(32), reg_changes, color="mediumseagreen") + axes[1, 0].set_xlabel("Register", fontsize=11) + axes[1, 0].set_ylabel("Non-zero Count", fontsize=11) + axes[1, 0].set_title("Register Usage", fontsize=12) + axes[1, 0].set_xticks(range(0, 32, 4)) + + # Execution timeline + axes[1, 1].text( + 0.5, + 0.5, + f"Total Instructions: {num_steps}\n" + f"Unique Instructions: {len(instr_counts)}\n" + f"Memory Locations Accessed: {len(mem_access)}\n" + f"Final PC: 0x{self.cpu.pc:04x}\n" + f"Final SP: 0x{self.cpu.sp:04x}", + ha="center", + va="center", + fontsize=14, + bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.3), + ) + axes[1, 1].set_title("Execution Summary", fontsize=12) + axes[1, 1].axis("off") + + plt.tight_layout() + plt.show() diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 3b1a540..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Test package for tiny8. - -Contains unit tests for the tiny8 CPU, assembler and memory subsystems. -""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5f85ff9 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,192 @@ +"""Pytest configuration and shared fixtures for Tiny8 test suite. + +This module provides common fixtures, helpers, and configuration for all tests. +""" + +import pytest + +from tiny8 import CPU, assemble, assemble_file + + +@pytest.fixture +def cpu(): + """Provide a fresh CPU instance for each test. + + Returns: + CPU: A new, uninitialized CPU instance. + """ + return CPU() + + +@pytest.fixture +def cpu_with_program(cpu): + """Provide a helper function to load and optionally run a program. + + Returns: + Callable: Function that takes assembly source and returns configured CPU. + """ + + def _load_program(src: str, max_steps: int = 1000, run: bool = True) -> CPU: + """Load assembly code into CPU and optionally run it. + + Args: + src: Assembly source code string. + max_steps: Maximum steps to execute. + run: Whether to run the program immediately. + + Returns: + CPU instance with program loaded (and possibly executed). + """ + asm = assemble(src) + cpu.load_program(asm) + if run: + cpu.run(max_steps=max_steps) + return cpu + + return _load_program + + +@pytest.fixture +def cpu_from_file(cpu): + """Provide a helper function to load and run a program from file. + + Returns: + Callable: Function that takes file path and returns configured CPU. + """ + + def _load_file(path: str, max_steps: int = 10000) -> CPU: + """Load assembly file into CPU and run it. + + Args: + path: Path to assembly file. + max_steps: Maximum steps to execute. + + Returns: + CPU instance with program executed. + """ + asm = assemble_file(path) + cpu.load_program(asm) + cpu.run(max_steps=max_steps) + return cpu + + return _load_file + + +class CPUTestHelper: + """Helper class providing common CPU testing utilities.""" + + @staticmethod + def assert_register(cpu: CPU, reg: int, expected: int, msg: str = None): + """Assert register value with helpful error message. + + Args: + cpu: CPU instance to check. + reg: Register number (0-31). + expected: Expected value. + msg: Optional custom message. + """ + actual = cpu.read_reg(reg) + if msg: + assert actual == expected, f"{msg}: R{reg}={actual}, expected {expected}" + else: + assert actual == expected, ( + f"R{reg}={actual} (0x{actual:02X}), expected {expected} (0x{expected:02X})" + ) + + @staticmethod + def assert_registers(cpu: CPU, values: dict): + """Assert multiple register values. + + Args: + cpu: CPU instance to check. + values: Dictionary mapping register number to expected value. + """ + for reg, expected in values.items(): + CPUTestHelper.assert_register(cpu, reg, expected) + + @staticmethod + def assert_memory(cpu: CPU, addr: int, expected: int, msg: str = None): + """Assert memory value with helpful error message. + + Args: + cpu: CPU instance to check. + addr: Memory address. + expected: Expected value. + msg: Optional custom message. + """ + actual = cpu.read_ram(addr) + if msg: + assert actual == expected, ( + f"{msg}: MEM[0x{addr:04X}]={actual}, expected {expected}" + ) + else: + assert actual == expected, ( + f"MEM[0x{addr:04X}]={actual} (0x{actual:02X}), expected {expected} (0x{expected:02X})" + ) + + @staticmethod + def assert_flag(cpu: CPU, flag: int, expected: bool, name: str = None): + """Assert SREG flag value. + + Args: + cpu: CPU instance to check. + flag: Flag bit position. + expected: Expected boolean value. + name: Optional flag name for error message. + """ + actual = cpu.get_flag(flag) + flag_name = name or f"flag[{flag}]" + assert actual == expected, f"{flag_name}={actual}, expected {expected}" + + @staticmethod + def assert_flags(cpu: CPU, flags: dict): + """Assert multiple flag values. + + Args: + cpu: CPU instance to check. + flags: Dictionary mapping flag bit to expected boolean value. + """ + from tiny8.cpu import ( + SREG_C, + SREG_H, + SREG_I, + SREG_N, + SREG_S, + SREG_T, + SREG_V, + SREG_Z, + ) + + flag_names = { + SREG_C: "C", + SREG_Z: "Z", + SREG_N: "N", + SREG_V: "V", + SREG_S: "S", + SREG_H: "H", + SREG_T: "T", + SREG_I: "I", + } + + for flag, expected in flags.items(): + CPUTestHelper.assert_flag(cpu, flag, expected, flag_names.get(flag)) + + +@pytest.fixture +def helper(): + """Provide CPUTestHelper instance for tests. + + Returns: + CPUTestHelper: Helper class with assertion utilities. + """ + return CPUTestHelper() + + +# Pytest configuration +def pytest_configure(config): + """Configure pytest with custom markers.""" + config.addinivalue_line( + "markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')" + ) + config.addinivalue_line("markers", "integration: marks tests as integration tests") + config.addinivalue_line("markers", "parametrize: marks parametrized tests") diff --git a/tests/test_arithmetic.py b/tests/test_arithmetic.py index b689caa..6074a69 100644 --- a/tests/test_arithmetic.py +++ b/tests/test_arithmetic.py @@ -1,92 +1,438 @@ -import unittest +"""Test suite for arithmetic instructions. -from tiny8 import CPU, assemble +Covers: ADD, ADC, SUB, SUBI, SBC, SBCI, INC, DEC, MUL, DIV, NEG, ADIW, SBIW +""" + +import pytest + +from tiny8 import assemble from tiny8.cpu import SREG_C, SREG_H, SREG_N, SREG_S, SREG_V, SREG_Z -class TestArithmetic(unittest.TestCase): - def setUp(self): - self.cpu = CPU() +class TestADD: + """Test ADD instruction and flag behavior.""" + + def test_add_basic(self, cpu_with_program, helper): + """Test basic addition without overflow.""" + cpu = cpu_with_program(""" + ldi r0, 10 + ldi r1, 20 + add r0, r1 + """) + helper.assert_register(cpu, 0, 30) + + def test_add_overflow_unsigned(self, cpu_with_program, helper): + """Test unsigned overflow sets carry flag.""" + cpu = cpu_with_program(""" + ldi r0, 0xFF + ldi r1, 0x01 + add r0, r1 + """) + helper.assert_register(cpu, 0, 0x00) + helper.assert_flags(cpu, {SREG_C: True, SREG_Z: True, SREG_H: True}) + + def test_add_signed_overflow(self, cpu_with_program, helper): + """Test signed overflow (positive + positive = negative).""" + cpu = cpu_with_program(""" + ldi r0, 0x7F + ldi r1, 0x01 + add r0, r1 + """) + helper.assert_register(cpu, 0, 0x80) + helper.assert_flags( + cpu, + { + SREG_N: True, # Result negative + SREG_V: True, # Signed overflow + SREG_S: False, # S = N ^ V = 1 ^ 1 = 0 + SREG_H: True, # Half carry from bit 3 + SREG_C: False, # No unsigned carry + SREG_Z: False, # Not zero + }, + ) - def run_asm(self, src: str): - prog, labels = assemble(src) - self.cpu.load_program(prog, labels) - self.cpu.run() - return self.cpu + def test_add_zero_flag(self, cpu_with_program, helper): + """Test zero flag with result of zero.""" + cpu = cpu_with_program(""" + ldi r0, 0x00 + ldi r1, 0x00 + add r0, r1 + """) + helper.assert_register(cpu, 0, 0) + helper.assert_flag(cpu, SREG_Z, True, "Z") - def test_add_basic_and_flags(self): - # 0x7F + 0x01 -> 0x80: tests N,V (overflow), Z, C, H - src = """ - ldi r0, $7F - ldi r1, $01 + @pytest.mark.parametrize( + "a,b,expected,carry", + [ + (0, 0, 0, False), + (1, 1, 2, False), + (128, 128, 0, True), + (255, 1, 0, True), + (127, 1, 128, False), + ], + ) + def test_add_parametrized(self, cpu_with_program, helper, a, b, expected, carry): + """Test ADD with various input combinations.""" + cpu = cpu_with_program(f""" + ldi r0, {a} + ldi r1, {b} add r0, r1 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(0), 0x80) - # N = 1 (bit7 set), V = 1 (signed overflow), S = N ^ V = 0 - self.assertTrue(cpu.get_flag(SREG_N)) - self.assertTrue(cpu.get_flag(SREG_V)) - self.assertFalse(cpu.get_flag(SREG_S)) - # Z = 0, C = 0, H = 1 (carry from bit3) - self.assertFalse(cpu.get_flag(SREG_Z)) - self.assertFalse(cpu.get_flag(SREG_C)) - self.assertTrue(cpu.get_flag(SREG_H)) - - def test_add_with_carry(self): - # 0xFF + 0x01 -> 0x00 with carry - src = """ - ldi r2, $FF - ldi r3, $01 - add r2, r3 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(2), 0x00) - self.assertTrue(cpu.get_flag(SREG_Z)) - self.assertTrue(cpu.get_flag(SREG_C)) - - def test_adc_uses_carry_in(self): - # ADC should include carry in - src = """ - ldi r4, $FF - ldi r5, $00 - adc r4, r5 - """ - prog, labels = assemble(src) - self.cpu.load_program(prog, labels) - # set carry before run - self.cpu.set_flag(SREG_C, True) - self.cpu.run() - self.assertEqual(self.cpu.read_reg(4), 0x00) - self.assertTrue(self.cpu.get_flag(SREG_C)) - - def test_sub_and_cp_flags(self): - # 0x00 - 0x01 -> 0xFF with borrow - src = """ - ldi r6, $00 - ldi r7, $01 - sub r6, r7 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(6), 0xFF) - self.assertTrue(cpu.get_flag(SREG_C)) - self.assertTrue(cpu.get_flag(SREG_N)) - - def test_inc_overflow_flag(self): - src = """ - ldi r8, $7F - inc r8 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(8), 0x80) - self.assertTrue(cpu.get_flag(SREG_V)) - self.assertTrue(cpu.get_flag(SREG_N)) - - def test_dec_overflow_flag(self): - src = """ - ldi r9, $80 - dec r9 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(9), 0x7F) - self.assertTrue(cpu.get_flag(SREG_V)) - self.assertFalse(cpu.get_flag(SREG_N)) + """) + helper.assert_register(cpu, 0, expected) + helper.assert_flag(cpu, SREG_C, carry, "C") + + +class TestADC: + """Test ADC (Add with Carry) instruction.""" + + def test_adc_with_carry_clear(self, cpu_with_program, helper): + """Test ADC when carry is 0.""" + cpu = cpu_with_program(""" + ldi r0, 10 + ldi r1, 5 + adc r0, r1 + """) + helper.assert_register(cpu, 0, 15) + + def test_adc_with_carry_set(self, cpu, helper): + """Test ADC when carry is 1.""" + asm = assemble(""" + ldi r0, 10 + ldi r1, 5 + adc r0, r1 + """) + cpu.load_program(asm) + cpu.set_flag(SREG_C, True) # Set carry before execution + cpu.run() + helper.assert_register(cpu, 0, 16) # 10 + 5 + 1 + + def test_adc_chain_addition(self, cpu, helper): + """Test multi-byte addition using ADC.""" + asm = assemble(""" + ; Add 0x0201 + 0x0304 = 0x0505 + ldi r0, 0x01 ; Low byte 1 + ldi r1, 0x02 ; High byte 1 + ldi r2, 0x04 ; Low byte 2 + ldi r3, 0x03 ; High byte 2 + add r0, r2 ; Add low bytes + adc r1, r3 ; Add high bytes with carry + """) + cpu.load_program(asm) + cpu.run() + helper.assert_registers(cpu, {0: 0x05, 1: 0x05}) + + +class TestSUB: + """Test SUB and SUBI instructions.""" + + def test_sub_basic(self, cpu_with_program, helper): + """Test basic subtraction.""" + cpu = cpu_with_program(""" + ldi r0, 30 + ldi r1, 10 + sub r0, r1 + """) + helper.assert_register(cpu, 0, 20) + + def test_sub_with_borrow(self, cpu_with_program, helper): + """Test subtraction with borrow (negative result).""" + cpu = cpu_with_program(""" + ldi r0, 5 + ldi r1, 10 + sub r0, r1 + """) + helper.assert_register(cpu, 0, 251) # -5 in two's complement = 0xFB + helper.assert_flag(cpu, SREG_C, True, "C") # Borrow set + helper.assert_flag(cpu, SREG_N, True, "N") # Negative + + def test_subi_immediate(self, cpu_with_program, helper): + """Test SUBI with immediate value.""" + cpu = cpu_with_program(""" + ldi r16, 100 + subi r16, 42 + """) + helper.assert_register(cpu, 16, 58) + + def test_sub_zero_result(self, cpu_with_program, helper): + """Test subtraction resulting in zero.""" + cpu = cpu_with_program(""" + ldi r0, 42 + ldi r1, 42 + sub r0, r1 + """) + helper.assert_register(cpu, 0, 0) + helper.assert_flag(cpu, SREG_Z, True, "Z") + + +class TestSBC: + """Test SBC (Subtract with Carry) instruction.""" + + def test_sbc_with_carry_clear(self, cpu_with_program, helper): + """Test SBC when carry/borrow is 0.""" + cpu = cpu_with_program(""" + ldi r0, 20 + ldi r1, 5 + sbc r0, r1 + """) + helper.assert_register(cpu, 0, 15) + + def test_sbc_with_borrow_set(self, cpu, helper): + """Test SBC when carry/borrow is 1.""" + asm = assemble(""" + ldi r0, 20 + ldi r1, 5 + sbc r0, r1 + """) + cpu.load_program(asm) + cpu.set_flag(SREG_C, True) # Set borrow + cpu.run() + helper.assert_register(cpu, 0, 14) # 20 - 5 - 1 + + def test_sbci_immediate(self, cpu, helper): + """Test SBCI with immediate value.""" + asm = assemble(""" + ldi r16, 100 + sbci r16, 10 + """) + cpu.load_program(asm) + cpu.set_flag(SREG_C, True) + cpu.run() + helper.assert_register(cpu, 16, 89) # 100 - 10 - 1 + + +class TestINCDEC: + """Test INC and DEC instructions.""" + + def test_inc_basic(self, cpu_with_program, helper): + """Test basic increment.""" + cpu = cpu_with_program(""" + ldi r0, 42 + inc r0 + """) + helper.assert_register(cpu, 0, 43) + + def test_inc_overflow(self, cpu_with_program, helper): + """Test increment overflow (0xFF -> 0x00).""" + cpu = cpu_with_program(""" + ldi r0, 0xFF + inc r0 + """) + helper.assert_register(cpu, 0, 0) + helper.assert_flag(cpu, SREG_Z, True, "Z") + + def test_inc_signed_overflow(self, cpu_with_program, helper): + """Test increment with signed overflow (0x7F -> 0x80).""" + cpu = cpu_with_program(""" + ldi r0, 0x7F + inc r0 + """) + helper.assert_register(cpu, 0, 0x80) + helper.assert_flag(cpu, SREG_V, True, "V") # Signed overflow + helper.assert_flag(cpu, SREG_N, True, "N") # Negative + + def test_dec_basic(self, cpu_with_program, helper): + """Test basic decrement.""" + cpu = cpu_with_program(""" + ldi r0, 42 + dec r0 + """) + helper.assert_register(cpu, 0, 41) + + def test_dec_underflow(self, cpu_with_program, helper): + """Test decrement underflow (0x00 -> 0xFF).""" + cpu = cpu_with_program(""" + ldi r0, 0 + dec r0 + """) + helper.assert_register(cpu, 0, 0xFF) + helper.assert_flag(cpu, SREG_N, True, "N") + + @pytest.mark.parametrize( + "value,inc_result,dec_result", + [ + (0, 1, 255), + (1, 2, 0), + (127, 128, 126), + (128, 129, 127), + (255, 0, 254), + ], + ) + def test_inc_dec_parametrized( + self, cpu_with_program, helper, value, inc_result, dec_result + ): + """Test INC and DEC with various values.""" + cpu = cpu_with_program(f""" + ldi r0, {value} + ldi r1, {value} + inc r0 + dec r1 + """) + helper.assert_register(cpu, 0, inc_result) + helper.assert_register(cpu, 1, dec_result) + + +class TestMUL: + """Test MUL (Multiply) instruction.""" + + def test_mul_basic(self, cpu_with_program, helper): + """Test basic multiplication.""" + cpu = cpu_with_program(""" + ldi r0, 6 + ldi r1, 7 + mul r0, r1 + """) + helper.assert_register(cpu, 0, 42) # Low byte + helper.assert_register(cpu, 1, 0) # High byte + + def test_mul_with_overflow(self, cpu_with_program, helper): + """Test multiplication producing 16-bit result.""" + cpu = cpu_with_program(""" + ldi r2, 200 + ldi r3, 100 + mul r2, r3 + """) + # 200 * 100 = 20000 = 0x4E20 + helper.assert_register(cpu, 2, 0x20) # Low byte + helper.assert_register(cpu, 3, 0x4E) # High byte + + def test_mul_zero(self, cpu_with_program, helper): + """Test multiplication by zero.""" + cpu = cpu_with_program(""" + ldi r4, 42 + ldi r5, 0 + mul r4, r5 + """) + helper.assert_register(cpu, 4, 0) + helper.assert_register(cpu, 5, 0) + helper.assert_flag(cpu, SREG_Z, True, "Z") + + @pytest.mark.parametrize( + "a,b,low,high", + [ + (1, 1, 1, 0), + (15, 15, 225, 0), + (16, 16, 0, 1), + (255, 255, 1, 254), + (128, 2, 0, 1), + ], + ) + def test_mul_parametrized(self, cpu_with_program, helper, a, b, low, high): + """Test MUL with various input combinations.""" + cpu = cpu_with_program(f""" + ldi r0, {a} + ldi r1, {b} + mul r0, r1 + """) + helper.assert_register(cpu, 0, low) + helper.assert_register(cpu, 1, high) + + +class TestDIV: + """Test DIV (Division) instruction.""" + + def test_div_basic(self, cpu_with_program, helper): + """Test basic division.""" + cpu = cpu_with_program(""" + ldi r0, 42 + ldi r1, 7 + div r0, r1 + """) + helper.assert_register(cpu, 0, 6) # Quotient + helper.assert_register(cpu, 1, 0) # Remainder + + def test_div_with_remainder(self, cpu_with_program, helper): + """Test division with remainder.""" + cpu = cpu_with_program(""" + ldi r2, 100 + ldi r3, 7 + div r2, r3 + """) + helper.assert_register(cpu, 2, 14) # 100 // 7 = 14 + helper.assert_register(cpu, 3, 2) # 100 % 7 = 2 + + def test_div_by_one(self, cpu_with_program, helper): + """Test division by 1.""" + cpu = cpu_with_program(""" + ldi r4, 123 + ldi r5, 1 + div r4, r5 + """) + helper.assert_register(cpu, 4, 123) + helper.assert_register(cpu, 5, 0) + + @pytest.mark.parametrize( + "dividend,divisor,quotient,remainder", + [ + (10, 3, 3, 1), + (20, 4, 5, 0), + (255, 16, 15, 15), + (100, 100, 1, 0), + (7, 10, 0, 7), + ], + ) + def test_div_parametrized( + self, cpu_with_program, helper, dividend, divisor, quotient, remainder + ): + """Test DIV with various input combinations.""" + cpu = cpu_with_program(f""" + ldi r0, {dividend} + ldi r1, {divisor} + div r0, r1 + """) + helper.assert_register(cpu, 0, quotient) + helper.assert_register(cpu, 1, remainder) + + +class TestNEG: + """Test NEG (Two's complement negation) instruction.""" + + def test_neg_positive(self, cpu_with_program, helper): + """Test negation of positive number.""" + cpu = cpu_with_program(""" + ldi r0, 42 + neg r0 + """) + helper.assert_register(cpu, 0, 214) # -42 in two's complement + helper.assert_flag(cpu, SREG_N, True, "N") + + def test_neg_zero(self, cpu_with_program, helper): + """Test negation of zero.""" + cpu = cpu_with_program(""" + ldi r0, 0 + neg r0 + """) + helper.assert_register(cpu, 0, 0) + helper.assert_flag(cpu, SREG_Z, True, "Z") + + def test_neg_max_negative(self, cpu_with_program, helper): + """Test negation of 0x80 (most negative number).""" + cpu = cpu_with_program(""" + ldi r0, 0x80 + neg r0 + """) + helper.assert_register(cpu, 0, 0x80) + helper.assert_flag(cpu, SREG_V, True, "V") # Overflow + + +class TestWordOperations: + """Test ADIW and SBIW (16-bit word operations).""" + + def test_adiw_basic(self, cpu_with_program, helper): + """Test add immediate to word.""" + cpu = cpu_with_program(""" + ldi r24, 0x00 + ldi r25, 0x01 + adiw r24, 1 + """) + helper.assert_register(cpu, 24, 0x01) + helper.assert_register(cpu, 25, 0x01) + + def test_sbiw_basic(self, cpu_with_program, helper): + """Test subtract immediate from word.""" + cpu = cpu_with_program(""" + ldi r24, 0x05 + ldi r25, 0x00 + sbiw r24, 3 + """) + helper.assert_register(cpu, 24, 0x02) + helper.assert_register(cpu, 25, 0x00) diff --git a/tests/test_control_flow.py b/tests/test_control_flow.py index 0af5d67..f4568c3 100644 --- a/tests/test_control_flow.py +++ b/tests/test_control_flow.py @@ -1,172 +1,473 @@ -import unittest +"""Test suite for control flow instructions. -from tiny8 import CPU, assemble -from tiny8.cpu import SREG_C, SREG_I, SREG_Z +Covers: JMP, RJMP, CALL, RCALL, RET, RETI, BR* (branches), CP, CPI, CPSE +""" +import pytest -class TestControlFlow(unittest.TestCase): - def setUp(self): - self.cpu = CPU() +from tiny8.cpu import SREG_C, SREG_Z - def run_asm(self, src: str): - prog, labels = assemble(src) - self.cpu.load_program(prog, labels) - self.cpu.run() - return self.cpu - def test_branch_on_zero_and_carry(self): - src = """ - ldi r0, 1 - ldi r1, 1 - sub r0, r1 - breq is_zero +class TestJMP: + """Test JMP and RJMP (Jump) instructions.""" + + def test_jmp_forward(self, cpu_with_program, helper): + """Test jumping forward to a label.""" + cpu = cpu_with_program(""" + ldi r0, 0 + jmp skip + ldi r0, 99 + skip: + ldi r0, 42 + """) + helper.assert_register(cpu, 0, 42) + + def test_jmp_backward_loop(self, cpu_with_program, helper): + """Test backward jump creating a loop.""" + cpu = cpu_with_program(""" + ldi r0, 0 + ldi r1, 5 + loop: + inc r0 + dec r1 + brne loop + """) + helper.assert_register(cpu, 0, 5) + + def test_conditional_jump_pattern(self, cpu_with_program, helper): + """Test conditional execution with jumps.""" + cpu = cpu_with_program(""" + ldi r0, 10 + ldi r1, 10 + cp r0, r1 + brne not_equal ldi r2, 1 jmp done - is_zero: - ldi r2, 2 + not_equal: + ldi r2, 0 done: nop - """ - cpu = self.run_asm(src) - self.assertTrue(cpu.get_flag(SREG_Z)) - self.assertEqual(cpu.read_reg(2), 2) - - def test_brcc_brcs(self): - # Build a small program where we set the carry then immediately hit BRCS - prog, labels = assemble(""" - ldi r3, 0 - ldi r4, 0 - ; placeholder - """) - self.cpu.load_program(prog, labels) - # set carry immediately before inserting the branch - self.cpu.set_flag(SREG_C, True) - # append a branch instruction which should be taken when C==1 - self.cpu.program.append(("brcs", ("label1",))) - # destination: skip the following ldi if branch taken - self.cpu.labels["label1"] = len(self.cpu.program) + 1 - self.cpu.program.append(("ldi", (("reg", 5), 9))) - self.cpu.run() - # If branch taken, r5 should not have been set to 9 - self.assertNotEqual(self.cpu.read_reg(5), 9) - - def test_sei_cli(self): - cpu = CPU() - prog, labels = assemble(""" - sei - cli - """) - cpu.load_program(prog, labels) - cpu.run() - # after sei then cli the I bit should be cleared - self.assertFalse(cpu.get_flag(SREG_I)) - - def test_sbci_behaviour(self): - # show that SBCI subtracts immediate and includes carry - cpu = CPU() - prog, labels = assemble(""" - ldi r0, $00 - ldi r1, $02 - ldi r2, $01 - ; set carry to 1 - ldi r3, $01 - ; simulate carry set - """) - # initialize r1 and set carry - cpu.write_reg(1, 2) - cpu.set_flag(SREG_C, True) - # perform sbci on r1 (2 - 1 - carry(1) = 0) - prog2, labels2 = assemble(""" - sbci r1, $01 - """) - cpu.load_program(prog2, labels2) - cpu.run() - self.assertEqual(cpu.read_reg(1), 0x00) - self.assertTrue(cpu.get_flag(SREG_Z)) - - def test_cpse_skips_next(self): - cpu = CPU() - prog, labels = assemble(""" - ldi r4, $05 - ldi r5, $05 - cpse r4, r5 - ldi r6, $AA - ldi r7, $BB - """) - cpu.load_program(prog, labels) - cpu.run() - # If cpse skipped the next instruction, r6 should remain zero and r7 should be loaded - self.assertEqual(cpu.read_reg(6), 0x00) - self.assertEqual(cpu.read_reg(7), 0xBB) - - def test_brlt(self): - # BRLT: branch when signed (Rd < Rr). Use -1 (0xFF) < 1 (0x01) - src = """ - ldi r0, $FF ; -1 - ldi r1, $01 ; 1 + """) + helper.assert_register(cpu, 2, 1) # Should take equal branch + + +class TestBranches: + """Test conditional branch instructions.""" + + def test_brne_branch_taken(self, cpu_with_program, helper): + """Test BRNE when Z=0 (not equal).""" + cpu = cpu_with_program(""" + ldi r0, 5 + ldi r1, 10 + cp r0, r1 + brne taken + ldi r2, 0 + jmp done + taken: + ldi r2, 1 + done: + nop + """) + helper.assert_register(cpu, 2, 1) + + def test_brne_branch_not_taken(self, cpu_with_program, helper): + """Test BRNE when Z=1 (equal).""" + cpu = cpu_with_program(""" + ldi r0, 10 + ldi r1, 10 + cp r0, r1 + brne taken + ldi r2, 0 + jmp done + taken: + ldi r2, 1 + done: + nop + """) + helper.assert_register(cpu, 2, 0) + + def test_breq_branch_taken(self, cpu_with_program, helper): + """Test BREQ when Z=1 (equal).""" + cpu = cpu_with_program(""" + ldi r0, 42 + ldi r1, 42 + cp r0, r1 + breq equal + ldi r2, 0 + jmp done + equal: + ldi r2, 1 + done: + nop + """) + helper.assert_register(cpu, 2, 1) + + def test_brcs_carry_set(self, cpu_with_program, helper): + """Test BRCS when carry flag is set.""" + cpu = cpu_with_program(""" + ldi r0, 5 + ldi r1, 10 + cp r0, r1 + brcs carry_set + ldi r2, 0 + jmp done + carry_set: + ldi r2, 1 + done: + nop + """) + helper.assert_register(cpu, 2, 1) + + def test_brcc_carry_clear(self, cpu_with_program, helper): + """Test BRCC when carry flag is clear.""" + cpu = cpu_with_program(""" + ldi r0, 10 + ldi r1, 5 + cp r0, r1 + brcc carry_clear + ldi r2, 0 + jmp done + carry_clear: + ldi r2, 1 + done: + nop + """) + helper.assert_register(cpu, 2, 1) + + def test_brge_greater_equal(self, cpu_with_program, helper): + """Test BRGE (branch if greater or equal, signed).""" + cpu = cpu_with_program(""" + ldi r0, 10 + ldi r1, 5 + cp r0, r1 + brge greater + ldi r2, 0 + jmp done + greater: + ldi r2, 1 + done: + nop + """) + helper.assert_register(cpu, 2, 1) + + def test_brlt_less_than(self, cpu_with_program, helper): + """Test BRLT (branch if less than, signed).""" + cpu = cpu_with_program(""" + ldi r0, 5 + ldi r1, 10 cp r0, r1 brlt less - ldi r2, $01 + ldi r2, 0 jmp done less: - ldi r2, $02 + ldi r2, 1 done: nop - """ - cpu = self.run_asm(src) - # -1 < 1 so branch taken -> r2 == 2 - self.assertEqual(cpu.read_reg(2), 0x02) + """) + helper.assert_register(cpu, 2, 1) - def test_brge(self): - # BRGE: branch when signed (Rd >= Rr). Use 1 >= -1 - src2 = """ - ldi r0, $01 ; 1 - ldi r1, $FF ; -1 + @pytest.mark.parametrize( + "a,b,branch,expected", + [ + (10, 10, "breq", 1), # Equal + (10, 5, "brne", 1), # Not equal + (5, 10, "brcs", 1), # Carry set (a < b) + (10, 5, "brcc", 1), # Carry clear (a >= b) + ], + ) + def test_branches_parametrized( + self, cpu_with_program, helper, a, b, branch, expected + ): + """Test various branch conditions.""" + cpu = cpu_with_program(f""" + ldi r0, {a} + ldi r1, {b} cp r0, r1 - brge ge - ldi r2, $01 + {branch} taken + ldi r2, 0 jmp done - ge: - ldi r2, $02 + taken: + ldi r2, 1 done: nop - """ - cpu2 = self.run_asm(src2) - # 1 >= -1 so branch taken -> r2 == 2 - self.assertEqual(cpu2.read_reg(2), 0x02) + """) + helper.assert_register(cpu, 2, expected) - def test_brmi(self): - # BRMI: branch when negative (N == 1) - src = """ - ldi r0, $FF ; -1 - ldi r1, $00 ; 0 - cp r0, r1 - brmi neg - ldi r2, $01 + +class TestCALL: + """Test CALL and RET (Subroutine call/return) instructions.""" + + def test_call_ret_basic(self, cpu_with_program, helper): + """Test basic subroutine call and return.""" + cpu = cpu_with_program(""" + ldi r0, 0 + call increment + jmp done + increment: + inc r0 + ret + done: + nop + """) + helper.assert_register(cpu, 0, 1) + + def test_call_with_parameters(self, cpu_with_program, helper): + """Test subroutine with register parameters.""" + cpu = cpu_with_program(""" + ldi r0, 10 + ldi r1, 20 + call add_nums + jmp done + add_nums: + add r0, r1 + ret + done: + nop + """) + helper.assert_register(cpu, 0, 30) + + def test_nested_calls(self, cpu_with_program, helper): + """Test nested subroutine calls.""" + cpu = cpu_with_program(""" + ldi r0, 5 + call double + jmp done + double: + call add_self + ret + add_self: + add r0, r0 + ret + done: + nop + """) + helper.assert_register(cpu, 0, 10) + + def test_call_preserves_registers(self, cpu_with_program, helper): + """Test that subroutine can preserve registers via stack.""" + cpu = cpu_with_program(""" + ldi r0, 100 + ldi r1, 200 + call modify_r0 jmp done - neg: - ldi r2, $02 + modify_r0: + push r1 + ldi r1, 50 + add r0, r1 + pop r1 + ret done: nop - """ - cpu = self.run_asm(src) - # -1 < 0 so negative -> branch taken -> r2 == 2 - self.assertEqual(cpu.read_reg(2), 0x02) + """) + # 100 + 50 = 150, r1 should be preserved as 200 + helper.assert_registers(cpu, {0: 150, 1: 200}) + - def test_brpl(self): - # BRPL: branch when plus (N == 0) - src2 = """ - ldi r0, $02 ; 2 - ldi r1, $FF ; -1 +class TestCompare: + """Test CP, CPC, CPI (Compare) instructions.""" + + def test_cp_equal(self, cpu_with_program, helper): + """Test CP when values are equal.""" + cpu = cpu_with_program(""" + ldi r0, 42 + ldi r1, 42 + cp r0, r1 + """) + helper.assert_flag(cpu, SREG_Z, True, "Z") + helper.assert_flag(cpu, SREG_C, False, "C") + + def test_cp_less_than(self, cpu_with_program, helper): + """Test CP when first < second.""" + cpu = cpu_with_program(""" + ldi r0, 10 + ldi r1, 20 cp r0, r1 - brpl plus - ldi r2, $01 - jmp done2 - plus: - ldi r2, $02 - done2: - nop - """ - cpu2 = self.run_asm(src2) - # 2 >= -1 so non-negative -> branch taken -> r2 == 2 - self.assertEqual(cpu2.read_reg(2), 0x02) + """) + helper.assert_flag(cpu, SREG_C, True, "C") # Borrow set + helper.assert_flag(cpu, SREG_Z, False, "Z") + + def test_cp_greater_than(self, cpu_with_program, helper): + """Test CP when first > second.""" + cpu = cpu_with_program(""" + ldi r0, 20 + ldi r1, 10 + cp r0, r1 + """) + helper.assert_flag(cpu, SREG_C, False, "C") + helper.assert_flag(cpu, SREG_Z, False, "Z") + + def test_cpi_immediate(self, cpu_with_program, helper): + """Test CPI with immediate value.""" + cpu = cpu_with_program(""" + ldi r16, 100 + cpi r16, 100 + """) + helper.assert_flag(cpu, SREG_Z, True, "Z") + + def test_cpse_skip_if_equal(self, cpu_with_program, helper): + """Test CPSE skips next instruction when equal.""" + cpu = cpu_with_program(""" + ldi r0, 42 + ldi r1, 42 + ldi r2, 0 + cpse r0, r1 + ldi r2, 99 + """) + helper.assert_register(cpu, 2, 0) # LDI should be skipped + + def test_cpse_no_skip_if_not_equal(self, cpu_with_program, helper): + """Test CPSE doesn't skip when not equal.""" + cpu = cpu_with_program(""" + ldi r0, 42 + ldi r1, 43 + ldi r2, 0 + cpse r0, r1 + ldi r2, 99 + """) + helper.assert_register(cpu, 2, 99) # LDI should execute + + +class TestLoops: + """Integration tests for loop patterns.""" + + def test_count_up_loop(self, cpu_with_program, helper): + """Test counting up with loop.""" + cpu = cpu_with_program(""" + ldi r0, 0 + ldi r1, 0 + ldi r2, 10 + loop: + inc r0 + inc r1 + cp r1, r2 + brne loop + """) + helper.assert_register(cpu, 0, 10) + helper.assert_register(cpu, 1, 10) + + def test_count_down_loop(self, cpu_with_program, helper): + """Test counting down with loop.""" + cpu = cpu_with_program(""" + ldi r0, 0 + ldi r1, 10 + loop: + inc r0 + dec r1 + brne loop + """) + helper.assert_register(cpu, 0, 10) + helper.assert_register(cpu, 1, 0) + + def test_nested_loops(self, cpu_with_program, helper): + """Test nested loop structure.""" + cpu = cpu_with_program(""" + ldi r0, 0 + ldi r1, 3 + outer: + ldi r2, 2 + inner: + inc r0 + dec r2 + brne inner + dec r1 + brne outer + """) + helper.assert_register(cpu, 0, 6) # 3 * 2 + + def test_while_loop_pattern(self, cpu_with_program, helper): + """Test while loop pattern with condition at start.""" + cpu = cpu_with_program(""" + ldi r0, 0 + ldi r1, 5 + loop: + cp r1, r0 + breq done + inc r0 + jmp loop + done: + nop + """) + helper.assert_register(cpu, 0, 5) + + @pytest.mark.parametrize("count,expected", [(1, 1), (5, 5), (10, 10), (20, 20)]) + def test_loop_iterations_parametrized( + self, cpu_with_program, helper, count, expected + ): + """Test loop with different iteration counts.""" + cpu = cpu_with_program(f""" + ldi r0, 0 + ldi r1, {count} + loop: + inc r0 + dec r1 + brne loop + """) + helper.assert_register(cpu, 0, expected) + + +class TestControlFlowIntegration: + """Complex integration tests for control flow.""" + + def test_if_else_pattern(self, cpu_with_program, helper): + """Test if-else control structure.""" + cpu = cpu_with_program(""" + ldi r0, 10 + ldi r1, 5 + ldi r2, 0 + cp r0, r1 + brcs else_branch + ldi r2, 1 + jmp endif + else_branch: + ldi r2, 2 + endif: + nop + """) + helper.assert_register(cpu, 2, 1) + + def test_switch_case_pattern(self, cpu_with_program, helper): + """Test switch-case like pattern.""" + cpu = cpu_with_program(""" + ldi r0, 2 + ldi r1, 0 + cpi r0, 1 + breq case1 + cpi r0, 2 + breq case2 + cpi r0, 3 + breq case3 + jmp default + case1: + ldi r1, 10 + jmp endswitch + case2: + ldi r1, 20 + jmp endswitch + case3: + ldi r1, 30 + jmp endswitch + default: + ldi r1, 99 + endswitch: + nop + """) + helper.assert_register(cpu, 1, 20) + + def test_function_with_early_return(self, cpu_with_program, helper): + """Test function with multiple return points.""" + cpu = cpu_with_program(""" + ldi r0, 0 + call check_value + jmp done + check_value: + cpi r0, 0 + brne not_zero + ldi r1, 100 + ret + not_zero: + ldi r1, 200 + ret + done: + nop + """) + helper.assert_register(cpu, 1, 100) diff --git a/tests/test_cpu.py b/tests/test_cpu.py deleted file mode 100644 index c23671a..0000000 --- a/tests/test_cpu.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Unit tests for the tiny8 CPU and instruction implementations. - -Tests are small and self-contained; they exercise arithmetic, logical, -memory and control-flow instructions implemented by the CPU. -""" - -import unittest - -from tiny8 import CPU, assemble, assemble_file - - -class Tiny8TestCase(unittest.TestCase): - """Base test case providing helpers and a fresh CPU for each test.""" - - def setUp(self) -> None: - self.cpu = CPU() - - def run_asm(self, src: str = None, path: str = None, max_cycles: int = 1000): - """Assemble the provided source or file, load into CPU and run. - - Returns the CPU instance for assertions. - """ - if path: - prog, labels = assemble_file(path) - else: - prog, labels = assemble(src) - self.cpu.load_program(prog, labels) - self.cpu.run(max_cycles=max_cycles) - return self.cpu - - -class TestCPU(Tiny8TestCase): - def test_ldi_add_mul_div_and_logic(self): - # verify multiple arithmetic/logical operations in one compact test - src = """ - ldi r0, 10 - ldi r1, 20 - add r0, r1 - ldi r2, 6 - ldi r3, 7 - mul r2, r3 - ldi r4, 20 - ldi r5, 3 - div r4, r5 - ldi r6, $0F - ldi r7, $F0 - and r6, r7 - ldi r8, $AA - ldi r9, $55 - eor r8, r9 - ldi r10, 0 - ldi r11, 1 - or r10, r11 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(0), 30) # 10 + 20 - self.assertEqual(cpu.read_reg(2), 42) # 6 * 7 low byte - self.assertEqual(cpu.read_reg(4), 6) # 20 // 3 - self.assertEqual(cpu.read_reg(5), 2) # remainder in r5 - self.assertEqual(cpu.read_reg(6), 0x00) - self.assertEqual(cpu.read_reg(8), 0xFF) - self.assertEqual(cpu.read_reg(10), 1) - - def test_inc_dec_shift(self): - src = """ - ldi r12, 1 - inc r12 - ldi r13, 4 - lsl r13 - ldi r14, 8 - lsr r14 - ldi r15, 0 - dec r15 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(12), 2) - self.assertEqual(cpu.read_reg(13), 8) - self.assertEqual(cpu.read_reg(14), 4) - self.assertEqual(cpu.read_reg(15), 255) - - def test_sbi_cbi_and_io(self): - src = """ - ldi r0, 0 - out 100, r0 - sbi 100, 2 - cbi 100, 2 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_ram(100), 0) - - def test_fib_no_overflow(self): - # Load the fibonacci program but don't run so we can set the input first - prog, labels = assemble_file("examples/fibonacci.asm") - self.cpu.load_program(prog, labels) - # compute fib(7) => 13 - self.cpu.write_reg(17, 7) - self.cpu.run(max_cycles=200) - self.assertEqual(self.cpu.read_reg(16), 13) - - -class TestInstructions(Tiny8TestCase): - def test_adc_with_carry(self): - src = """ - ldi r0, 200 - ldi r1, 100 - adc r0, r1 - """ - # assemble/load but set carry flag before executing ADC - prog, labels = assemble(src) - self.cpu.load_program(prog, labels) - self.cpu.set_flag(0, True) - self.cpu.run() - self.assertEqual(self.cpu.read_reg(0), 301 & 0xFF) - self.assertTrue(self.cpu.get_flag(0)) - - def test_cp_and_branch(self): - src = """ - ldi r0, 5 - ldi r1, 5 - cp r0, r1 - breq equal - ldi r2, 1 - jmp done - equal: - ldi r2, 2 - done: - nop - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(2), 2) - - def test_push_pop_and_call(self): - src = """ - ldi r3, 55 - push r3 - ldi r3, 0 - pop r4 - call func - jmp done - func: - ldi r5, 9 - ret - done: - nop - """ - cpu = self.run_asm(src, max_cycles=50) - self.assertEqual(cpu.read_reg(4), 55) - self.assertEqual(cpu.read_reg(5), 9) - - def test_ld_st_and_rotations(self): - src = """ - ldi r8, 150 ; address - ldi r9, 77 ; value - st r8, r9 - ldi r10, 150 - ld r11, r10 - ldi r12, 0b10000000 - ldi r13, 1 - lsl r12 - lsr r13 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(11), 77) - self.assertEqual(cpu.read_reg(12), 0) - self.assertTrue(cpu.get_flag(0)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_data_transfer.py b/tests/test_data_transfer.py index 05dab73..ce4e8c7 100644 --- a/tests/test_data_transfer.py +++ b/tests/test_data_transfer.py @@ -1,50 +1,283 @@ -import unittest +"""Test suite for data transfer instructions. -from tiny8 import CPU, assemble +Covers: LDI, MOV, LD, ST, IN, OUT, PUSH, POP, LDS, STS +""" +import pytest -class TestDataTransfer(unittest.TestCase): - def setUp(self): - self.cpu = CPU() - def run_asm(self, src: str): - prog, labels = assemble(src) - self.cpu.load_program(prog, labels) - self.cpu.run() - return self.cpu +class TestLDI: + """Test LDI (Load Immediate) instruction.""" - def test_ldi_mov_ld_st(self): - src = """ + def test_ldi_basic(self, cpu_with_program, helper): + """Test loading immediate values into registers.""" + cpu = cpu_with_program(""" + ldi r16, 42 + ldi r17, 0xFF + ldi r18, 0x00 + """) + helper.assert_registers(cpu, {16: 42, 17: 255, 18: 0}) + + def test_ldi_hex_binary_formats(self, cpu_with_program, helper): + """Test different number formats in LDI.""" + cpu = cpu_with_program(""" + ldi r20, $FF + ldi r21, 0xFF + ldi r22, 0b11111111 + ldi r23, 255 + """) + for reg in [20, 21, 22, 23]: + helper.assert_register(cpu, reg, 255) + + @pytest.mark.parametrize( + "reg,value", [(16, 0), (17, 1), (18, 127), (19, 128), (20, 255)] + ) + def test_ldi_parametrized(self, cpu_with_program, helper, reg, value): + """Test LDI with various registers and values.""" + cpu = cpu_with_program(f""" + ldi r{reg}, {value} + """) + helper.assert_register(cpu, reg, value) + + +class TestMOV: + """Test MOV (Copy register) instruction.""" + + def test_mov_basic(self, cpu_with_program, helper): + """Test copying value between registers.""" + cpu = cpu_with_program(""" ldi r0, 123 - ldi r1, 5 - mov r2, r0 - ldi r3, 10 - st r3, r2 - ldi r4, 10 - ld r5, r4 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(0), 123) - self.assertEqual(cpu.read_reg(2), 123) - self.assertEqual(cpu.read_ram(10), 123) - self.assertEqual(cpu.read_reg(5), 123) - - def test_push_pop_call_ret(self): - src = """ - ldi r6, 77 - push r6 - ldi r6, 0 - pop r7 - call fn - jmp done - fn: - ldi r8, 9 - ret - done: - nop - """ - cpu = self.run_asm( - src, + mov r1, r0 + """) + helper.assert_register(cpu, 1, 123) + + def test_mov_chain(self, cpu_with_program, helper): + """Test chaining MOV operations.""" + cpu = cpu_with_program(""" + ldi r0, 42 + mov r1, r0 + mov r2, r1 + mov r3, r2 + """) + for reg in [0, 1, 2, 3]: + helper.assert_register(cpu, reg, 42) + + def test_mov_preserves_source(self, cpu_with_program, helper): + """Test that MOV doesn't modify source register.""" + cpu = cpu_with_program(""" + ldi r0, 100 + mov r1, r0 + ldi r2, 200 + mov r3, r2 + """) + helper.assert_registers(cpu, {0: 100, 1: 100, 2: 200, 3: 200}) + + +class TestLDST: + """Test LD (Load from RAM) and ST (Store to RAM) instructions.""" + + def test_st_ld_basic(self, cpu_with_program, helper): + """Test storing and loading from memory.""" + cpu = cpu_with_program(""" + ldi r16, 100 ; Address + ldi r17, 42 ; Value + st r16, r17 ; Store value to MEM[100] + ldi r18, 0 ; Clear r18 + ld r18, r16 ; Load from MEM[100] + """) + helper.assert_register(cpu, 18, 42) + helper.assert_memory(cpu, 100, 42) + + def test_st_multiple_addresses(self, cpu_with_program, helper): + """Test storing to multiple memory locations.""" + cpu = cpu_with_program(""" + ldi r20, 100 + ldi r21, 11 + st r20, r21 + + ldi r20, 101 + ldi r21, 22 + st r20, r21 + + ldi r20, 102 + ldi r21, 33 + st r20, r21 + """) + helper.assert_memory(cpu, 100, 11) + helper.assert_memory(cpu, 101, 22) + helper.assert_memory(cpu, 102, 33) + + def test_ld_uninitialized_memory(self, cpu_with_program, helper): + """Test loading from uninitialized memory returns 0.""" + cpu = cpu_with_program(""" + ldi r16, 200 + ld r17, r16 + """) + helper.assert_register(cpu, 17, 0) + + def test_memory_array_operations(self, cpu_with_program, helper): + """Test writing and reading array in memory.""" + cpu = cpu_with_program(""" + ; Write array [10, 20, 30, 40, 50] + ldi r0, 100 + ldi r1, 10 + st r0, r1 + inc r0 + ldi r1, 20 + st r0, r1 + inc r0 + ldi r1, 30 + st r0, r1 + inc r0 + ldi r1, 40 + st r0, r1 + inc r0 + ldi r1, 50 + st r0, r1 + """) + for i, value in enumerate([10, 20, 30, 40, 50]): + helper.assert_memory(cpu, 100 + i, value) + + +class TestINOUT: + """Test IN (Input from port) and OUT (Output to port) instructions.""" + + def test_out_in_basic(self, cpu_with_program, helper): + """Test writing and reading I/O ports.""" + cpu = cpu_with_program(""" + ldi r16, 123 + out 50, r16 + in r17, 50 + """) + helper.assert_register(cpu, 17, 123) + helper.assert_memory(cpu, 50, 123) + + def test_out_multiple_ports(self, cpu_with_program, helper): + """Test writing to multiple I/O ports.""" + cpu = cpu_with_program(""" + ldi r20, 10 + out 0x10, r20 + ldi r21, 20 + out 0x20, r21 + ldi r22, 30 + out 0x30, r22 + """) + helper.assert_memory(cpu, 0x10, 10) + helper.assert_memory(cpu, 0x20, 20) + helper.assert_memory(cpu, 0x30, 30) + + +class TestPUSHPOP: + """Test PUSH and POP (Stack operations) instructions.""" + + def test_push_pop_basic(self, cpu_with_program, helper): + """Test basic stack push and pop.""" + cpu = cpu_with_program(""" + ldi r16, 42 + push r16 + ldi r16, 0 + pop r17 + """) + helper.assert_register(cpu, 17, 42) + + def test_push_pop_multiple(self, cpu_with_program, helper): + """Test multiple push/pop operations (LIFO).""" + cpu = cpu_with_program(""" + ldi r16, 10 + ldi r17, 20 + ldi r18, 30 + push r16 + push r17 + push r18 + pop r20 + pop r21 + pop r22 + """) + # LIFO: last in (30), first out + helper.assert_registers(cpu, {20: 30, 21: 20, 22: 10}) + + def test_stack_preserves_registers(self, cpu_with_program, helper): + """Test stack for temporary register storage.""" + cpu = cpu_with_program(""" + ldi r0, 100 + ldi r1, 200 + push r0 + push r1 + ; Modify registers + ldi r0, 999 + ldi r1, 888 + ; Restore from stack + pop r1 + pop r0 + """) + helper.assert_registers(cpu, {0: 100, 1: 200}) + + @pytest.mark.parametrize( + "values", + [ + [1, 2, 3], + [10, 20, 30, 40, 50], + [255, 0, 128, 64], + ], + ) + def test_push_pop_sequence(self, cpu_with_program, helper, values): + """Test push/pop with various sequences.""" + push_code = "\n".join( + [f"ldi r{i + 16}, {v}\npush r{i + 16}" for i, v in enumerate(values)] ) - self.assertEqual(cpu.read_reg(7), 77) - self.assertEqual(cpu.read_reg(8), 9) + pop_code = "\n".join([f"pop r{i}" for i in range(len(values))]) + + cpu = cpu_with_program(push_code + "\n" + pop_code) + + # Verify LIFO order + for i, value in enumerate(reversed(values)): + helper.assert_register(cpu, i, value) + + +class TestDataTransferIntegration: + """Integration tests combining multiple data transfer instructions.""" + + def test_copy_memory_block(self, cpu_with_program, helper): + """Test copying a block of memory.""" + cpu = cpu_with_program(""" + ; Initialize source memory [100-102] + ldi r0, 100 + ldi r1, 11 + st r0, r1 + inc r0 + ldi r1, 22 + st r0, r1 + inc r0 + ldi r1, 33 + st r0, r1 + + ; Copy to destination [200-202] + ldi r2, 100 + ldi r3, 200 + ld r4, r2 + st r3, r4 + inc r2 + inc r3 + ld r4, r2 + st r3, r4 + inc r2 + inc r3 + ld r4, r2 + st r3, r4 + """) + for i in range(3): + src_val = cpu.read_ram(100 + i) + dst_val = cpu.read_ram(200 + i) + assert src_val == dst_val, f"Memory copy failed at offset {i}" + + def test_swap_registers_via_stack(self, cpu_with_program, helper): + """Test swapping register values using stack.""" + cpu = cpu_with_program(""" + ldi r16, 42 + ldi r17, 99 + push r16 + push r17 + pop r16 + pop r17 + """) + helper.assert_registers(cpu, {16: 99, 17: 42}) diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..21180b2 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,317 @@ +"""Integration tests for complete programs and examples. + +Tests real-world programs like Fibonacci and bubble sort. +""" + +import pytest + + +class TestBubbleSort: + """Test bubble sort program.""" + + @pytest.mark.slow + def test_bubblesort_example(self, cpu_from_file, helper): + """Test the bubble sort example program.""" + cpu = cpu_from_file("examples/bubblesort.asm", max_steps=15000) + + # Read sorted array from memory + sorted_array = [cpu.read_ram(i) for i in range(100, 132)] + + # Verify array is sorted + for i in range(len(sorted_array) - 1): + assert sorted_array[i] >= sorted_array[i + 1], ( + f"Array not sorted at index {i}: {sorted_array[i]} > {sorted_array[i + 1]}" + ) + + @pytest.mark.slow + def test_bubblesort_deterministic(self, cpu_from_file): + """Test that bubble sort produces consistent results.""" + # Run twice and compare + from tiny8 import CPU, assemble_file + + asm = assemble_file("examples/bubblesort.asm") + + cpu1 = CPU() + cpu1.load_program(asm) + cpu1.run(max_steps=15000) + result1 = [cpu1.read_ram(i) for i in range(100, 132)] + + cpu2 = CPU() + cpu2.load_program(asm) + cpu2.run(max_steps=15000) + result2 = [cpu2.read_ram(i) for i in range(100, 132)] + + # Both should produce identical sorted results + assert result1 == result2, "Bubble sort should be deterministic" + + +class TestCPUState: + """Test CPU state management and inspection.""" + + def test_register_read_write(self, cpu, helper): + """Test reading and writing registers.""" + # Registers are 8-bit, so values are masked to 0-255 + for i in range(32): + value = (i * 10) & 0xFF + cpu.write_reg(i, value) + helper.assert_register(cpu, i, value) + + def test_memory_read_write(self, cpu, helper): + """Test reading and writing memory.""" + # RAM size is 2048 by default, so test addresses within that range + for addr in [0, 100, 255, 1000, 2047]: + cpu.write_ram(addr, addr & 0xFF) + helper.assert_memory(cpu, addr, addr & 0xFF) + + def test_flag_operations(self, cpu, helper): + """Test flag get/set operations.""" + from tiny8.cpu import SREG_C, SREG_N, SREG_V, SREG_Z + + for flag in [SREG_C, SREG_Z, SREG_N, SREG_V]: + cpu.set_flag(flag, True) + helper.assert_flag(cpu, flag, True) + cpu.set_flag(flag, False) + helper.assert_flag(cpu, flag, False) + + def test_pc_sp_operations(self, cpu): + """Test PC and SP get/set operations.""" + # PC and SP are directly accessible attributes + cpu.pc = 0x100 + assert cpu.pc == 0x100 + + cpu.sp = 0x500 + assert cpu.sp == 0x500 + + def test_step_trace(self, cpu_with_program): + """Test that step trace is recorded.""" + cpu = cpu_with_program(""" + ldi r0, 10 + ldi r1, 20 + add r0, r1 + """) + + assert hasattr(cpu, "step_trace"), "CPU should have step_trace" + assert len(cpu.step_trace) > 0, "Step trace should not be empty" + + # Verify trace contains expected keys + for entry in cpu.step_trace: + assert "pc" in entry + assert "sp" in entry + assert "sreg" in entry + assert "regs" in entry + assert "mem" in entry + + +class TestErrorHandling: + """Test error handling and edge cases.""" + + def test_invalid_register_number(self, cpu): + """Test that invalid register access is handled.""" + # Registers are 0-31, accessing outside should fail gracefully + with pytest.raises((IndexError, ValueError)): + cpu.write_reg(32, 100) + + def test_infinite_loop_protection(self, cpu): + """Test that max_steps prevents infinite loops.""" + from tiny8 import assemble + + asm = assemble(""" + loop: + jmp loop + """) + cpu.load_program(asm) + cpu.run(max_steps=100) + + # Should stop after 100 steps + assert len(cpu.step_trace) <= 100 + + def test_empty_program(self, cpu): + """Test running with no program loaded.""" + from tiny8 import assemble + + asm = assemble("") + cpu.load_program(asm) + cpu.run(max_steps=10) + + # Should handle gracefully + assert True + + +class TestAssembler: + """Test assembler features.""" + + def test_assemble_comments(self, cpu_with_program, helper): + """Test that comments are properly ignored.""" + cpu = cpu_with_program(""" + ; This is a comment + ldi r0, 42 ; Inline comment + ldi r1, 99 + ; Another comment + add r0, r1 ; Final comment + """) + helper.assert_register(cpu, 0, 141) + + def test_assemble_labels(self, cpu_with_program, helper): + """Test label resolution.""" + cpu = cpu_with_program(""" + start: + ldi r0, 0 + jmp middle + skip: + ldi r0, 99 + middle: + ldi r0, 42 + """) + helper.assert_register(cpu, 0, 42) + + def test_assemble_number_formats(self, cpu_with_program, helper): + """Test different number format support.""" + cpu = cpu_with_program(""" + ldi r16, 255 ; Decimal + ldi r17, $FF ; Hex $ + ldi r18, 0xFF ; Hex 0x + ldi r19, 0b11111111 ; Binary + """) + for reg in [16, 17, 18, 19]: + helper.assert_register(cpu, reg, 255) + + def test_assemble_case_insensitive(self, cpu_with_program, helper): + """Test that mnemonics are case-insensitive.""" + cpu = cpu_with_program(""" + LDI r0, 10 + ldi r1, 20 + ADD r0, r1 + add r2, r0 + """) + helper.assert_registers(cpu, {0: 30, 1: 20}) + + +class TestPerformance: + """Performance and stress tests.""" + + @pytest.mark.slow + def test_long_execution(self, cpu_with_program, helper): + """Test program with many iterations.""" + cpu = cpu_with_program( + """ + ldi r0, 0 + ldi r1, 100 + loop: + inc r0 + dec r1 + brne loop + """, + max_steps=10000, + ) + helper.assert_register(cpu, 0, 100) + + @pytest.mark.slow + def test_deep_call_stack(self, cpu_with_program, helper): + """Test deep recursion-like call stack.""" + # Create a chain of calls + calls = "\n".join([f"call func{i}" for i in range(10)]) + funcs = "\n".join([f"func{i}:\n nop\n ret" for i in range(10)]) + + cpu_with_program( + f""" + ldi r0, 0 + {calls} + jmp done + {funcs} + done: + nop + """, + max_steps=5000, + ) + # Should complete without stack overflow + assert True + + @pytest.mark.slow + def test_large_memory_operations(self, cpu_with_program, helper): + """Test operations on large memory ranges.""" + # Write to 256 memory locations + writes = "\n".join( + [f"ldi r0, {i}\nldi r1, {i}\nst r0, r1" for i in range(0, 256, 10)] + ) + + cpu = cpu_with_program(writes, max_steps=10000) + + # Verify some values + for i in range(0, 256, 10): + helper.assert_memory(cpu, i, i) + + +@pytest.mark.integration +class TestCompletePrograms: + """Integration tests for complete, realistic programs.""" + + def test_sum_array(self, cpu_with_program, helper): + """Test program that sums an array in memory.""" + cpu = cpu_with_program(""" + ; Initialize array [100-104] = [10, 20, 30, 40, 50] + ldi r0, 100 + ldi r1, 10 + st r0, r1 + inc r0 + ldi r1, 20 + st r0, r1 + inc r0 + ldi r1, 30 + st r0, r1 + inc r0 + ldi r1, 40 + st r0, r1 + inc r0 + ldi r1, 50 + st r0, r1 + + ; Sum array + ldi r2, 0 ; sum + ldi r3, 100 ; address + ldi r4, 5 ; count + loop: + ld r5, r3 + add r2, r5 + inc r3 + dec r4 + brne loop + """) + helper.assert_register(cpu, 2, 150) # 10+20+30+40+50 + + def test_find_maximum(self, cpu_with_program, helper): + """Test program that finds maximum value in array.""" + cpu = cpu_with_program(""" + ; Initialize array + ldi r0, 100 + ldi r1, 25 + st r0, r1 + inc r0 + ldi r1, 75 + st r0, r1 + inc r0 + ldi r1, 42 + st r0, r1 + inc r0 + ldi r1, 99 + st r0, r1 + inc r0 + ldi r1, 10 + st r0, r1 + + ; Find max + ldi r2, 100 ; address + ld r3, r2 ; max = first element + ldi r4, 4 ; remaining count + loop: + inc r2 + ld r5, r2 + cp r3, r5 + brcs update_max + jmp check_done + update_max: + mov r3, r5 + check_done: + dec r4 + brne loop + """) + helper.assert_register(cpu, 3, 99) # Maximum value diff --git a/tests/test_logic_and_bit.py b/tests/test_logic_and_bit.py deleted file mode 100644 index a4927ac..0000000 --- a/tests/test_logic_and_bit.py +++ /dev/null @@ -1,56 +0,0 @@ -import unittest - -from tiny8 import CPU, assemble -from tiny8.cpu import SREG_N, SREG_Z - - -class TestLogicAndBit(unittest.TestCase): - def setUp(self): - self.cpu = CPU() - - def run_asm(self, src: str): - prog, labels = assemble(src) - self.cpu.load_program(prog, labels) - self.cpu.run() - return self.cpu - - def test_and_eor_or_flags(self): - # Run AND alone and check flags - src_and = """ - ldi r0, $F0 - ldi r1, $0F - and r0, r1 - """ - cpu = self.run_asm(src_and) - self.assertEqual(cpu.read_reg(0), 0x00) - self.assertTrue(cpu.get_flag(SREG_Z)) - - # Run EOR alone and check N - src_eor = """ - ldi r2, $AA - ldi r3, $55 - eor r2, r3 - """ - cpu = self.run_asm(src_eor) - self.assertEqual(cpu.read_reg(2), 0xFF) - self.assertTrue(cpu.get_flag(SREG_N)) - - # Run OR alone - src_or = """ - ldi r4, $01 - ldi r5, $02 - or r4, r5 - """ - cpu = self.run_asm(src_or) - self.assertEqual(cpu.read_reg(4), 0x03) - - def test_sbi_cbi_behavior(self): - src = """ - ldi r10, 0x00 - out 200, r10 - sbi 200, 3 - sbi 200, 7 - cbi 200, 3 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_ram(200), 0x80) diff --git a/tests/test_mcu_control.py b/tests/test_mcu_control.py deleted file mode 100644 index 4764f21..0000000 --- a/tests/test_mcu_control.py +++ /dev/null @@ -1,30 +0,0 @@ -import unittest - -from tiny8 import CPU, assemble - - -class TestMCUControl(unittest.TestCase): - def setUp(self): - self.cpu = CPU() - - def test_nop_and_ser_clr(self): - prog, labels = assemble(""" - ser r0 - clr r1 - nop - """) - self.cpu.load_program(prog, labels) - self.cpu.run() - self.assertEqual(self.cpu.read_reg(0), 0xFF) - self.assertEqual(self.cpu.read_reg(1), 0x00) - - def test_sbi_cbi(self): - prog, labels = assemble(""" - ldi r0, 0 - out 50, r0 - sbi 50, 1 - cbi 50, 1 - """) - self.cpu.load_program(prog, labels) - self.cpu.run() - self.assertEqual(self.cpu.read_ram(50), 0) diff --git a/tests/test_misc_ops.py b/tests/test_misc_ops.py deleted file mode 100644 index 58f9d30..0000000 --- a/tests/test_misc_ops.py +++ /dev/null @@ -1,238 +0,0 @@ -import unittest - -from tiny8 import CPU, assemble -from tiny8.cpu import SREG_C, SREG_N, SREG_Z - - -class TestMiscOps(unittest.TestCase): - def setUp(self): - self.cpu = CPU() - - def run_asm(self, src: str): - prog, labels = assemble(src) - self.cpu.load_program(prog, labels) - self.cpu.run() - return self.cpu - - def test_com_sets_c_and_flags(self): - src = """ - ldi r0, $00 - com r0 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(0), 0xFF) - self.assertTrue(cpu.get_flag(SREG_C)) - self.assertTrue(cpu.get_flag(SREG_N)) - self.assertFalse(cpu.get_flag(SREG_Z)) - - def test_neg_behaves_like_sub_from_zero(self): - src = """ - ldi r1, $01 - neg r1 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(1), 0xFF) - self.assertTrue(cpu.get_flag(SREG_C)) - - def test_swap_nibbles(self): - src = """ - ldi r2, $AB - swap r2 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(2), 0xBA) - - def test_tst_sets_flags_but_no_store(self): - src = """ - ldi r3, $00 - tst r3 - """ - cpu = self.run_asm(src) - self.assertTrue(cpu.get_flag(SREG_Z)) - - def test_andi_ori_eori(self): - src = """ - ldi r4, $F0 - andi r4, $0F - ldi r5, $0F - ori r5, $F0 - ldi r6, $FF - eori r6, $FF - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(4), 0x00) - self.assertTrue(cpu.get_flag(SREG_Z)) - self.assertEqual(cpu.read_reg(5), 0xFF) - self.assertEqual(cpu.read_reg(6), 0x00) - - def test_subi_and_sbc(self): - # test subi on r7 - src1 = """ - ldi r7, $05 - subi r7, $03 - """ - cpu1 = CPU() - prog1, labels1 = assemble(src1) - cpu1.load_program(prog1, labels1) - cpu1.run() - self.assertEqual(cpu1.read_reg(7), 0x02) - - # test subi resulting in borrow on r8 - src2 = """ - ldi r8, $02 - subi r8, $03 - """ - cpu2 = CPU() - prog2, labels2 = assemble(src2) - cpu2.load_program(prog2, labels2) - cpu2.run() - self.assertEqual(cpu2.read_reg(8), 0xFF) - self.assertTrue(cpu2.get_flag(SREG_C)) - - # test sbc separately - src3 = """ - ldi r9, $03 - ldi r10, $02 - sbc r9, r10 - """ - cpu3 = CPU() - prog3, labels3 = assemble(src3) - cpu3.load_program(prog3, labels3) - cpu3.run() - self.assertEqual(cpu3.read_reg(9), 0x01) - self.assertFalse(cpu3.get_flag(SREG_C)) - - def test_adiw(self): - cpu = CPU() - # set r24:r25 = 0x00FF - cpu.write_reg(24, 0xFF) - cpu.write_reg(25, 0x00) - prog, labels = assemble(""" - adiw r24, $0001 - """) - cpu.load_program(prog, labels) - cpu.run() - # expect 0x00FF + 1 = 0x0100 -> r24=0x00, r25=0x01 - self.assertEqual(cpu.read_reg(24), 0x00) - self.assertEqual(cpu.read_reg(25), 0x01) - - def test_reti_pushes_and_returns(self): - cpu = CPU() - # simulate an interrupt by pushing a return address then calling reti - # we use trigger_interrupt which already pushes PC and jumps to vector - cpu.interrupts[1] = True - # program: reti; ldi r0,$55 (should run after RETI returns to pushed ret) - prog, labels = assemble(""" - reti - ldi r0, $55 - """) - cpu.load_program(prog, labels) - # trigger vector 1 (should push return and jump to instruction 0) - cpu.trigger_interrupt(1) - # now run - reti should pop and return to our ldi - cpu.run() - self.assertEqual(cpu.read_reg(0), 0x55) - - def test_sbrs_sbrc(self): - cpu = CPU() - prog, labels = assemble(""" - ldi r0, $01 - sbrs r0, 0 - ldi r1, $AA - ldi r2, $BB - """) - cpu.load_program(prog, labels) - cpu.run() - # sbrs should skip the ldi r1 and execute ldi r2 - self.assertEqual(cpu.read_reg(1), 0x00) - self.assertEqual(cpu.read_reg(2), 0xBB) - - def test_sbis_sbic(self): - cpu = CPU() - # set RAM[10] bit0 = 0, so sbic should skip and sbis should not - cpu.write_ram(10, 0x00) - prog, labels = assemble(""" - sbis 10, 0 - ldi r3, $11 - sbic 10, 0 - ldi r4, $22 - """) - cpu.load_program(prog, labels) - cpu.run() - # sbis sees bit clear -> should not skip, so r3 gets loaded - self.assertEqual(cpu.read_reg(3), 0x11) - # sbic sees bit clear -> skip next -> r4 should remain 0 - self.assertEqual(cpu.read_reg(4), 0x00) - - def test_sbiw_rjmp_rcall(self): - cpu = CPU() - # test sbiw subtracts from r6:r7 word - cpu.write_reg(6, 0x00) - cpu.write_reg(7, 0x01) - prog1, labels1 = assemble(""" - sbiw r6, $0001 - """) - cpu.load_program(prog1, labels1) - cpu.run() - # 0x0100 - 1 = 0x00FF -> r6=0xFF, r7=0x00 - self.assertEqual(cpu.read_reg(6), 0xFF) - self.assertEqual(cpu.read_reg(7), 0x00) - - # rjmp: jump relative +2 (skip next two instructions) - prog2, labels2 = assemble(""" - rjmp 2 - ldi r8, $01 - ldi r9, $02 - ldi r10, $03 - """) - cpu.load_program(prog2, labels2) - cpu.run() - # rjmp +2 should land executing ldi r10 - self.assertEqual(cpu.read_reg(8), 0x00) - self.assertEqual(cpu.read_reg(9), 0x00) - self.assertEqual(cpu.read_reg(10), 0x03) - - # rcall relative +1 should call next instruction (simulate push/pop) - prog3, labels3 = assemble(""" - rcall 1 - ldi r11, $99 - ldi r12, $77 - """) - cpu.load_program(prog3, labels3) - cpu.run() - # rcall +1 jumps to ldi r11; ensure r11 loaded - self.assertEqual(cpu.read_reg(11), 0x99) - - def test_adiw_flags_edge_cases(self): - cpu = CPU() - # Case: carry out from 0xFFFF + 1 - cpu.write_reg(20, 0xFF) # low - cpu.write_reg(21, 0xFF) # high -> word = 0xFFFF - prog, labels = assemble(""" - adiw r20, $0001 - """) - cpu.load_program(prog, labels) - cpu.run() - # expect wrap to 0x0000, Z=1, C=1 - self.assertEqual((cpu.read_reg(21) << 8) | cpu.read_reg(20), 0x0000) - self.assertTrue(cpu.get_flag(1)) # Z - self.assertTrue(cpu.get_flag(0)) # C - - def test_sbiw_flags_edge_cases(self): - cpu = CPU() - # Case: borrow from 0x0000 - 1 - cpu.write_reg(18, 0x00) - cpu.write_reg(19, 0x00) - prog, labels = assemble(""" - sbiw r18, $0001 - """) - cpu.load_program(prog, labels) - cpu.run() - # expect wrap to 0xFFFF, Z=0, C=1 - self.assertEqual((cpu.read_reg(19) << 8) | cpu.read_reg(18), 0xFFFF) - self.assertFalse(cpu.get_flag(1)) # Z - self.assertTrue(cpu.get_flag(0)) # C - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_shifts_rotates.py b/tests/test_shifts_rotates.py deleted file mode 100644 index cf68040..0000000 --- a/tests/test_shifts_rotates.py +++ /dev/null @@ -1,57 +0,0 @@ -import unittest - -from tiny8 import CPU, assemble -from tiny8.cpu import SREG_C, SREG_N, SREG_S, SREG_V, SREG_Z - - -class TestShiftsRotates(unittest.TestCase): - def setUp(self): - self.cpu = CPU() - - def run_asm(self, src: str): - prog, labels = assemble(src) - self.cpu.load_program(prog, labels) - self.cpu.run() - return self.cpu - - def test_lsl_sets_c_and_v(self): - src = """ - ldi r0, 0b01000000 - lsl r0 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(0), 0b10000000) - # C should be taken from old MSB (which was 0 for this input) - self.assertFalse(cpu.get_flag(SREG_C)) - # S should equal N ^ V per helper implementation - self.assertEqual( - cpu.get_flag(SREG_S), cpu.get_flag(SREG_N) ^ cpu.get_flag(SREG_V) - ) - - def test_lsr_sets_c_and_z(self): - src = """ - ldi r1, 0b00000001 - lsr r1 - """ - cpu = self.run_asm(src) - self.assertEqual(cpu.read_reg(1), 0) - self.assertTrue(cpu.get_flag(SREG_C)) - self.assertTrue(cpu.get_flag(SREG_Z)) - - def test_rol_ror_with_carry(self): - src = """ - ldi r2, 0b10000000 - ldi r3, 0b00000001 - ; set C then rol/ror - """ - prog, labels = assemble(src) - self.cpu.load_program(prog, labels) - self.cpu.set_flag(SREG_C, True) - # perform rol on r2 - self.cpu.program.append(("rol", (("reg", 2),))) - # perform ror on r3 - self.cpu.program.append(("ror", (("reg", 3),))) - self.cpu.run() - # verify carry bits updated and registers rotated - self.assertIn(self.cpu.read_reg(2), (0b00000001, 0b00000000)) - self.assertIn(self.cpu.read_reg(3), (0b10000000, 0b00000000)) diff --git a/uv.lock b/uv.lock index 2a98d03..b632a00 100644 --- a/uv.lock +++ b/uv.lock @@ -193,6 +193,98 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] +[[package]] +name = "coverage" +version = "7.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847", size = 215912, upload-time = "2025-10-15T15:12:40.665Z" }, + { url = "https://files.pythonhosted.org/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc", size = 216310, upload-time = "2025-10-15T15:12:42.461Z" }, + { url = "https://files.pythonhosted.org/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0", size = 246706, upload-time = "2025-10-15T15:12:44.001Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7", size = 248634, upload-time = "2025-10-15T15:12:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623", size = 250741, upload-time = "2025-10-15T15:12:47.222Z" }, + { url = "https://files.pythonhosted.org/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287", size = 246837, upload-time = "2025-10-15T15:12:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552", size = 248429, upload-time = "2025-10-15T15:12:50.73Z" }, + { url = "https://files.pythonhosted.org/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de", size = 246490, upload-time = "2025-10-15T15:12:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601", size = 246208, upload-time = "2025-10-15T15:12:54.586Z" }, + { url = "https://files.pythonhosted.org/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e", size = 247126, upload-time = "2025-10-15T15:12:56.485Z" }, + { url = "https://files.pythonhosted.org/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c", size = 218314, upload-time = "2025-10-15T15:12:58.365Z" }, + { url = "https://files.pythonhosted.org/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9", size = 219203, upload-time = "2025-10-15T15:12:59.902Z" }, + { url = "https://files.pythonhosted.org/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745", size = 217879, upload-time = "2025-10-15T15:13:01.35Z" }, + { url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573, upload-time = "2025-10-15T15:13:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706, upload-time = "2025-10-15T15:13:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221, upload-time = "2025-10-15T15:13:10.964Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624, upload-time = "2025-10-15T15:13:12.5Z" }, + { url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744, upload-time = "2025-10-15T15:13:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325, upload-time = "2025-10-15T15:13:16.438Z" }, + { url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180, upload-time = "2025-10-15T15:13:17.959Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479, upload-time = "2025-10-15T15:13:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290, upload-time = "2025-10-15T15:13:21.593Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924, upload-time = "2025-10-15T15:13:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, + { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, + { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, + { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, + { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, + { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, + { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, + { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -278,6 +370,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -695,6 +796,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -725,6 +835,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, ] +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -910,6 +1050,8 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, { name = "ruff" }, { name = "shibuya" }, { name = "sphinx" }, @@ -920,11 +1062,62 @@ requires-dist = [{ name = "matplotlib", specifier = ">=3.10.7" }] [package.metadata.requires-dev] dev = [ + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "ruff", specifier = ">=0.14.1" }, { name = "shibuya", specifier = ">=2025.10.20" }, { name = "sphinx", specifier = ">=8.2.3" }, ] +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + [[package]] name = "urllib3" version = "2.5.0"