diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 7949f14eb..8ad31de00 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,18 +1,18 @@
-[bumpversion]
-current_version = 1.18.2
-
-[bumpversion:file:src/zxbc/version.py]
-search = VERSION: Final[str] = "{current_version}"
-replace = VERSION: Final[str] = "{new_version}"
-
-[bumpversion:file:src/zxbasm/version.py]
-search = VERSION = "{current_version}"
-replace = VERSION = "{new_version}"
-
-[bumpversion:file:pyproject.toml]
-search = version = "{current_version}"
-replace = version = "{new_version}"
-
-[bumpversion:file:docs/archive.md]
-search = {current_version}
-replace = {new_version}
+[bumpversion]
+current_version = 1.18.7
+
+[bumpversion:file:src/zxbc/version.py]
+search = VERSION: Final[str] = "{current_version}"
+replace = VERSION: Final[str] = "{new_version}"
+
+[bumpversion:file:src/zxbasm/version.py]
+search = VERSION = "{current_version}"
+replace = VERSION = "{new_version}"
+
+[bumpversion:file:pyproject.toml]
+search = version = "{current_version}"
+replace = version = "{new_version}"
+
+[bumpversion:file:docs/archive.md]
+search = {current_version}
+replace = {new_version}
diff --git a/.coveragerc b/.coveragerc
index f3a5cd4ea..2301d8835 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,3 +1,3 @@
-[run]
-omit =
- .tox/*
+[run]
+omit =
+ .tox/*
diff --git a/.gitattributes b/.gitattributes
index f4802a3df..335e83d39 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,18 +1,18 @@
-
-* text=false
-
-*.bas -crlf
-*.asm -crlf
-*.bi -crlf
-
-*.txt text
-*.md text
-*.py text
-*.ini text
-*.yml text
-
-*.png binary
-*.bin binary
-
-*.bas linguist-vendored
-*.asm linguist-vendored
+
+* text=false
+
+*.bas -crlf
+*.asm -crlf
+*.bi -crlf
+
+*.txt text
+*.md text
+*.py text
+*.ini text
+*.yml text
+
+*.png binary
+*.bin binary
+
+*.bas linguist-vendored
+*.asm linguist-vendored
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index ad038dd86..2d1da814d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -1,54 +1,54 @@
-name: Bug Report
-description: File a bug report.
-title: "[Bug]: "
-labels: ["bug", "triage"]
-projects: ["boriel-basic/1"]
-assignees:
- - boriel
-body:
- - type: markdown
- attributes:
- value: |
- Thanks for taking the time to fill out this bug report!
- - type: input
- id: contact
- attributes:
- label: Contact Details
- description: How can we get in touch with you if we need more info?
- placeholder: e.g. Telegram @nickname
- validations:
- required: false
- - type: input
- id: version
- attributes:
- label: Compiler version
- description: |
- Always check you have the latest version.
- You can get the version with zxbc --version
- placeholder: v1.17.3
- validations:
- required: true
- - type: textarea
- id: what-happened
- attributes:
- label: What happened?
- description: Also tell us, what did you expect to happen?
- placeholder: Tell us what you see!
- validations:
- required: true
- - type: textarea
- id: logs
- attributes:
- label: Error and Warning messages
- description: |
- Please copy and paste any relevant log output.
- This will be automatically formatted into code, so no need for backticks.
- render: shell
- - type: checkboxes
- id: terms
- attributes:
- label: Code of Conduct
- description: By submitting this issue, you agree to follow our [Code of Conduct](https://boriel-basic/zxbasic/blob/main/CODE_OF_CONDUCT.md).
- options:
- - label: I agree to follow this project's Code of Conduct
- required: true
+name: Bug Report
+description: File a bug report.
+title: "[Bug]: "
+labels: ["bug", "triage"]
+projects: ["boriel-basic/1"]
+assignees:
+ - boriel
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report!
+ - type: input
+ id: contact
+ attributes:
+ label: Contact Details
+ description: How can we get in touch with you if we need more info?
+ placeholder: e.g. Telegram @nickname
+ validations:
+ required: false
+ - type: input
+ id: version
+ attributes:
+ label: Compiler version
+ description: |
+ Always check you have the latest version.
+ You can get the version with zxbc --version
+ placeholder: v1.17.3
+ validations:
+ required: true
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened?
+ description: Also tell us, what did you expect to happen?
+ placeholder: Tell us what you see!
+ validations:
+ required: true
+ - type: textarea
+ id: logs
+ attributes:
+ label: Error and Warning messages
+ description: |
+ Please copy and paste any relevant log output.
+ This will be automatically formatted into code, so no need for backticks.
+ render: shell
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Code of Conduct
+ description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/boriel-basic/zxbasic/blob/main/CODE_OF_CONDUCT.md).
+ options:
+ - label: I agree to follow this project's Code of Conduct
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/bug_report_es.yaml b/.github/ISSUE_TEMPLATE/bug_report_es.yaml
index c3da634f0..c834d2218 100644
--- a/.github/ISSUE_TEMPLATE/bug_report_es.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report_es.yaml
@@ -1,54 +1,54 @@
-name: Informe de fallo
-description: Informar de un problema
-title: "[Bug]: "
-labels: ["bug", "triage"]
-projects: ["boriel-basic/1"]
-assignees:
- - boriel
-body:
- - type: markdown
- attributes:
- value: |
- ¡Gracias por tomarte el tiempo de informar de este fallo!
- - type: input
- id: contact
- attributes:
- label: Detalles de Contacto
- description: ¿Cómo podemos contactarte si ncesitamos información? (opcional)
- placeholder: e.g. Telegram @nickname
- validations:
- required: false
- - type: input
- id: version
- attributes:
- label: Versión del compilador
- description: |
- Always check you have the latest version.
- You can get the version with zxbc --version
- placeholder: v1.17.3
- validations:
- required: true
- - type: textarea
- id: what-happened
- attributes:
- label: ¿Qué ha pasado?
- description: Cuéntanos también que esperabas que sucediera.
- placeholder: (descripción del problema)
- validations:
- required: true
- - type: textarea
- id: logs
- attributes:
- label: Mensajes de error o warnings del compilador
- description: |
- Por favor, copia y pega la salida de los logs.
- Serán formateados automáticamente como salida de log. No necesitas usar formato.
- render: shell
- - type: checkboxes
- id: terms
- attributes:
- label: Código de Conducta
- description: Al enviar este formulario, acuerdas seguir nuestro [Código de Conducta](https://boriel-basic/zxbasic/blob/main/CODE_OF_CONDUCT.md).
- options:
- - label: I agree to follow this project's Code of Conduct
- required: true
+name: Informe de fallo
+description: Informar de un problema
+title: "[Bug]: "
+labels: ["bug", "triage"]
+projects: ["boriel-basic/1"]
+assignees:
+ - boriel
+body:
+ - type: markdown
+ attributes:
+ value: |
+ ¡Gracias por tomarte el tiempo de informar de este fallo!
+ - type: input
+ id: contact
+ attributes:
+ label: Detalles de Contacto
+ description: ¿Cómo podemos contactarte si ncesitamos información? (opcional)
+ placeholder: e.g. Telegram @nickname
+ validations:
+ required: false
+ - type: input
+ id: version
+ attributes:
+ label: Versión del compilador
+ description: |
+ Always check you have the latest version.
+ You can get the version with zxbc --version
+ placeholder: v1.17.3
+ validations:
+ required: true
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: ¿Qué ha pasado?
+ description: Cuéntanos también que esperabas que sucediera.
+ placeholder: (descripción del problema)
+ validations:
+ required: true
+ - type: textarea
+ id: logs
+ attributes:
+ label: Mensajes de error o warnings del compilador
+ description: |
+ Por favor, copia y pega la salida de los logs.
+ Serán formateados automáticamente como salida de log. No necesitas usar formato.
+ render: shell
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Código de Conducta
+ description: Al enviar este formulario, acuerdas seguir nuestro [Código de Conducta](https://github.com/boriel-basic/zxbasic/blob/main/CODE_OF_CONDUCT.md).
+ options:
+ - label: I agree to follow this project's Code of Conduct
+ required: true
diff --git a/.github/workflows/python-app.yml b/.github/workflows/run-tests.yml
similarity index 86%
rename from .github/workflows/python-app.yml
rename to .github/workflows/run-tests.yml
index 44d531397..7a583cc15 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/run-tests.yml
@@ -1,7 +1,7 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
-name: ZXBasic
+name: Run Test
on:
pull_request:
@@ -9,7 +9,7 @@ on:
- docs
env:
- PYTHON_VERSION: 3.12.3
+ PYTHON_VERSION: 3.14.3
jobs:
build:
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
@@ -29,12 +29,12 @@ jobs:
- name: Install poetry
run: |
- pip install -U pip~=24.0
- pip install poetry~=1.7.1
+ pip install -U pip~=26.0
+ pip install poetry~=2.3.2
poetry config virtualenvs.in-project true
- name: Set up poetry cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: .venv
key: venv-${{ runner.os }}-py-${{ env.PYTHON_VERSION }}-${{ hashFiles('poetry.lock') }}
diff --git a/.gitignore b/.gitignore
index 8385908be..3282193d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,21 +1,26 @@
-# Created by .ignore support plugin (hsz.mobi)
-*.egg-info/
-*.pyc
-.cache/
-.pytest_cache/
-.mypy_cache/
-.pytype/
-.idea/
-.python-version
-.tox/
-MANIFEST
-dist/
-examples/*.bin
-examples/*.tzx
-scratch/
-.coverage
-htmlcov/
-build/
-venv/
-.pypi-token
-.coverage.*
+# Created by .ignore support plugin (hsz.mobi)
+*.egg-info/
+*.pyc
+.cache/
+.pytest_cache/
+.mypy_cache/
+.pytype/
+.idea/
+.python-version
+.tox/
+MANIFEST
+dist/
+examples/*.bin
+examples/*.tzx
+scratch/
+.coverage
+htmlcov/
+build/
+venv/
+.pypi-token
+.coverage.*
+
+# zx81sd test build artifacts (compila.bat + split_sd81.py output)
+/compila.bat
+/TESTSD81*.P
+/TESTSD81*.BIN
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 42cff9ce8..e55fcbc4c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,20 +1,20 @@
-repos:
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
- hooks:
- - id: check-yaml
- exclude: \.(bin|out)$
- - id: end-of-file-fixer
- exclude: \.(bin|out)$
- - id: trailing-whitespace
- exclude: \.(bin|out)$
-
- - repo: https://github.com/astral-sh/ruff-pre-commit
- # Ruff version.
- rev: v0.9.4
- hooks:
- # Run the linter.
- - id: ruff
- args: [ --fix ]
- # Run the formatter.
- - id: ruff-format
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.6.0
+ hooks:
+ - id: check-yaml
+ exclude: \.(bin|out)$
+ - id: end-of-file-fixer
+ exclude: \.(bin|out)$
+ - id: trailing-whitespace
+ exclude: \.(bin|out)$
+
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ # Ruff version.
+ rev: v0.15.12
+ hooks:
+ # Run the linter.
+ - id: ruff
+ args: [ --fix ]
+ # Run the formatter.
+ - id: ruff-format
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index afe9f1939..ae98c99f5 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -1,21 +1,21 @@
-# .readthedocs.yaml
-# Read the Docs configuration file
-# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
-
-# Required
-version: 2
-
-# Set the version of Python and other tools you might need
-build:
- os: ubuntu-22.04
- tools:
- python: "3.11"
-
-python:
- install:
- - requirements: ./docs/requirements.txt
-
-# Build documentation in the docs/ directory with mkdocs
-mkdocs:
- configuration: mkdocs.yml
- fail_on_warning: false
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the version of Python and other tools you might need
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
+python:
+ install:
+ - requirements: ./docs/requirements.txt
+
+# Build documentation in the docs/ directory with mkdocs
+mkdocs:
+ configuration: mkdocs.yml
+ fail_on_warning: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 81e2d1a79..04320bd24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,36 @@
-[v1.18.2](https://github.com/boriel-basic/zxbasic/tree/v1.18.1)
+[v1.18.7](https://github.com/boriel-basic/zxbasic/tree/v1.18.7)
+===
++ ! Fix a bug with Fixed point numbers conversion to Long
++ ! Consolidate casting of any decimal number to -INF
+
+[v1.18.6](https://github.com/boriel-basic/zxbasic/tree/v1.18.6)
+===
++ ! Critical bugfix which prevented using zxnext arch backend
+
+[v1.18.5](https://github.com/boriel-basic/zxbasic/tree/v1.18.5)
+===
++ ! Fixed a regression bug with arrays passed byRef
++ ! Fixed a bug with mul for Long and ULong in zx48k arch
++ Refact some runtime libraries to use that of zx48k arch from zxnext
+
+[v1.18.4](https://github.com/boriel-basic/zxbasic/tree/v1.18.4)
+===
++ ! Fixed a bug with arrays and strings
++ Updated documentation
++ Added Scroll-Aligned (aligned to column) functions
+
+[v1.18.3](https://github.com/boriel-basic/zxbasic/tree/v1.18.3)
+===
++ ! Fix `ByRef` with an array element
++ ! Fix `@array` label emission in generated code
++ Small optimization for -O2
++ Add `getPaintData()` and `getChars()` functions to `putchars.bas` lib
++ Refactor: move memory runtime routines to `runtime/mem/`
++ ! Fix linting and update tests suite
++ Documentation fixes and improvements (links, syntax highlighting)
++ License housekeeping: ensure all files use AGPLv3 headers
+
+[v1.18.2](https://github.com/boriel-basic/zxbasic/tree/v1.18.2)
===
+ ! Allows installation with `pip install` in python 3.11 environments
+ * Add `hmirror.bas` library
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
new file mode 100644
index 000000000..6fe02f35b
--- /dev/null
+++ b/CONTRIBUTORS.md
@@ -0,0 +1,35 @@
+# License
+
+```
+| SPDX-License-Identifier: AGPL-3.0-or-later
+| © Copyright 2008-2024 José Manuel Rodríguez de la Rosa
+| and contributors.
+```
+
+See https://www.gnu.org/licenses/agpl-3.0.html for license details.
+
+# Contributors
+
+Not all the Contributors are listed here. If you are missing, please let us know.
+
+* Jose Rodriguez ([@boriel](https://github.com/boriel))
+* Paul Fisher [@britlion](https://github.com/Britlion)
+* David Saphier [@em00k](https://github.com/em00k)
+* Agustín Gimenez Bernad [@gusmanb](https://github.com/gusmanb)
+* Baltasar García Perez-Schofield ([@Baltasarq](https://github.com/Baltasarq))
+* Bartolomé Sánchez Salado ([@bartsanchez](https://github.com/bartsanchez))
+* Cronomantic ([@cronomantic](https://github.com/cronomantic))
+* Juan Segura Durán ([@Duefectu](https://github.com/Duefectu))
+* Einar Saukas ([@einar-saukas](https://github.com/einar-saukas))
+* Ivan Kosarev ([@kosarev](https://github.com/kosarev))
+* Miguel Angel Rodríguez Jódar ([@mcleod-ideafix](https://github.com/mcleod-ideafix))
+* [@MrKOSMOS](https://github.com/MrKOSMOS)
+* [@patters-match](https://github.com/patters-match)
+* [@programadorhedonista](https://github.com/programadorhedonista)
+* Rogerio Biondi ([@rogeriobiondi](https://github.com/rogeriobiondi))
+* [@spectrumcomputing](https://github.com/spectrumcomputing)
+* Stefan ([@polluks](https://github.com/polluks))
+* Alejandro Valero ([@wilco2009](https://github.com/wilco2009))
+* Pedro Gimeno ([@pgimeno](https://codeberg.org/pgimeno))
+
+Many thanks! Every line counts! 🫶
diff --git a/README.md b/README.md
index 19184b400..a3bd52fd0 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ This is a very little help file.
- For DOCUMENTATION in English go to the [ZX BASIC docs](https://zxbasic.readthedocs.io/en/latest/).
- - For help, support, updates meet the community at the [forum](https://www.boriel.com/forum).
+ - For help, support, updates meet the community at the [forum](https://forum.boriel.com/).
INSTALLATION
@@ -65,7 +65,7 @@ You can use this version in Windows, but will need to install a python interpret
|---|---|---|
| An in-game screenshot of Eleuterio by [@na_th_an](https://github.com/mojontwins) | Ingame screenshot of _El Hobbit_ by [@Wilco2009](https://github.com/wilco2009)| Ingame screenshot of _Knignt and Demonds DX_ by [Einar Saukas](https://github.com/einar-saukas)
-See more examples at the [Relased Programs](https://zxbasic.readthedocs.io/en/latest/released_programs/) page.
+See more examples at the [Released Programs](https://zxbasic.readthedocs.io/en/latest/released_programs/) page.
QUICK START
-----------
@@ -94,7 +94,7 @@ Create a text file with the following content:
Save it as `hello.bas` and finally compile it with:
~~~~
-zxbc -taB hello.bas
+zxbc -f tap --autorun --BASIC hello.bas
~~~~
If everything went well, a file named `hello.tap` should be created.
diff --git a/docs/acs.md b/docs/acs.md
index a4d364c36..bd757033c 100644
--- a/docs/acs.md
+++ b/docs/acs.md
@@ -25,7 +25,7 @@ PRINT "Arc Cosine value of a is "; ACS(a)
* This function is 100% Sinclair BASIC Compatible
* If the given argument type is not float, it will be [converted](cast.md) to float before operating with it.
-##See also
+## See also
* [SIN](sin.md) and [ASN](asn.md)
* [TAN](tan.md) and [ATN](atn.md)
diff --git a/docs/archive.md b/docs/archive.md
index 429c6d6a3..c57312e94 100644
--- a/docs/archive.md
+++ b/docs/archive.md
@@ -8,30 +8,30 @@ repository (git).
## Latest Version
You can contribute to ZX BASIC by reporting possible bugs or improvement suggestions at the
-[forum](http://www.boriel.com/forum) or in social media.
+[forum](https://forum.boriel.com/) or in social media.
-The latest stable version is **1.18.2**.
+The latest stable version is **1.18.7**.
Click on the desired icon below to download the package suitable for your platform:
* [
- https://www.boriel.com/files/zxb/zxbasic-1.18.2-win32.zip](https://www.boriel.com/files/zxb/zxbasic-1.18.2-win32.zip)
+ https://www.boriel.com/files/zxb/zxbasic-1.18.7-win32.zip](https://www.boriel.com/files/zxb/zxbasic-1.18.7-win32.zip)
Windows .exe zip package. No install needed, just uncompress it in a directory of your choice.
* [
- https://www.boriel.com/files/zxb/zxbasic-1.18.2-macos.tar.gz](https://www.boriel.com/files/zxb/zxbasic-1.18.2-macos.tar.gz)
+ https://www.boriel.com/files/zxb/zxbasic-1.18.7-macos.tar.gz](https://www.boriel.com/files/zxb/zxbasic-1.18.7-macos.tar.gz)
Mac OS x64 package. No install needed, just uncompress it in a directory of your choice (needs Python installed
in your system).
* [
- https://www.boriel.com/files/zxb/zxbasic-1.18.2-linux64.tar.gz](https://www.boriel.com/files/zxb/zxbasic-1.18.2-linux64.tar.gz)
+ https://www.boriel.com/files/zxb/zxbasic-1.18.7-linux64.tar.gz](https://www.boriel.com/files/zxb/zxbasic-1.18.7-linux64.tar.gz)
Linux x64 binary package. No install needed, just uncompress it in a directory of your choice.
* [
- https://www.boriel.com/files/zxb/zxbasic-1.18.2.zip](https://www.boriel.com/files/zxb/zxbasic-1.18.2.zip)
+ https://www.boriel.com/files/zxb/zxbasic-1.18.7.zip](https://www.boriel.com/files/zxb/zxbasic-1.18.7.zip)
Windows, Linux, Mac zip package, with python scripts. Requires python installed in your system.
* [
- https://www.boriel.com/files/zxb/zxbasic-1.18.2.tar.gz](https://www.boriel.com/files/zxb/zxbasic-1.18.2.tar.gz)
+ https://www.boriel.com/files/zxb/zxbasic-1.18.7.tar.gz](https://www.boriel.com/files/zxb/zxbasic-1.18.7.tar.gz)
Windows, Linux, Mac tar.gz package, with python scripts. Requires python installed in your system.
### What's new
diff --git a/docs/asn.md b/docs/asn.md
index eac4f9b9c..8853bee9e 100644
--- a/docs/asn.md
+++ b/docs/asn.md
@@ -22,8 +22,8 @@ PRINT "Arc Sine value of a is "; ASN(a)
## Remarks
-* This function is 100% Sinclair BASIC Compatible
-* If the given argument type is not float, it will be [converted](cast.md) to float before operating with it.
+* This function is 100% Sinclair BASIC Compatible
+* If the given argument type is not float, it will be [converted](cast.md) to float before operating with it.
## See also
diff --git a/docs/bin.md b/docs/bin.md
new file mode 100644
index 000000000..d37e002d7
--- /dev/null
+++ b/docs/bin.md
@@ -0,0 +1,27 @@
+# BIN
+
+## Syntax
+
+```
+BIN
+```
+
+
+## Description
+
+`BIN` is used to denote a binary number. It exists mostly for compatibility reasons with Sinclair BASIC.
+You can denote a binary number by prefixing it with `BIN`, but also using the `%` prefix or the `b` suffix.
+
+For example, the number `5` can be denoted as follows:
+
+ * `BIN 101`
+ * `101b`
+ * `%101`
+
+These three expressions are equivalent.
+
+
+## Remarks
+
+* This keyword is 100% Sinclair BASIC Compatible
+* Binary numbers are always considered unsigned integers and the type depends on the size of the number.
diff --git a/docs/draw.md b/docs/draw.md
index 37214821e..c4b169574 100644
--- a/docs/draw.md
+++ b/docs/draw.md
@@ -1,47 +1,56 @@
# DRAW
+The **DRAW** command is used to draw straight lines and circular arcs on the screen.
## Syntax
```
- DRAW dx, dy [, arc]
+DRAW dx, dy [, arc]
```
- or
+or
```
- DRAW ; dx, dy [, arc]
+DRAW ; dx, dy [, arc]
```
-Draws a _straight line_ starting from the current drawing coordinates (x, y)
-(see [PLOT](plot.md)) to `(x + dx, y + dy)` position. Coordinate `(0, 0)`
-designates bottom-left screen corner. Drawing coordinates are updated
-to the last position.
-**DRAW** is enhanced in ZX BASIC to allow drawing on the last two screen rows (this was not possible in Sinclair BASIC). So now we have 16 lines more (192 in total). Sinclair BASIC only used top 176 scan lines. This means that in Sinclair BASIC
+## Description
+Draws a *straight line* starting from the current drawing position `(x, y)` to the new position `(x + dx, y + dy)`. The values `dx` and `dy` represent the relative horizontal and vertical distance from the current position.
+
+The coordinate `(0, 0)` designates the bottom-left corner of the screen. After the command completes, the current drawing coordinates are updated to the last plotted position.
+
+To set the initial drawing position, you can use the [PLOT](plot.md) command.
+
+### ZX BASIC Enhancements
+**DRAW** is enhanced in ZX BASIC to allow drawing across all 192 scan lines of the Spectrum screen. Original Sinclair BASIC only permitted drawing on the top 176 lines, reserving the bottom 16 lines for system use.
+
+Because of this difference, programs ported from Sinclair BASIC will appear **shifted down by 16 pixels**. To maintain the same vertical screen position, you should add 16 to the Y-coordinate of the initial `PLOT` command:
+
+**Sinclair BASIC code:**
```
-PLOT x0, y0: DRAW x, y
+PLOT x0, y0: DRAW dx, dy
```
-is equivalent in ZX BASIC to
+**ZX BASIC equivalent (for same screen position):**
```
-PLOT x0, y0 + 16: DRAW x, y
+PLOT x0, y0 + 16: DRAW dx, dy
```
-
-**Remark**: This primitive uses Bresenham's algorithm for faster drawing instead of ROMs implementation.
-
### Drawing Arcs
-When used with 3 parameters it draws arcs the same way the Sinclair BASIC version does,
-but again the 192 scan-lines are available.
+When used with three parameters, **DRAW** draws a circular arc from the current position to the relative target `(x + dx, y + dy)`.
```
DRAW dx, dy, arc
```
-The above will draw an arc from the current position to (x + dx, y + dy) position, with a curve of ''arc'' radians. This routine also have some strange behaviors. High values of arc draws strange patterns.
+
+The `arc` parameter specifies the angle of the curve in radians.
+* A positive `arc` value draws the curve turning to the left (counter-clockwise).
+* A negative `arc` value draws the curve turning to the right (clockwise).
+
+**Remark:** This routine may exhibit unusual behavior when using very large values for `arc`.
### Remarks
-* This function is not strictly Sinclair BASIC compatible since it uses all 192 screen lines instead of top 176.
-If you translate **PLOT**, **DRAW** & **CIRCLE**, commands from Sinclair BASIC _as is_
-your drawing will be _shifted down_ 16 pixels.
+* **Fast Drawing**: This primitive uses Bresenham's algorithm for faster drawing instead of the Spectrum ROM implementation.
+* **Compatibility**: This function is not strictly Sinclair BASIC compatible because it uses the full 192 screen lines. If you translate **PLOT**, **DRAW**, or **CIRCLE** commands from Sinclair BASIC *as is*, your drawing will be *shifted down* by 16 pixels.
### See Also
* [PLOT](plot.md)
diff --git a/docs/external_resources.md b/docs/external_resources.md
index 83cd40d92..3fa445a18 100644
--- a/docs/external_resources.md
+++ b/docs/external_resources.md
@@ -9,9 +9,9 @@ Other people contribute to it creating alternative IDEs, libraries, tools and ut
### Tommy Gun
A free IDE created by [Tony Thompson](http://www.users.on.net/~tonyt73). This powerful tool is designed to create
generic 8-bit and 16-bit projects, and can be set up to be used with ZX Basic.
-Have a look at the [TommyGun files](https://sourceforge.net/projects/tommygun/files/) page.
+Have a look at the [TommyGun files](https://sourceforge.net/projects/tommygun/files/) or [source code](https://github.com/tonyt73/TommyGun) pages.
-To integrate ZX Basic with Tommy Gun, please follow this [tutorial](https://www.boriel.com/forum/showthread.php?tid=110).
+To integrate ZX Basic with Tommy Gun, please follow this [tutorial](https://forum.boriel.com/showthread.php?tid=110).
### SpectNet IDE
A free and impressive IDE created by [Istvan Novak](https://github.com/Dotneteer).
@@ -25,7 +25,7 @@ further information. The documentation of it is very well maintained.
* [Multipaint](http://multipaint.kameli.net/), a Java-based graphics editor that allows you to draw pictures with the color limitations of some typical screen formats from 8-bit computer platforms, like ZX Spectrum, C64 and Plus/4 high-resolution and multicolor, MSX 1 and Amstrad CPC0.
-* [ZX-Paintbrush] (https://zx-modules.de/), a Windows graphics editor specifically for ZX-Spectrum screen formats, including multicolor.
+* [ZX-Paintbrush](https://zx-modules.jimdofree.com/), a Windows graphics editor specifically for ZX-Spectrum screen formats, including multicolor.
## Utilities
Other generic utilities not above are:
@@ -42,16 +42,16 @@ I find this very useful.
##Engines
There are several external engines that can be used within compiler to achieve effects like multicolour display, or sprite handling.
-* [FASE](https://www.boriel.com/forum/showthread.php?tid=568)
- (or [here](https://www.boriel.com/forum/showthread.php?tid=575)), a sprite, tile and map engine.
+* [FASE](https://forum.boriel.com/showthread.php?tid=568)
+ (or [here](https://forum.boriel.com/showthread.php?tid=575)), a sprite, tile and map engine.
* [Nirvana](https://spectrumcomputing.co.uk/entry/30001/ZX-Spectrum/NIRVANA_ENGINE)
- (or [https://www.ime.usp.br/~einar/bifrost/ here]), a multicolour 8x2 (30x22 chars) graphics engine.
+ (or [here](https://www.ime.usp.br/~einar/bifrost/)), a multicolour 8x2 (30x22 chars) graphics engine.
* [Nirvana+](https://spectrumcomputing.co.uk/entry/30002/ZX-Spectrum/NIRVANA_ENGINE)
- (or [https://www.ime.usp.br/~einar/bifrost/ here]), a multicolour 8x2 (32x23 chars) graphics engine.
+ (or [here](https://www.ime.usp.br/~einar/bifrost/)), a multicolour 8x2 (32x23 chars) graphics engine.
* [Bifrost*](https://spectrumcomputing.co.uk/entry/27405/ZX-Spectrum/BIFROST*_ENGINE)
- (or [https://www.ime.usp.br/~einar/bifrost/ here]), a multicolour 8x1 (18x18 chars) graphics engine.
+ (or [here](https://www.ime.usp.br/~einar/bifrost/)), a multicolour 8x1 (18x18 chars) graphics engine.
* [Bifrost*2](https://spectrumcomputing.co.uk/entry/30003/ZX-Spectrum/BIFROST*2_ENGINE)
- (or [https://www.ime.usp.br/~einar/bifrost/ here]), a multicolour 8x1 (20x22 chars) graphics engine.
+ (or [here](https://www.ime.usp.br/~einar/bifrost/)), a multicolour 8x1 (20x22 chars) graphics engine.
* [Zxodus\]\[Engine](https://spectrumcomputing.co.uk/entry/26639/ZX-Spectrum/ZXodus_Engine)
- (or [here](https://www.boriel.com/forum/showthread.php?tid=568)), a 3d maze and RPG engine with
+ (or [here](https://forum.boriel.com/showthread.php?tid=568)), a 3d maze and RPG engine with
multicolour 8x1 (18x18 chars) support.
diff --git a/docs/for.md b/docs/for.md
index 3ad506557..5fbc1dc7a 100644
--- a/docs/for.md
+++ b/docs/for.md
@@ -10,7 +10,6 @@
## Parameters
* _iterator_: a variable identifier that is used to iterate from an initial value to an end value.
-* _datatype_: If specified, the variable iterator will automatically be declared with the type datatype.
* _startvalue_: an expression that denotes the starting value of the iterator.
* _endvalue_: an expression used to compare with the value of the iterator.
* _stepvalue_: an expression that is added to the iterator after every iteration.
diff --git a/docs/function.md b/docs/function.md
index f622874ae..5a009d309 100644
--- a/docs/function.md
+++ b/docs/function.md
@@ -80,5 +80,6 @@ that can be ignored.
* [ASM](asm.md)
* [END](end.md)
* [RETURN](return.md)
+* [DECLARE](declare.md)
* [ByREF](byref.md)
* [ByVAL](byval.md)
diff --git a/docs/identifier.md b/docs/identifier.md
index 05cc3d0c7..2fa82fb4c 100644
--- a/docs/identifier.md
+++ b/docs/identifier.md
@@ -9,7 +9,7 @@ Note that there are a number of common statements that you may find in ZX BASIC
## Reserved Identifiers
-The following identifiers are _reserved words_, and can't be used as variables, functions or labels. Reserved identifiers are _case insensitive_ (it doesn't matter whether you write them in upper or lower case letters, or a mix of them). So **PRINT**, **print** and **PrInT** means all the same in ZX BASIC. On the other hand, non-reserved words can be either case sensitive or not (depending on the [options](options.md)) in effect.
+The following identifiers are _reserved words_, and can't be used as variables, functions or labels. Reserved identifiers are _case insensitive_ (it doesn't matter whether you write them in upper or lower case letters, or a mix of them). So **PRINT**, **print** and **PrInT** means all the same in ZX BASIC. On the other hand, non-reserved words can be either case sensitive or not (depending on the [options](zxb.md#command-line-options) in effect).
Identifiers shown in bold are taken from the Sinclair BASIC (beware their meaning here might be different, however). Some of them has been marked as _statements_, _functions_ or _operators_:
@@ -21,6 +21,7 @@ Identifiers shown in bold are taken from the Sinclair BASIC (beware their meanin
* **[AT](at.md)**
* **[ATN](atn.md)** **(function)**
* **[bAND](bitwiselogic.md#bAND)** **(operator)**
+* **[BIN](bin.md)**
* **[bNOT](bitwiselogic.md#bNOT)** **(operator)**
* **[bOR](bitwiselogic.md#bOR)** **(operator)**
* **[bXOR](bitwiselogic.md#bXOR)** **(operator)**
@@ -100,6 +101,7 @@ Identifiers shown in bold are taken from the Sinclair BASIC (beware their meanin
* **[STOP](stop.md)**
* **[STR](str.md)** **(function)** (Can also be written as **STR$**)
* **[SUB](sub.md)**
+* **[TAB](tab.md)**
* **[TAN](tan.md)** **(function)**
* **[THEN](if.md)**
* **[TO](to.md)**
@@ -120,7 +122,7 @@ You should also avoid defining (with a SUB or FUNCTION command) routines with th
* **[CSRLIN (Library Function)](library/csrlin.md)** **(function)**
* [HEX (Library Function)](library/hex.md) **(function)**
* [HEX16 (Library Function)](library/hex.md) **(function)**
-* **[INPUT (Library Function)](library/input.md)** **(function)**
+* **[INPUT42 (Library Function)](library/input42.md)** **(function)**
* **[GetKey (Library Function)](library/keys/getkey.md)** **(function)**
* **[MultiKeys (Library Function)](library/keys/multikeys.md)** **(function)**
* **[GetKeyScanCode (Library Function)](library/keys/getkeyscancode.md)** **(function)**
diff --git a/docs/imgs.sh b/docs/imgs.sh
index c3fc30811..9531161b6 100755
--- a/docs/imgs.sh
+++ b/docs/imgs.sh
@@ -1,15 +1,15 @@
-#!/bin/bash
-
-for i in $(egrep -e 'png|gif' released_programs.md); do
- IMG=$(echo $i|sed -e 's/^!*\[\([^]]*\)\].*$/\1/')
- LIMG=$(echo $IMG| tr A-Z a-z)
- DEST=./img/games/$LIMG
- (ls $DEST 2>/dev/null) && continue
- IMGPATH=$(find ~/migration/client6/web10/web/wiki/ -type f -name "240px-$IMG")
- if [[ "$IMGPATH" == "" ]]; then
- continue
- fi
- #echo "Image: $IMG => $IMGPATH"
- cp -v $IMGPATH $DEST
-done
-
+#!/bin/bash
+
+for i in $(egrep -e 'png|gif' released_programs.md); do
+ IMG=$(echo $i|sed -e 's/^!*\[\([^]]*\)\].*$/\1/')
+ LIMG=$(echo $IMG| tr A-Z a-z)
+ DEST=./img/games/$LIMG
+ (ls $DEST 2>/dev/null) && continue
+ IMGPATH=$(find ~/migration/client6/web10/web/wiki/ -type f -name "240px-$IMG")
+ if [[ "$IMGPATH" == "" ]]; then
+ continue
+ fi
+ #echo "Image: $IMG => $IMGPATH"
+ cp -v $IMGPATH $DEST
+done
+
diff --git a/docs/index.md b/docs/index.md
index 3eb16e062..0b5f2be27 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -50,8 +50,8 @@ Get the latest version of Boriel BASIC from the [archive](archive.md).
## Help and Support
-* [Community Forum](http://www.boriel.com/forum)
-
Have a question? Need help or comment a report a bug? Go to the [Boriel BASIC forum](http://www.boriel.com/forum)
+* [Community Forum](https://forum.boriel.com/)
+
Have a question? Need help or comment a report a bug? Go to the [Boriel BASIC forum](https://forum.boriel.com/)
## External resources
diff --git a/docs/installation.md b/docs/installation.md
index 5e15cae81..5fd70dac4 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -8,7 +8,7 @@ ZX Basic SDK comes two several flavours:
* Multiplatform python scripts (will require Python 3.8+ to be installed in your system)
### Prerequisites
-For the _Multi-platform_ bundle, you will need a [python](http://www.python.org) interpreter **version 3.8** or
+For the _Multi-platform_ bundle, you will need a [python](http://www.python.org) interpreter **version 3.11** or
higher installed in your system (Linux and Mac OS X users will probably have it already).
### Downloading
diff --git a/docs/library/attr.md b/docs/library/attr.md
index 481eedf3f..51bcbf5b5 100644
--- a/docs/library/attr.md
+++ b/docs/library/attr.md
@@ -31,5 +31,5 @@ PRINT AT 0, 0; "The attribute of screen position 9, 10 is "; s
## See also
* [ CSRLIN ](csrlin.md)
-* [ POS](pos.md)
+* [ POS ](pos.md)
* [ AT ](../at.md)
diff --git a/docs/library/csrlin.md b/docs/library/csrlin.md
index d0725bbad..c31d13479 100644
--- a/docs/library/csrlin.md
+++ b/docs/library/csrlin.md
@@ -2,6 +2,7 @@
## Syntax
+
```
CSRLIN()
```
@@ -14,6 +15,7 @@ Returns the row (line) position of the text cursor (0 means the top line).
CSRLIN is a library function to be included with the following command:
+
```
# include
```
diff --git a/docs/library/doublesizeprint.bas.md b/docs/library/doublesizeprint.bas.md
index 0a881cf6c..3c5736a08 100644
--- a/docs/library/doublesizeprint.bas.md
+++ b/docs/library/doublesizeprint.bas.md
@@ -57,13 +57,13 @@ EX DE,HL ;' Save it in DE
LD A,(IX+9) ;'Character
-CP 164 ;' > UDG "U" ?
+CP 165 ;' >= RND , > UDG "U" ?
JP NC, doubleSizePrintCharEnd
-CP 32 ;' < space+1?
+CP 32 ;' < space?
JP C, doubleSizePrintCharEnd
-CP 144 ;' >144?
+CP 144 ;' >= UDG "A" ?
JP NC, doubleSizePrintCharUDGAddress
LD L,A
diff --git a/docs/library/hex.md b/docs/library/hex.md
new file mode 100644
index 000000000..e04868855
--- /dev/null
+++ b/docs/library/hex.md
@@ -0,0 +1,37 @@
+# HEX/HEX16/HEX8
+
+## Syntax
+
+
+```basic
+A$ = hex(n32)
+B$ = hex16(n16)
+C$ = hex8(n8)
+```
+Where `n32` is a 32-bit ULONG, `n16` is a 16-bit UINTEGER and `n8` is an 8-bit UBYTE.
+
+## Description
+
+* HEX:
+Takes one _32_ bit unsigned integer number and returns an 8 chars str containing the HEX string representation.
+* HEX16:
+Takes one _16_ bit unsigned integer number and returns a 4 chars str containing the HEX string representation.
+* HEX8:
+Takes one _8_ bit unsigned integer number and returns a 2 chars str containing the HEX string representation.
+
+## Requirements
+
+HEX, HEX16 and HEX8 can be included with the following command:
+
+```
+#include
+```
+
+## Remarks
+
+* This function is not available in Sinclair BASIC.
+* Avoid recursive / multiple inclusion when calling this function.
+* HEX16 ad HEX8 both call HEX to perform conversion, but differ in the size of the string they return.
+
+## See also
+
diff --git a/docs/library/input42.md b/docs/library/input42.md
new file mode 100644
index 000000000..59a6debe6
--- /dev/null
+++ b/docs/library/input42.md
@@ -0,0 +1,35 @@
+# Input42.bas
+
+## INPUT42
+
+Simple INPUT routine (not as powerful as Sinclair BASIC's), but this one uses the PRINT42 routine.
+Issues a cursor to the screen, waits for the user to type and returns the user's input through PRINT42 when the user presses ENTER.
+
+## Syntax
+
+```basic
+A$ = INPUT42(MaxChars)
+```
+MaxChars is the number of characters the INPUT42 function will accept as a maximum. It is a UINTEGER and thus has a maximum value of 65535.
+
+## Requirements
+
+INPUT42 is a library function that must be included before it can be used. Use the following directive:
+
+```
+# include
+```
+
+## Remarks
+
+* Note that this function ALWAYS RETURNS A STRING, which is very different from Sinclair BASIC's INPUT statement.
+* This function places the Input cursor at the last print position, not at the bottom of the screen. Remember that ZX Basic allows access to all 24 screen lines, so PRINT AT 24,0; sets the PRINT cursor to the bottom of the screen.
+* Avoid recursive / multiple inclusion
+* The input subroutine DOES NOT act like ZX Spectrum INPUT command
+* Uses ZX SPECTRUM ROM
+
+## See also
+
+* [ INKEY ](../inkey.md)
+* [ INPUT ](../input.md)
+* [ PRINT42 ](print42.bas.md)
diff --git a/docs/library/print42.bas.md b/docs/library/print42.bas.md
index b6d96a6b0..372dccca8 100644
--- a/docs/library/print42.bas.md
+++ b/docs/library/print42.bas.md
@@ -19,7 +19,7 @@ Moves the print42 system's print cursor to row Y, column X. Note that `0 <= x <=
for X can be up to 41. The range of values for Y is the normal 0-23.
```
-printat42(STRING)
+print42(STRING)
```
Prints the string to the screen at the current Print42 co-ordinates. It does so in the current permanent colours.
diff --git a/docs/library/print64x32.bas.md b/docs/library/print64x32.bas.md
index eb25adcce..4fff115fe 100644
--- a/docs/library/print64x32.bas.md
+++ b/docs/library/print64x32.bas.md
@@ -6,7 +6,7 @@ This further enhances the screen by allowing 32 rows of 6 pixels instead of 24 r
This allows 2048 character positions on the screen.
Note that the screen tables file, needed for inclusion, can be downloaded from the forum
-thread http://www.boriel.com/forum/wishlist/64-char-print-32-lines-version-t680.html
+thread https://forum.boriel.com/wishlist/64-char-print-32-lines-version-t680.html
## Usage
diff --git a/docs/operators.md b/docs/operators.md
index c66889333..0e5106ed7 100644
--- a/docs/operators.md
+++ b/docs/operators.md
@@ -39,10 +39,8 @@ which is _wrong_. If in doubt, use always parenthesis to enforce the desired eva
## Logical Operators
-Logicals operators are like in ZX Spectrum Basic. Their result can be either _False_ (which is represented with 0)
-or _True_, which might be any other value. Don't expect _True_ value number to be always **1**.
-If you need 0/1 values for boolean evaluations, use `--strict-boolean` [compiler option](zxb.md#Command Line Options).
-This might add a little overhead to boolean evaluations, tough.
+Logicals operators are like in ZX Spectrum Basic. Their result can be either False
+(which is represented with 0) or True (represented by 1).
Operator arguments must be numbers and the result is an unsigned byte value. For binary operators,
if arguments are of different types they are [converted](cast.md) to a common type before being evaluated:
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
index 86d46c0a3..c3e7f2261 100644
--- a/docs/overrides/main.html
+++ b/docs/overrides/main.html
@@ -1,6 +1,6 @@
-{% extends "base.html" %}
-
-{% block extrahead %}
-
-
+{% extends "base.html" %}
+
+{% block extrahead %}
+
+
{% endblock %}
\ No newline at end of file
diff --git a/docs/overrides/stylesheets/extra.css b/docs/overrides/stylesheets/extra.css
index 3a81f3722..89ae1f4b5 100644
--- a/docs/overrides/stylesheets/extra.css
+++ b/docs/overrides/stylesheets/extra.css
@@ -1,148 +1,148 @@
-/* Light mode - very light yellow header */
-[data-md-color-scheme="default"] .md-header {
- background-color: #d0d6da; /* Very light yellow background color */
- height: 4rem;
-}
-
-[data-md-color-scheme="default"] .md-header__title {
- color: #23231c;
- padding-left: 2rem;
-}
-
-/* Dark mode - dark gray header */
-[data-md-color-scheme="slate"] .md-header {
- background-color: #333333; /* Dark gray background color */
- /*background-image: url('../img/boriel-basic-sinclair.png');*/
- /*background-repeat: no-repeat;*/
- /*background-position: center center;*/
- /*background-size: auto 90%;*/
- height: 4rem;
-}
-
-[data-md-color-scheme="slate"] .md-header__title {
- color: #af0d0d;
- padding-left: 2rem;
-}
-
-.md-header__title {
- font-family: 'Press Start 2P', cursive;
- }
-
-/* Make the site name responsive */
-.md-header__title .md-header__ellipsis {
- font-size: 1.2rem;
-}
-
-/* Responsive adjustments for smaller screens */
-@media screen and (max-width: 1220px) {
- [data-md-color-scheme="default"] .md-header {
- height: 3.5rem;
- }
-
- [data-md-color-scheme="slate"] .md-header {
- height: 3.5rem;
- }
-
- [data-md-color-scheme="default"] .md-header__title {
- padding-left: 3.0rem;
- }
-
- [data-md-color-scheme="slate"] .md-header__title {
- padding-left: 3.0rem;
- }
-
- .md-header__title .md-header__ellipsis {
- font-size: 1rem;
- }
-
- .md-header__inner {
- display: flex;
- }
-
- .md-header__button {
- margin: 0;
- padding: 0.5rem;
- }
-
- .md-header__button[for="__drawer"] {
- order: 3;
- }
-
- label[for="__search"] {
- order: 2;
- }
-}
-
-.md-header__inner {
- background-image: url('../img/zxbasic_logo.png');
- background-repeat: no-repeat;
- background-position: left center;
- background-size: auto 100%;
- height: 100%;
-}
-
-@media screen and (max-width: 480px) {
- [data-md-color-scheme="default"] .md-header {
- height: 3rem;
- }
-
- [data-md-color-scheme="slate"] .md-header {
- height: 3rem;
- }
-
- [data-md-color-scheme="default"] .md-header__title {
- padding-left: 48px;
- }
-
- [data-md-color-scheme="slate"] .md-header__title {
- padding-left: 48px;
- }
-
- .md-header__title .md-header__ellipsis {
- font-size: 0.8rem;
- }
-
- .md-header__inner {
- display: flex;
- }
-
- .md-header__button {
- margin: 2px;
- padding: 2px;
- }
-
- .md-header__button[for="__drawer"] {
- order: 3; /* Ponlo al final (tras tema y búsqueda) */
- }
-
- label[for="__search"] {
- order: 2;
- }
-}
-
-/*!* Make the logo size 100% of the header *!*/
-.md-header__button.md-logo {
- color: #ff000000;
- height: 100%;
-}
-
-/* Increase font weight for headings */
-h1, h2, h3, h4, h5, h6 {
- /* font-weight: 600; /* Bold */
- font-family: "Roboto Mono", sans-serif;
-}
-
-/* Target Material theme specific heading classes if any */
-.md-typeset h1,
-.md-typeset h2,
-.md-typeset h3,
-.md-typeset h4,
-.md-typeset h5,
-.md-typeset h6 {
- font-weight: 600; /* Bold */
-}
-
-.md-typeset .admonition,
-.md-typeset details {
- font-size: 0.75rem
-}
+/* Light mode - very light yellow header */
+[data-md-color-scheme="default"] .md-header {
+ background-color: #d0d6da; /* Very light yellow background color */
+ height: 4rem;
+}
+
+[data-md-color-scheme="default"] .md-header__title {
+ color: #23231c;
+ padding-left: 2rem;
+}
+
+/* Dark mode - dark gray header */
+[data-md-color-scheme="slate"] .md-header {
+ background-color: #333333; /* Dark gray background color */
+ /*background-image: url('../img/boriel-basic-sinclair.png');*/
+ /*background-repeat: no-repeat;*/
+ /*background-position: center center;*/
+ /*background-size: auto 90%;*/
+ height: 4rem;
+}
+
+[data-md-color-scheme="slate"] .md-header__title {
+ color: #af0d0d;
+ padding-left: 2rem;
+}
+
+.md-header__title {
+ font-family: 'Press Start 2P', cursive;
+ }
+
+/* Make the site name responsive */
+.md-header__title .md-header__ellipsis {
+ font-size: 1.2rem;
+}
+
+/* Responsive adjustments for smaller screens */
+@media screen and (max-width: 1220px) {
+ [data-md-color-scheme="default"] .md-header {
+ height: 3.5rem;
+ }
+
+ [data-md-color-scheme="slate"] .md-header {
+ height: 3.5rem;
+ }
+
+ [data-md-color-scheme="default"] .md-header__title {
+ padding-left: 3.0rem;
+ }
+
+ [data-md-color-scheme="slate"] .md-header__title {
+ padding-left: 3.0rem;
+ }
+
+ .md-header__title .md-header__ellipsis {
+ font-size: 1rem;
+ }
+
+ .md-header__inner {
+ display: flex;
+ }
+
+ .md-header__button {
+ margin: 0;
+ padding: 0.5rem;
+ }
+
+ .md-header__button[for="__drawer"] {
+ order: 3;
+ }
+
+ label[for="__search"] {
+ order: 2;
+ }
+}
+
+.md-header__inner {
+ background-image: url('../img/zxbasic_logo.png');
+ background-repeat: no-repeat;
+ background-position: left center;
+ background-size: auto 100%;
+ height: 100%;
+}
+
+@media screen and (max-width: 480px) {
+ [data-md-color-scheme="default"] .md-header {
+ height: 3rem;
+ }
+
+ [data-md-color-scheme="slate"] .md-header {
+ height: 3rem;
+ }
+
+ [data-md-color-scheme="default"] .md-header__title {
+ padding-left: 48px;
+ }
+
+ [data-md-color-scheme="slate"] .md-header__title {
+ padding-left: 48px;
+ }
+
+ .md-header__title .md-header__ellipsis {
+ font-size: 0.8rem;
+ }
+
+ .md-header__inner {
+ display: flex;
+ }
+
+ .md-header__button {
+ margin: 2px;
+ padding: 2px;
+ }
+
+ .md-header__button[for="__drawer"] {
+ order: 3; /* Ponlo al final (tras tema y búsqueda) */
+ }
+
+ label[for="__search"] {
+ order: 2;
+ }
+}
+
+/*!* Make the logo size 100% of the header *!*/
+.md-header__button.md-logo {
+ color: #ff000000;
+ height: 100%;
+}
+
+/* Increase font weight for headings */
+h1, h2, h3, h4, h5, h6 {
+ /* font-weight: 600; /* Bold */
+ font-family: "Roboto Mono", sans-serif;
+}
+
+/* Target Material theme specific heading classes if any */
+.md-typeset h1,
+.md-typeset h2,
+.md-typeset h3,
+.md-typeset h4,
+.md-typeset h5,
+.md-typeset h6 {
+ font-weight: 600; /* Bold */
+}
+
+.md-typeset .admonition,
+.md-typeset details {
+ font-size: 0.75rem
+}
diff --git a/docs/print.md b/docs/print.md
index e1bf50424..16c9846de 100644
--- a/docs/print.md
+++ b/docs/print.md
@@ -67,11 +67,12 @@ PRINT "Variable 'a' contains the value: "; a
```
## Changing the print position
-You can change the current _cursor_ position using the [AT](at.md) modifier:
+You can change the current _cursor_ position using the [AT](at.md) or [TAB](tab.md) modifiers:
```
PRINT AT 5, 0; "This message starts at ROW 5"
PRINT AT 10, 10; "This message starts at ROW 10, COLUMN 10"
+PRINT TAB 5; "This starts at column 5"
```
Again, you can chain all `PRINT` _items_ using semicolon:
@@ -115,6 +116,7 @@ NEXT i
* [CLS](cls.md)
* [AT](at.md)
+* [TAB](tab.md)
* [PAPER](paper.md)
* [BORDER](border.md)
* [INVERSE](inverse.md)
diff --git a/docs/released_programs.md b/docs/released_programs.md
index a24282e87..d459ed3f7 100644
--- a/docs/released_programs.md
+++ b/docs/released_programs.md
@@ -745,7 +745,7 @@ Year: 2013
Source: Yes
-Link: [https://www.boriel.com/forum/showthread.php?tid=410](https://www.boriel.com/forum/showthread.php?tid=410)
+Link: [https://forum.boriel.com/showthread.php?tid=410](https://forum.boriel.com/showthread.php?tid=410)

@@ -990,7 +990,7 @@ Year: 2016
Source: No
-Link: [https://www.boriel.com/forum/showthread.php?tid=764](https://www.boriel.com/forum/showthread.php?tid=764)
+Link: [https://forum.boriel.com/showthread.php?tid=764](https://forum.boriel.com/showthread.php?tid=764)

@@ -1005,7 +1005,7 @@ Year: 2016
Source: No
-Link: [https://www.boriel.com/forum/showthread.php?tid=747](https://www.boriel.com/forum/showthread.php?tid=747)
+Link: [https://forum.boriel.com/showthread.php?tid=747](https://forum.boriel.com/showthread.php?tid=747)

@@ -1020,7 +1020,7 @@ Year: 2016
Source: No
-Link: [https://www.boriel.com/forum/showthread.php?tid=757](https://www.boriel.com/forum/showthread.php?tid=757)
+Link: [https://forum.boriel.com/showthread.php?tid=757](https://forum.boriel.com/showthread.php?tid=757)
Description:
@@ -1137,7 +1137,7 @@ Year: 2013
Source: Yes
-Link: [https://www.boriel.com/forum/showthread.php?tid=529](https://www.boriel.com/forum/showthread.php?tid=529)
+Link: [https://forum.boriel.com/showthread.php?tid=529](https://forum.boriel.com/showthread.php?tid=529)

@@ -1152,7 +1152,7 @@ Year: 2014
Source: Yes
-Link: [https://www.boriel.com/forum/showthread.php?tid=299&pid=4142#pid4142](https://www.boriel.com/forum/showthread.php?tid=299&pid=4142#pid4142)
+Link: [https://forum.boriel.com/showthread.php?tid=299&pid=4142#pid4142](https://forum.boriel.com/showthread.php?tid=299&pid=4142#pid4142)

@@ -1167,7 +1167,7 @@ Year: 2015
Source: No
-Link: [https://www.boriel.com/forum/showthread.php?tid=644](https://www.boriel.com/forum/showthread.php?tid=644)
+Link: [https://forum.boriel.com/showthread.php?tid=644](https://forum.boriel.com/showthread.php?tid=644)

diff --git a/docs/released_programs/zx81.md b/docs/released_programs/zx81.md
index 8f063ad89..f593f091a 100644
--- a/docs/released_programs/zx81.md
+++ b/docs/released_programs/zx81.md
@@ -36,7 +36,7 @@ Year: 2016
Source: yes
-Link: [https://www.boriel.com/forum/showthread.php?tid=730](https://www.boriel.com/forum/showthread.php?tid=730)
+Link: [https://forum.boriel.com/showthread.php?tid=730](https://forum.boriel.com/showthread.php?tid=730)
Description: a slideshow.
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 75220cf0c..97c7eb6e6 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -6,9 +6,9 @@ charset-normalizer==3.4.0
click==8.1.7
colorama==0.4.6
ghp-import==2.1.0
-idna==3.10
+idna==3.15
Jinja2==3.1.6
-Markdown==3.7
+Markdown==3.8.1
MarkupSafe==3.0.2
mergedeep==1.3.4
mkdocs==1.6.1
@@ -19,14 +19,14 @@ packaging==24.2
paginate==0.5.7
pathspec==0.12.1
platformdirs==4.3.6
-Pygments==2.18.0
-pymdown-extensions==10.12
+Pygments==2.20.*
+pymdown-extensions==10.21.3
python-dateutil==2.9.0.post0
PyYAML==6.0.2
pyyaml_env_tag==0.1
regex==2024.11.6
-requests==2.32.4
+requests==2.33.0
six==1.16.0
-urllib3==2.5.0
+urllib3==2.7.*
watchdog==6.0.0
mkdocs-material
diff --git a/docs/sub.md b/docs/sub.md
index 4734b8f6b..1905a5df7 100644
--- a/docs/sub.md
+++ b/docs/sub.md
@@ -1,6 +1,5 @@
# SUB
-
ZX Basic allows function and subroutines declarations. Sinclair Basic does not allow named subroutines, only calls with the GOSUB command.
A subroutine defined with the SUB statement is invoked directly. Unlike a [FUNCTION](function.md), a SUB does not return a value. This is the fundamental difference between code defined with SUB and code defined with FUNCTION. Other than that, the setup for SUB and FUNCTION are almost identical.
@@ -71,3 +70,4 @@ It will also issue a warning (perhaps you forgot to call it?), that can be ignor
* [ASM](asm.md)
* [END](end.md)
* [RETURN](return.md)
+* [DECLARE](declare.md)
diff --git a/docs/tab.md b/docs/tab.md
new file mode 100644
index 000000000..adf404154
--- /dev/null
+++ b/docs/tab.md
@@ -0,0 +1,28 @@
+# TAB
+
+## Syntax
+```
+PRINT TAB ;
+```
+
+`TAB` is not a statement, but a [PRINT](print.md) modifier. It is used to move the cursor to a specific column.
+The ZX Spectrum screen has 32 columns, numbered from 0 to 31.
+
+When `TAB` is used, the cursor moves to the specified column. If the current position is already past the requested column, the cursor moves to that column on the next line.
+
+
+```
+PRINT TAB 10; "Hello"
+```
+
+The above example will print "Hello" starting at column 10 of the current row.
+
+## Remarks
+* This modifier is Sinclair BASIC compatible.
+* If the value is 32 or more, it is taken modulo 32.
+
+## See also
+* [PRINT](print.md)
+* [AT](at.md)
+* [POS](library/pos.md)
+* [CSRLIN](library/csrlin.md)
diff --git a/docs/to.md b/docs/to.md
new file mode 100644
index 000000000..eef3c9218
--- /dev/null
+++ b/docs/to.md
@@ -0,0 +1,10 @@
+# TO
+
+**TO** is a keyword used to specify a sequence of numbers to be used in a statement.
+
+a TO b (STEP c) will give the statement a sequence of numbers starting at a and ending at [the closest number less than or equal to] b,
+with each number after a being the previous number plus 1, or c if the STEP keyword is used (only supported by FOR).
+In the case of a TO b with a and b integers, the sequence will include both a and b.
+A, B and C can be expressions, can be floating point numbers [FOR i = 0 TO PI / 2 STEP q - circle.bas.md]
+Statements that can be used with TO are FOR, DIM [DIM b(0 TO 10) - dim.md, DIM a(3 TO 5, 1 TO 8) - lbound.md, only dimensions, indexes will only be valid within the declared range], [variable value assignments? NO, not even arrays], NOT array, string indexing [s$(TO N - 1) - left.md, s$(len(s$) - N - 1 TO) - right.md]
+GO TO, despite having TO, is a different statement and these rules do not apply to it
diff --git a/docs/tutorials.md b/docs/tutorials.md
index 0161c463a..6daf018d3 100644
--- a/docs/tutorials.md
+++ b/docs/tutorials.md
@@ -10,7 +10,7 @@ Link: [http://goo.gl/4jPd5](http://goo.gl/4jPd5)
## Making ZX Basic to work with Tommy Gun
Author: Boriel
-Link: [https://www.boriel.com/forum/showthread.php?tid=110](https://www.boriel.com/forum/showthread.php?tid=110)
+Link: [https://forum.boriel.com/showthread.php?tid=110](https://forum.boriel.com/showthread.php?tid=110)
## How to Use Inline Assembly
@@ -32,4 +32,4 @@ and via [Google](http://translate.google.com/translate?sl=auto&tl=en&js=n&prev=_
## Using Beepola with ZX BASIC
Author: LTee
-Link: [https://www.boriel.com/forum/showthread.php?tid=368](https://www.boriel.com/forum/showthread.php?tid=368)
+Link: [https://forum.boriel.com/showthread.php?tid=368](https://forum.boriel.com/showthread.php?tid=368)
diff --git a/docs/zxb.md b/docs/zxb.md
index 298e936d9..2a2bd2087 100644
--- a/docs/zxb.md
+++ b/docs/zxb.md
@@ -53,64 +53,82 @@ You can change the default origin using the -S command line parameter.
ZXB provides several (and useful) command line options. To see them, just type **_zxb.py -h_**, which outputs:
```
- usage: zxb [-h] [-d] [-O OPTIMIZE] [-o OUTPUT_FILE] [-T] [-t] [-B] [-a] [-A]
+ usage: zxbc.py [-h] [-d] [-O OPTIMIZE] [-o OUTPUT_FILE]
+ [-T | -t | -A | -E | --parse-only | -f {asm,bin,ir,sna,tap,tzx,z80}] [-B] [-a]
[-S ORG] [-e STDERR] [--array-base ARRAY_BASE]
- [--string-base STRING_BASE] [-Z] [-H HEAP_SIZE] [--debug-memory]
- [--debug-array] [--strict-bool] [--enable-break] [-E] [--explicit]
- [-D DEFINES] [-M MEMORY_MAP] [-i] [-I INCLUDE_PATH] [--strict]
+ [--string-base STRING_BASE] [-Z] [-H HEAP_SIZE] [--heap-address HEAP_ADDRESS] [--debug-memory]
+ [--debug-array] [--strict-bool] [--enable-break] [--explicit]
+ [-D DEFINES] [-M MEMORY_MAP] [-i] [-I INCLUDE_PATH] [--strict] [--headerless]
[--version]
+ [--append-binary APPEND_BINARY] [--append-headless-binary APPEND_HEADLESS_BINARY]
+ [-N] [--arch ARCH]
+ [--expect-warnings EXPECT_WARNINGS] [-W DISABLE_WARNING] [+W ENABLE_WARNING] [--hide-warning-codes]
+ [-F CONFIG_FILE] [--save-config SAVE_CONFIG] [--opt-strategy {size,speed,auto}]
PROGRAM
positional arguments:
PROGRAM BASIC program file
- optional arguments:
+ options:
-h, --help show this help message and exit
- -d, --debug Enable verbosity/debugging output. Additional -d
- increase verbosity/debug level
- -O OPTIMIZE, --optimize OPTIMIZE
- Sets optimization level. 0 = None (default level is 2)
- -o OUTPUT_FILE, --output OUTPUT_FILE
- Sets output file. Default is input filename with .bin
- extension
- -T, --tzx Sets output format to tzx (default is .bin)
- -t, --tap Sets output format to tap (default is .bin)
- -B, --BASIC Creates a BASIC loader which loads the rest of the
- CODE. Requires -T or -t
+ -d, --debug Enable verbosity/debugging output. Additional -d increase verbosity/debug level
+ -O, --optimize OPTIMIZE
+ Sets optimization level. 0 = None (default level is 2
+ -o, --output OUTPUT_FILE
+ Sets output file. Default is input filename with .bin extension
+ -T, --tzx Sets output format to .tzx (default is .bin).
+ -t, --tap Sets output format to .tap (default is .bin).
+ -A, --asm Sets output format to .asm. DEPRECATED. Use -f
+ -E, --emit-backend Emits backend code (IR) instead of ASM or binary.
+ --parse-only Only parses to check for syntax and semantic errors
+ -f, --output-format {asm,bin,ir,sna,tap,tzx,z80}
+ Output format
+ -B, --BASIC Creates a BASIC loader which loads the rest of the CODE. Requires -T ot -t
-a, --autorun Sets the program to be run once loaded
- -A, --asm Sets output format to asm
- -S ORG, --org ORG Start of machine code. By default 32768
- -e STDERR, --errmsg STDERR
- Error messages file (standard error console by
- default)
+ -S, --org ORG Start of machine code. By default 32768
+ -e, --errmsg STDERR Error messages file (standard error console by default)
--array-base ARRAY_BASE
Default lower index for arrays (0 by default)
--string-base STRING_BASE
Default lower index for strings (0 by default)
- -Z, --sinclair Enable by default some more original ZX Spectrum
- Sinclair BASIC features: ATTR, SCREEN$, POINT
- -H HEAP_SIZE, --heap-size HEAP_SIZE
+ -Z, --sinclair Enable by default some more original ZX Spectrum Sinclair BASIC features: ATTR, SCREEN$, POINT
+ -H, --heap-size HEAP_SIZE
Sets heap size in bytes (default 4768 bytes)
- --heap-size HEAP_ADDRESS
- Sets the start address of the heap
+ --heap-address HEAP_ADDRESS
+ Sets the heap address.
--debug-memory Enables out-of-memory debug
--debug-array Enables array boundary checking
- --strict-bool Enforce boolean values to be 0 or 1
+ --strict-bool Enforce boolean values to be 0 or 1 (Deprecated)
--enable-break Enables program execution BREAK detection
- -E, --emit-backend Emits backend code instead of ASM or binary
- --explicit Requires all variables and functions to be declared
- before used
- -D DEFINES, --define DEFINES
- Defines de given macro. Eg. -D MYDEBUG or -D
- NAME=Value
- -M MEMORY_MAP, --mmap MEMORY_MAP
+ --explicit Requires all variables and functions to be declared before used
+ -D, --define DEFINES Defines de given macro. Eg. -D MYDEBUG or -D NAME=Value
+ -M, --mmap MEMORY_MAP
Generate label memory map
- -i, --ignore-case Ignore case. Makes variable names are case insensitive
- -I INCLUDE_PATH, --include-path INCLUDE_PATH
- Add colon separated list of directories to add to
- include path. e.g. -I dir1:dir2
+ -i, --ignore-case Ignore case. Makes variable and function names insensitive
+ -I, --include-path INCLUDE_PATH
+ Add colon separated list of directories to add to include path. e.g. -I dir1:dir2
--strict Enables strict mode. Force explicit type declaration
+ --headerless Header-less mode: omit asm prologue and epilogue
--version show program's version number and exit
+ --append-binary APPEND_BINARY
+ Appends binary to tape file (only works with -t or -T)
+ --append-headless-binary APPEND_HEADLESS_BINARY
+ Appends binary to tape file (only works with -t or -T)
+ -N, --zxnext Enables ZX Next asm extended opcodes
+ --arch ARCH Target architecture (defaults is'zx48k'). Available architectures: zx48k,zxnext
+ --expect-warnings EXPECT_WARNINGS
+ Expects N warnings: first N warnings will be silenced
+ -W, --disable-warning DISABLE_WARNING
+ Disables warning WXXX (i.e. -W100 disables warning with code W100)
+ +W, --enable-warning ENABLE_WARNING
+ Enables warning WXXX (i.e. -W100 disables warning with code W100)
+ --hide-warning-codes Hides WXXX codes
+ -F, --config-file CONFIG_FILE
+ Loads config from config file
+ --save-config SAVE_CONFIG
+ Save options into a config file
+ --opt-strategy {size,speed,auto}
+ Optimization strategy (optimize for speed or size). Default: auto
```
Some options (-h, --version) are quite obvious. Let's focus on the rest:
@@ -193,10 +211,11 @@ _Out of Memory_ errors.
Setting this flag will raise ROM error Subscript out of Range. This flag will add a little overhead to your program
execution, but it's useful to detect Out of Range errors.
-* **--strict-bool**
+* **--strict-bool** DEPRECATED
By default, ZX BASIC will treat boolean values as 0 = False, Any other value = True.
Some programmers expect TRUE = 1 always. Using this option will enforce boolean results to be always 0 or 1.
Using this option might add a little overhead to your program. Using `--sinclair` option will also enable this feature.
+This option is currently deprecated, and will be removed in a future release.
* **--enable-break**
Unlike Sinclair BASIC, Your program, being converted to machine code, won't be affected by BREAK.
diff --git a/examples/maskedsprites.bas b/examples/maskedsprites.bas
new file mode 100644
index 000000000..dbf80a092
--- /dev/null
+++ b/examples/maskedsprites.bas
@@ -0,0 +1,784 @@
+' ----------------------------------------------------------------
+' This file is released under the MIT License
+'
+' Copyright (C) 2026 Conrado Badenas
+'
+' Example for
+' Print Masked (AND+OR) Sprites, version 2026.04.05
+' ----------------------------------------------------------------
+
+' DEFINEs to control the library cb/maskedsprites.bas
+ #define NumberofMaskedSprites 10 ' Try 50 here, with #define USE_MSFS
+ #define MaskedSprites_USE_STACK_TRANSFER
+'#include "maskedsprites.bas"
+ #include
+
+' DEFINEs to control this example program
+ #define NUMWAITS -1
+ #define BORDER_WAIT 0
+ #define USE_MSFS
+ #define INTS di
+
+#include
+
+CONST NUMSPRITES as UByte = NumberofMaskedSprites
+
+dim i as Byte
+dim memoryPaging,numberofscrolls,loops,warnings,modulus as UByte
+dim x0(0 to NUMSPRITES-1) as UByte
+dim y0(0 to NUMSPRITES-1) as UByte
+dim x1(0 to NUMSPRITES-1) as UByte
+dim y1(0 to NUMSPRITES-1) as UByte
+dim back(0 to NUMSPRITES-1) as UInteger
+#ifdef USE_MSFS
+ dim regs(0 to NUMSPRITES-1) as UInteger
+ dim regHero0,regFoe00,regFoe20,regFoe21,regFoe22,regFoe23 as UInteger
+#endif
+dim temp as Integer
+dim pointx1,pointx2,pointy1,pointy2 as Integer 'for subtractions
+
+ASM
+ INTS
+END ASM
+
+for i=0 to NUMSPRITES-1
+ y0(i)=255:y1(i)=255:x1(i)=randomLimit(240)
+next i:x1(0)=120
+
+'DIM Rosquilla_Sprite(31) AS UByte => { _
+' $00,$07,$18,$20,$23,$44,$48,$48, _
+' $48,$48,$44,$23,$20,$18,$07,$00, _
+' $00,$E0,$18,$04,$C4,$22,$12,$12, _
+' $12,$12,$22,$C4,$04,$18,$E0,$00 _
+' }
+DIM Rosquilla0(31) AS UByte => { _
+ $00,$07,$19,$21,$23,$44,$48,$48, _
+ $48,$58,$7C,$3B,$20,$18,$07,$00, _
+ $00,$E0,$98,$84,$C4,$22,$12,$12, _
+ $12,$1A,$3E,$DC,$04,$18,$E0,$00 _
+ }
+DIM Rosquilla1(31) AS UByte => { _
+ $00,$07,$1C,$2E,$2F,$44,$48,$48, _
+ $48,$48,$44,$2F,$2E,$1C,$07,$00, _
+ $00,$E0,$18,$04,$C4,$22,$12,$1E, _
+ $1E,$12,$22,$C4,$04,$18,$E0,$00 _
+ }
+DIM Rosquilla2(31) AS UByte => { _
+ $00,$07,$18,$20,$3B,$7C,$58,$48, _
+ $48,$48,$44,$23,$21,$19,$07,$00, _
+ $00,$E0,$18,$04,$DC,$3E,$1A,$12, _
+ $12,$12,$22,$C4,$84,$98,$E0,$00 _
+ }
+DIM Rosquilla3(31) AS UByte => { _
+ $00,$07,$18,$20,$23,$44,$48,$78, _
+ $78,$48,$44,$23,$20,$18,$07,$00, _
+ $00,$E0,$38,$74,$F4,$22,$12,$12, _
+ $12,$12,$22,$F4,$74,$38,$E0,$00 _
+ }
+memoryPaging = CheckMemoryPaging()
+if memoryPaging then SetVisibleScreen(5) '128K
+paper 1:ink 7:border 0:cls
+
+for i=0 to 126
+ pointx1=randomLimit(255):pointy1=randomLimit(191)
+ pointx2=randomLimit(255):pointy2=randomLimit(191)
+ plot pointx1,pointy1:draw pointx2-pointx1,pointy2-pointy1
+next i
+
+#ifdef USE_MSFS
+ print "Init MSFS at ";InitMaskedSpritesFileSystem()
+ print "Free Blocks in MSFS = ";GetNumberofFreeBlocksInMSFS()
+ regHero0 = RegisterSpriteImageInMSFS(@hero0):if regHero0=0 then STOP
+ print "regHero0 = ";regHero0
+ regFoe00 = RegisterSpriteImageInMSFS(@foe00):if regFoe00=0 then STOP
+ print "regFoe00 = ";regFoe00
+ regFoe20 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla0(0),@Rosquilla_Sprite_Mask):if regFoe20=0 then STOP
+ print "regFoe20 = ";regFoe20
+ regFoe21 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla1(0),@Rosquilla_Sprite_Mask):if regFoe21=0 then STOP
+ print "regFoe21 = ";regFoe21
+ regFoe22 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla2(0),@Rosquilla_Sprite_Mask):if regFoe22=0 then STOP
+ print "regFoe22 = ";regFoe22
+ regFoe23 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla3(0),@Rosquilla_Sprite_Mask):if regFoe23=0 then STOP
+ print "regFoe23 = ";regFoe23
+
+ for i=0 to NUMSPRITES-1: modulus = i mod 2
+ if i=0 then
+ regs(0)=regHero0
+ elseif modulus=1 then
+ regs(i)=regFoe00
+ else ' modulus=0 with i>0
+ regs(i)=regFoe20
+ end if
+ next i
+' pause 0
+#endif
+
+for i=0 to NUMSPRITES-1
+ back(i)=MaskedSpritesBackground(i)
+' print back(i)
+next i
+' print:pause 0
+
+if memoryPaging then '128K
+ dim back0(0 to NUMSPRITES-1) as UInteger
+ ChangeMaskedSpritesBackgroundSet
+ for i=0 to NUMSPRITES-1
+ back0(i)=MaskedSpritesBackground(i)
+' print back0(i)
+ next i
+ ChangeMaskedSpritesBackgroundSet
+' print:print MaskedSpritesFileSystemStart:pause 0
+
+ CopyScreen5ToScreen7()
+' print at 11,8;"This is Screen5"
+ SetDrawingScreen7() 'Bank7 is set at $c000
+' print at 13,8;"This is Screen7"
+ SetDrawingScreen5() 'Bank7 is still set at $c000
+' print at 15,8;"This is Screen5"
+end if
+
+' We start with VisibleScreen = DrawingScreen = 5
+numberofscrolls=0
+warnings=0
+loops=0
+WaitForNewFrame(0)
+do
+
+ ' 128K
+ if memoryPaging then
+ ' The brand-new VisibleScreen is ready for the ULA to show it on TV
+ ' But we wait to the beginning of a frame to avoid tearing
+ ' (unless wanted time-to-wait has already been spent)
+ out 254,BORDER_WAIT:WaitForNewFrame(NUMWAITS):out 254,0
+'GetInterruptStatusInBorder()
+ ToggleVisibleScreen()
+ ' Now we can modify DrawingScreen because it is not Visible
+
+ ' We restore background in x0,y0 if it is saved in MaskedSpritesBackground
+ if MaskedSpritesBackgroundSet then
+ for i=NUMSPRITES-1 to 0 step -1
+ if y0(i)<>255 then RestoreBackground(x0(i),y0(i),back0(i))
+ next i
+ else
+ for i=NUMSPRITES-1 to 0 step -1
+ if y0(i)<>255 then RestoreBackground(x0(i),y0(i),back(i))
+ next i
+ end if
+ end if
+
+ ' After restoring background in a Spectrum 128K, you can use CPU to do something else:
+ '
+ ' 0. Change/scroll background (ONLY NOW!!!)
+ ' 1. Check collisions
+ ' 2. Resolve collisions
+ ' 3. Change images of sprites for:
+ ' a. Animation of regular motion
+ ' b. Explosions and other extraordinary events
+ ' 4. Upgrade the score
+ ' 5. Play sound/music
+ ' ...
+ '
+ ' You have plenty of time to do whatever you want in a Spectrum 128K
+
+ ' 0. Change/scroll background (ONLY NOW!!!)
+ if memoryPaging=1 then '128K
+ if (in($7ffe) bAND 1)=0 then
+ if numberofscrolls=1 then
+ Scroll2()
+ else
+ Scroll1()
+ numberofscrolls=1
+ end if
+ else
+ if numberofscrolls=1 then
+ Scroll1()
+ end if
+ numberofscrolls=0
+ end if
+ end if
+
+#ifdef USE_MSFS
+ ' 3. Change images of sprites for: a. Animation of regular motion
+ for i=2 to NUMSPRITES-1 step 2: modulus = (i+loops) mod 4
+ if modulus=0 then
+ regs(i)=regFoe20
+ elseif modulus=1 then
+ regs(i)=regFoe21
+ elseif modulus=2 then
+ regs(i)=regFoe22
+ else
+ regs(i)=regFoe23
+ end if
+ next i
+#endif
+
+ ' We update x0,y0 and compute new x1,y1
+ for i=1 to NUMSPRITES-1
+ temp=x1(i): x0(i)=temp
+ temp=temp-1
+ if temp<0 then temp=240
+ x1(i)=temp
+ temp=y1(i): y0(i)=temp
+ if temp=255 then temp=randomLimit(176)
+ if randomLimit(3)=0 then temp=temp+randomLimit(2)-1
+ if temp<0 then temp=0
+ if temp>176 then temp=176
+ y1(i)=temp
+ next i
+
+ 'Special code for the hero
+ temp=x1(0): x0(0)=temp
+ temp=temp+1 -2*(in($dffe) bAND 1) +2*((in($dffe)>>1) bAND 1) '-noP+noO
+ if temp<0 then temp=240
+ if temp>240 then temp=0
+ x1(0)=temp
+ temp=y1(0): y0(0)=temp
+ if temp=255 then temp=randomLimit(176)
+ temp=temp -(in($fdfe) bAND 1) +(in($fbfe) bAND 1) '-noA+noQ
+ if temp<0 then temp=0
+ if temp>176 then temp=176
+ y1(0)=temp
+
+ ' 48K We restore background in x0,y0 if it is saved in MaskedSpritesBackground
+ if not memoryPaging then
+ for i=NUMSPRITES-1 to 0 step -1
+ if y0(i)<>255 then RestoreBackground(x0(i),y0(i),back(i))
+ next i
+ end if
+
+#ifdef USE_MSFS
+ ' Print WARNING (if not scrolling) twice if MSFS has run out of free blocks
+ if not numberofscrolls and warnings<2 and not GetNumberofFreeBlocksInMSFS() then
+ print at 23,0;"WARNING! MSFS has NO free blocks";
+ warnings=warnings+1
+ end if
+#endif
+
+ ' After restoring background in a Spectrum 48K, you should do NOTHING
+ ' ...
+ ' You have ABSOLUTELY NO TIME to do anything in a Spectrum 48K
+
+ ' We save background in MaskedSpritesBackground and draw sprites in x,y...
+#ifdef USE_MSFS
+ ' ...using SaveBackgroundAndDrawSpriteRegisteredInMSFS(,,,)
+ if MaskedSpritesBackgroundSet then
+ for i=0 to NUMSPRITES-1
+ SaveBackgroundAndDrawSpriteRegisteredInMSFS(x1(i),y1(i),back0(i),regs(i))
+ next i
+ else
+ for i=0 to NUMSPRITES-1
+ SaveBackgroundAndDrawSpriteRegisteredInMSFS(x1(i),y1(i),back(i),regs(i))
+ next i
+ end if
+#else
+ ' ...using SaveBackgroundAndDrawSprite(,,,)
+ if MaskedSpritesBackgroundSet then
+ SaveBackgroundAndDrawSprite(x1(0),y1(0),back0(0),@hero0)
+ for i=1 to NUMSPRITES-1
+ SaveBackgroundAndDrawSprite(x1(i),y1(i),back0(i),@foe00)
+ next i
+ else
+ SaveBackgroundAndDrawSprite(x1(0),y1(0),back(0),@hero0)
+ for i=1 to NUMSPRITES-1
+ SaveBackgroundAndDrawSprite(x1(i),y1(i),back(i),@foe00)
+ next i
+ end if
+#endif
+
+ ' After drawing sprites, you can use CPU to do something else:
+ '
+ ' 1. Check collisions
+ ' 2. Resolve collisions
+ ' 3. Change images of sprites for:
+ ' a. Animation of regular motion
+ ' b. Explosions and other extraordinary events
+ ' 4. Upgrade the score
+ ' 5. Play sound/music
+ ' ...
+ '
+ ' You have plenty of time to do whatever you want in ANY Spectrum
+
+ ' We change the Set of Backgrounds (128K) for next iteration
+ if memoryPaging then ChangeMaskedSpritesBackgroundSet()
+
+ ' We have finished drawing
+ if memoryPaging then
+ ' We change DrawingScreen (128K) for next iteration
+ ToggleDrawingScreen()
+ else
+ ' ULA shows DrawingScreen (48K) on TV for some frames before going on
+ out 254,BORDER_WAIT:WaitForNewFrame(NUMWAITS):out 254,0
+ end if
+'GetInterruptStatusInBorder()
+ ' Now, VisibleScreen = DrawingScreen, and
+ ' we could see anything we draw whilst it is drawn
+ loops=loops+1
+loop
+
+' SUB/FUNCTIONs that are not used, are "used" here to check compilation is OK
+ SetBankPreservingINTs(0)
+print GetBankPreservingRegs()
+print CheckMemoryPaging()
+ SetVisibleScreen(0)
+print GetVisibleScreen()
+ ToggleVisibleScreen()
+ CopyScreen5ToScreen7()
+ CopyScreen7ToScreen5()
+ SetDrawingScreen5()
+print SetDrawingScreen7()
+ ToggleDrawingScreen()
+print ChangeMaskedSpritesBackgroundSet()
+ SaveBackgroundAndDrawSprite(0,0,0,0)
+ RestoreBackground(0,0,0)
+print InitMaskedSpritesFileSystem()
+print GetNumberofFreeBlocksInMSFS()
+print RegisterSpriteImageInMSFS(0)
+print RegisterSpriteGraphAndMaskInMSFS(0,0)
+ SaveBackgroundAndDrawSpriteRegisteredInMSFS(0,0,0,0)
+GetInterruptStatusInBorder()
+Scroll()
+Scroll1()
+Scroll2()
+' Delete/Comment all these "uses" when used, or compilation checking is not needed
+
+hero0:
+ASM
+ defb %11000000,%00000000,%00001111,%00000000;1
+ defb %10000000,%00011111,%00000000,%11100000;2
+ defb %00000000,%00100000,%00000000,%00111110;3
+ defb %00000000,%01001110,%00000000,%11111000;4
+ defb %00000000,%01011001,%00000001,%00001100;5
+ defb %00000000,%01011010,%00000000,%00010100;6
+ defb %00000000,%00000011,%00000000,%11100010;7
+ defb %00000000,%01111100,%00000000,%00100010;8
+ defb %00000000,%01111100,%00000000,%00100010;8
+ defb %00000000,%00000011,%00000000,%11100010;7
+ defb %00000000,%01011010,%00000000,%00010100;6
+ defb %00000000,%01011001,%00000001,%00001100;5
+ defb %00000000,%01001110,%00000000,%11111000;4
+ defb %00000000,%00100000,%00000000,%00111110;3
+ defb %10000000,%00011111,%00000000,%11100000;2
+ defb %11000000,%00000000,%00001111,%00000000;1
+END ASM
+
+foe00:
+ASM
+ defb %00000011,%00000000,%11000000,%00000000;1
+ defb %00000011,%01111000,%11000000,%00011110;2
+ defb %00000011,%01000000,%11000000,%00010000;3
+ defb %00000001,%01010000,%10000000,%00010100;4
+ defb %00000000,%01001000,%00000000,%00100010;5
+ defb %00000000,%00000010,%00000000,%01110000;6
+ defb %11100000,%00000111,%00000111,%11100000;7
+ defb %11110000,%00000010,%00001111,%01000000;8
+ defb %11110000,%00000010,%00001111,%01000000;8
+ defb %11100000,%00000111,%00000111,%11100000;7
+ defb %00000000,%00001110,%00000000,%01000000;6'
+ defb %00000000,%01110100,%00000000,%00011110;5'
+ defb %00000001,%01000000,%10000000,%00010000;4'
+ defb %00000011,%01010000,%11000000,%00010100;3'
+ defb %00000011,%01001000,%11000000,%00010010;2'
+ defb %00000011,%00000000,%11000000,%00000000;1
+END ASM
+
+Rosquilla_Sprite_Mask:
+ASM
+ db $FF,$F8,$E0,$C0,$C0,$83,$87,$87
+ db $87,$87,$83,$C0,$C0,$E0,$F8,$FF
+ db $FF,$1F,$07,$03,$03,$C1,$E1,$E1
+ db $E1,$E1,$C1,$03,$03,$07,$1F,$FF
+END ASM
+
+
+' ----------------------------------------------------------------
+' READ_IFF2 is a MACRO that reads correctly the IFF2 flip-flop,
+' avoiding a "bug" reported in the Z80 User Manual,
+' which Pedro Picapiedra aka ProgramadorHedonista kindly pointed
+' me to: "If an interrupt occurs during execution of this
+' instruction [LD A,I or LD A,R], the Parity flag contains a 0."
+' ----------------------------------------------------------------
+#define READ_IFF2 \
+ ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even \
+ jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI \
+ ld a,i ; read IFF2 again to ensure that IFF2=0=DI \
+1:
+
+
+' ----------------------------------------------------------------
+' Wait for New Frame
+' This SUB uses a hack to ensure a good reading of the IFF2 flip-flop.
+' Hack was found thanks to Pedro Picapiedra aka ProgramadorHedonista.
+' Parameters:
+' Byte: if =0, you want just one HALT
+' if >0, minimum number of frames spent since last wait
+' if <0, return without waiting
+' ----------------------------------------------------------------
+SUB FASTCALL WaitForNewFrame(minimumNumberofFramesToWaitSinceLastWait AS Byte)
+ASM
+ PROC
+ LOCAL wait,temp
+ and a
+ ret m ; return if minimumNumberofFramesToWaitSinceLastWait < 0
+ ld hl,temp
+ ld de,23672
+ ld c,a ; A = C = minimumNumberofFramesToWaitSinceLastWait
+ READ_IFF2
+ ex af,af'
+ ei ; interrupts MUST be enabled before HALT
+ halt
+wait:
+ ld a,(de)
+ sub (hl) ; A = number of frames since last RET
+ cp c ; is A >= C? Yes: NoCarry. No: Carry
+ jr c,wait ; repeat if A < minimumNumberofFramesToWaitSinceLastWait
+ ld a,(de)
+ ld (hl),a
+ ex af,af'
+ ret pe ; Return with EI if IFF2=1=EI at the beginning
+ di ; Return with DI if IFF2=0=DI at the beginning
+ RET
+temp:
+ defb 0
+ ENDP
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' Get Interrupt Status in Border
+' This SUB uses a hack to ensure a good reading of the IFF2 flip-flop.
+' Hack was found thanks to Pedro Picapiedra aka ProgramadorHedonista.
+' Border results: 2 Red if Interrupts are Disabled IFF2=0
+' 4 Green if Interrupts are Enabled IFF2=1
+' ----------------------------------------------------------------
+SUB FASTCALL GetInterruptStatusInBorder()
+ASM
+ READ_IFF2
+ push af
+ pop bc
+ ld a,c
+ and 4 ; A=0/4 iff IFF2=0/1
+ rra ; A=0/2 iff IFF2=0/1
+ add a,2 ; A=2/4 iff IFF2=0/1
+ out (254),a
+END ASM
+END SUB
+
+
+SUB FASTCALL Scroll()
+ASM
+ PROC
+ LOCAL loop
+ ld hl,(.core.SCREEN_ADDR)
+ ld de,31
+ ld b,192
+loop:
+; Here, HL = screen address for first column
+ add hl,de; column 32
+ xor a
+ rl (hl) ; 32
+ dec l
+ rl (hl) ; 31
+ dec l
+ rl (hl) ; 30
+ dec l
+ rl (hl) ; 29
+ dec l
+ rl (hl) ; 28
+ dec l
+ rl (hl) ; 27
+ dec l
+ rl (hl) ; 26
+ dec l
+ rl (hl) ; 25
+ dec l
+ rl (hl) ; 24
+ dec l
+ rl (hl) ; 23
+ dec l
+ rl (hl) ; 22
+ dec l
+ rl (hl) ; 21
+ dec l
+ rl (hl) ; 20
+ dec l
+ rl (hl) ; 19
+ dec l
+ rl (hl) ; 18
+ dec l
+ rl (hl) ; 17
+ dec l
+ rl (hl) ; 16
+ dec l
+ rl (hl) ; 15
+ dec l
+ rl (hl) ; 14
+ dec l
+ rl (hl) ; 13
+ dec l
+ rl (hl) ; 12
+ dec l
+ rl (hl) ; 11
+ dec l
+ rl (hl) ; 10
+ dec l
+ rl (hl) ; 09
+ dec l
+ rl (hl) ; 08
+ dec l
+ rl (hl) ; 07
+ dec l
+ rl (hl) ; 06
+ dec l
+ rl (hl) ; 05
+ dec l
+ rl (hl) ; 04
+ dec l
+ rl (hl) ; 03
+ dec l
+ rl (hl) ; 02
+ dec l
+ rl (hl) ; 01
+ rla ; save CF in A
+ add hl,de; column 32
+ or (hl)
+ ld (hl),a
+ inc hl ; first column
+ djnz loop
+ ENDP
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' NEXT_ROW is a MACRO of ASM code, based on code from
+' https://zonadepruebas.com/viewtopic.php?f=15&t=8372&start=40#p81507
+' and found by Joaquin Ferrero
+' ----------------------------------------------------------------
+#define NEXT_ROW \
+ sub 224 ; 7 A was L, now A = L + 32 (SUB 224 is similar to +32) \
+ ; CF = 0/1 iff E >=/< 224 iff a third is/isn't crossed \
+ ld l,a ; 4 \
+ sbc a,a ; 4 A = 0/255 \
+ and 248 ; 7 A = 0/248 (248 = -8) \
+ add a,h ; 4 A = H/H-8 iff a third is/isn't crossed \
+ ld h,a ; 4 += 30 Ts
+
+
+' ----------------------------------------------------------------
+' Scroll 1 pixel up
+' ----------------------------------------------------------------
+SUB FASTCALL Scroll1()
+ASM
+ PROC
+ LOCAL loop1,loop2,exit
+ push ix
+ xor a
+ ld hl,(.core.SCREEN_ADDR); HL = $4000 or $c000
+ ld de,buffer0
+ call ldi32 ; scanline0
+ ; DEC HL is not needed because row = 0 <> 7, 15, 23
+ ld ixh,24 ; 24 rows (rows 0-23)
+loop1:
+ ld l,a ; A = low byte of screen addresses for this row
+; HL = scanline0 for this row
+ ld d,h
+ ld e,l ; DE = scanline0 of this row
+ inc h ; HL = scanline1 of this row
+ ld ixl,7 ; first 7 scanlines of one row
+loop2:
+; HL,DE = origin,destination screen address
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi ; BC-=32 DE+=32 HL+=32 (if E,L was 224, then D,H changes)
+ dec hl ; 6 in case H changes (because L was 224: rows 7,15,23)
+ ld l,a ; +4 = 10 Ts < 21 Ts (PUSH+POP)
+ ld d,h
+ ld e,l ; scanline_N+0 of this row
+ inc h ; scanline_N+1 of this row
+ dec ixl
+ jp nz,loop2
+ dec ixh
+ jr z,exit ; this JR Z here (instead of an JP NZ later) adds +7 Ts/row = 161 Ts for 23 rows
+; last scanline of first 23 rows: DE is OK, HL must change
+ NEXT_ROW
+ ld a,l
+ call ldi32 ; 539 Ts needed for each row except last row
+ dec hl ; with JR Z,EXIT before, we save 539-161 = 378 Ts for last row
+ jp loop1
+exit:
+ ld hl,buffer0
+ call ldi32 ; DE = scanline7 of last row
+ pop ix
+ ENDP
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' Scroll 2 pixels up
+' ----------------------------------------------------------------
+SUB FASTCALL Scroll2()
+ASM
+ PROC
+ LOCAL loop1,loop2,exit
+ push ix
+ xor a
+ ld hl,(.core.SCREEN_ADDR); HL = $4000 or $c000
+ ld de,buffer0
+ call ldi32 ; scanline0
+ ; DEC HL is not needed because row = 0 <> 7, 15, 23
+ ld l,a
+ inc h
+ call ldi32 ; scanline1
+ ; DEC HL is not needed because row = 0 <> 7, 15, 23
+ ld ixh,24 ; 24 rows (rows 0-23)
+loop1:
+ ld l,a ; A = low byte of screen addresses for this row
+; HL = scanline1 for this row
+ ld d,h
+ ld e,l
+ dec d ; DE = scanline0 of this row
+ inc h ; HL = scanline2 of this row
+ ld ixl,6 ; first 6 scanlines of one row
+loop2:
+; HL,DE = origin,destination screen address
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi ; BC-=32 DE+=32 HL+=32 (if E,L was 224, then D,H changes)
+ dec hl ; 6 in case H changes (because L was 224: rows 7,15,23)
+ ld l,a ; +4 = 10 Ts < 21 Ts (PUSH+POP)
+ ld d,h
+ ld e,l
+ dec d ; DE = scanline_N+0 of this row
+ inc h ; HL = scanline_N+2 of this row
+ dec ixl
+ jp nz,loop2
+ dec ixh
+ jr z,exit ; this JR Z here (instead of an JP NZ later) adds +7 Ts/row = 161 Ts for 23 rows
+; last scanline of first 23 rows: DE is OK, HL must change
+ NEXT_ROW
+ ld c,h ; C=H is larger than 32, so B does not change when BC-=32
+ ld b,e ; DE = scanline6 of this row SAVED in B
+ ld a,l ; HL = scanline0' of next row SAVED in A
+ call ldi32 ; 539 Ts needed for each row except last row
+ dec hl
+ ld l,a
+ dec de ; LD C,H + LD B,E + LD A,L + DEC HL + LD L,A + DEC DE + LD E,B
+ ld e,b ; = 4+4+4+6+4+6+4 = 32 Ts < 42 Ts 2*(PUSH+POP)
+ inc d ; DE = scanline7 of this row
+ inc h ; HL = scanline1' of next row
+ call ldi32 ; 539 Ts needed for each row except last row
+ dec hl ; with JR Z,EXIT before, we save 2*539-161 = 917 Ts for last row
+ jp loop1
+exit:
+ ld hl,buffer0
+ call ldi32 ; DE = scanline6 of last row
+ dec de
+ ld e,a
+ inc d ; DE = scanline7 of last row
+ call ldi32
+ pop ix
+ ret
+ ENDP
+
+buffer0:
+ defs 32
+
+buffer1:
+ defs 32
+
+ldi32:
+ ldi ; 32 LDI = 32*16 = 512 Ts
+ ldi ; CALL + RET = 17+10 = 27 Ts
+ ldi ; call ldi32 (+ret) = 512+27 = 539 Ts
+ ldi
+ ldi ; LD BC,32 + LDIR = 10 + 21*31+16 = 677 Ts
+ ldi
+ ldi ; 677 - 539 = 138 Ts
+ ldi
+ ldi ; then, "call ldi32 (+ret)" is 138 Ts faster than "ld bc,32 + ldir"
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi ; BC-=32 DE+=32 HL+=32 (if E,L was >=224, then D,H changes)
+END ASM
+END SUB
+
diff --git a/examples/play/chaos.bas b/examples/play/chaos.bas
new file mode 100644
index 000000000..7df5c978a
--- /dev/null
+++ b/examples/play/chaos.bas
@@ -0,0 +1,24 @@
+rem Chaos
+rem (C) 2026 by Ollibony
+
+#include
+
+cls
+
+let x$ = "1 a&&C&&a&&&g&a&&& f&&C&&a&&&f&a&&& g&&D&&b&&&a&b&&& a&&C&&D&&&C&b&g& "
+let y$ = "1 f&&C&&a&&&f&g&&& a&&C&&a&&&g&a&&& g&&D&&b&&&a&b&&& g&&b&&D&&&C&b&g& "
+
+let z$ = "O5 V8 5eV7eV6eV5e V8cV7cV6cV5c (V8dV7dV6dV5d) (V8cV7cV6cV5c) (V8dV7dV6dV5d)"
+
+let a$ = x$ + y$
+let b$ = z$
+let c$ = "V10 1&&& " + x$ + y$
+
+print ink 1; "Channel A"
+print a$: print
+print ink 1; "Channel B"
+print b$: print
+print ink 1; "Channel C"
+print c$: print
+
+Play a$, b$, c$
diff --git a/examples/play/evolution.bas b/examples/play/evolution.bas
new file mode 100644
index 000000000..1da411d8d
--- /dev/null
+++ b/examples/play/evolution.bas
@@ -0,0 +1,31 @@
+rem Evolution
+rem (C) 2026 by Ollibony
+
+#include
+
+cls
+
+let t$ = "T160 W0 X4000 "
+
+let w$ = "V15 O3 1e&&Ee&(e&&&)e& 1d&&Dd&(d&&&)d& 1c&&Cc&(c&&&)c& O2 1a&&Aa&(a&&&)a& "
+let x$ = "O3 1e&&Ee&(e&&&)e& 1g&&Gg&g&&&d&4d1& 1c&&Cc&(c&&&)c& O2 1a&&Aa&a&&&a&a&a& "
+let y$ = "U O4 3GbG1b3G1b3GbG 3BDB1D3B1D4B1BGD 3ECE1C3E1C3EC1EC 3DaD1a6#C1#CE#C "
+let z$ = "3GbG1b3G1b3G O5 1b&D& 3bdb1d3b1d1bdDbgd O4 3ECE1C3E1C3E1CgCb 3ae1aCDE5D#C)"
+
+let i$ = "V15 O5 ((1gab&&&ab&&e&&&d&) 1abC&&&bC&&e&&&d& 1ega&5&&&) "
+let j$ = "U O4 3E&E1&3E1&3E&E 3G&G1&3G1&4G& 3C&C1&3C1&3C&& 3a&a1&6a4& "
+let k$ = "3E&E1&3E1&3E1G&B& 3G&G1&3G1&4&& 3C&C1&3C1&3C&& 3e&1e4&5ee)"
+
+let q$ = "V0 9&&&&& V15 O5 1bCD&&&CD&&b&&&g& 1CDE&&&DE&&C&&&g& 1Cba&5&&& "
+let r$ = "U O4 3b&b1&3b1&3b&b 3D&D1&3D1&4D& 3g&g1&3g1&3g&& 3$F&$F1&6$F4& "
+let s$ = "3b&b1&3b1&3b1E&G& 3D&D1&3D1&4&& 3g&g1&3g1&3g&& 3c&1c4&5dd)"
+
+let a$ = t$ + w$ + x$ + y$ + z$
+let b$ = i$ + j$ + k$
+let c$ = q$ + r$ + s$
+
+print ink 1; a$
+print ink 2; b$
+print ink 3; c$
+
+Play a$, b$, c$
diff --git a/examples/play/readme.md b/examples/play/readme.md
new file mode 100644
index 000000000..a6a3a9c29
--- /dev/null
+++ b/examples/play/readme.md
@@ -0,0 +1,4 @@
+This folder contains examples of using the 'Play' library.
+
+Note that these examples are compatible with ZX Spectrum 128K Basic interpreter,
+if processed with the 'zmakebas' utility (https://github.com/ohnosec/zmakebas) in auto-line-numbering mode.
diff --git a/examples/sd81/block7test.bas b/examples/sd81/block7test.bas
new file mode 100644
index 000000000..7c0104d68
--- /dev/null
+++ b/examples/sd81/block7test.bas
@@ -0,0 +1,63 @@
+' BLOCK7TEST -- verifica si el mapeador ($E7) mapea de verdad la
+' pagina 20 al bloque 7 ($E000-$FFFF), sin nada de MSFS por medio.
+' Escribe un patron dependiente de direccion, cambia a otra pagina y
+' vuelve, y comprueba que el contenido sigue ahi (paginas distintas =
+' contenido independiente) y que el patron se lee correctamente.
+
+#include
+
+DIM addr AS UINTEGER
+DIM v, expected AS UBYTE
+DIM errors AS UINTEGER
+
+CLS
+PRINT "BLOCK7TEST"
+PRINT
+
+' Paso 1: mapear pagina 20 y rellenar con patron
+Map(7, 20)
+PRINT "Escribiendo pagina 20..."
+FOR addr = $E000 TO $E0FF
+ v = addr BAND 255
+ POKE addr, v
+NEXT addr
+
+' Paso 2: cambiar a pagina 63 (la "libre") y rellenar con OTRO patron
+Map(7, 63)
+PRINT "Escribiendo pagina 63..."
+FOR addr = $E000 TO $E0FF
+ v = 255 - (addr BAND 255)
+ POKE addr, v
+NEXT addr
+
+' Paso 3: releer pagina 63 (deberia tener el segundo patron)
+errors = 0
+FOR addr = $E000 TO $E0FF
+ expected = 255 - (addr BAND 255)
+ v = PEEK(addr)
+ IF v <> expected THEN
+ errors = errors + 1
+ IF errors <= 3 THEN PRINT "pagina 63 mal en "; addr; ": "; v; "<>"; expected
+ END IF
+NEXT addr
+PRINT "Pagina 63 tras releer: ";
+IF errors = 0 THEN PRINT "OK" ELSE PRINT errors; " errores"
+
+' Paso 4: volver a pagina 20 y comprobar que SIGUE el primer patron
+' (si esto falla con "0", el bloque 7 no esta guardando datos por
+' pagina de verdad -- todas las paginas comparten la misma RAM)
+Map(7, 20)
+errors = 0
+FOR addr = $E000 TO $E0FF
+ expected = addr BAND 255
+ v = PEEK(addr)
+ IF v <> expected THEN
+ errors = errors + 1
+ IF errors <= 3 THEN PRINT "pagina 20 mal en "; addr; ": "; v; "<>"; expected
+ END IF
+NEXT addr
+PRINT "Pagina 20 tras volver: ";
+IF errors = 0 THEN PRINT "OK" ELSE PRINT errors; " errores"
+
+PRINT
+PRINT "FIN"
diff --git a/examples/sd81/bounce_sd81.bas b/examples/sd81/bounce_sd81.bas
new file mode 100644
index 000000000..8a60112fc
--- /dev/null
+++ b/examples/sd81/bounce_sd81.bas
@@ -0,0 +1,191 @@
+' bounce_sd81.bas — SD81 Booster double buffer (dbuf) demo, port $E7
+'
+' ZX BASIC port of bounce.asm (SD81-Booster/EXAMPLES/DBUF), reworked for
+' the zxbasic runtime:
+' - the screen ($C000), Superfast HiRes Spectrum mode and Chroma81
+' colour are already set up by tools/boot1.asm for every zx81sd
+' program -- no need to POKE 2043/2044/2045 or write to $7FEF here.
+' - screen + attributes are already cleared at program start by
+' runtime/bootstrap.asm -- no need to clear them here either.
+' - memory-mapped IO (POKE 2056/2057) is disabled by boot1.asm before
+' the compiled program starts, so the double buffer is controlled
+' through port $E7's pseudo-block-8 path (SD81DBufOn/SD81DBufOff,
+' see stdlib/dbuf.bas) instead of the classic POKE 2057.
+'
+' A 16x16 ball bounces around the screen. Each frame: wait for VSYNC,
+' erase the ball, waste ~6ms on purpose (simulates heavy drawing that
+' crosses the beam), move, redraw.
+' - dbuf ON (green border): solid image, no flicker.
+' - dbuf OFF (red border): visible flicker/tearing.
+' SPACE toggles the double buffer live; M freezes/unfreezes movement;
+' Q quits.
+'
+' This is also the first real test of the port $E7 pseudo-block-8 dbuf
+' path (only the POKE 2057 path had been validated on real hardware
+' before this): see stdlib/dbuf.bas for the front-buffer-block notes.
+
+#include
+
+dim bally as ubyte
+dim ballx as ubyte
+dim dy as byte
+dim dx as byte
+dim moven as ubyte
+dim dbufst as ubyte
+dim k$ as string
+
+bally = 80
+ballx = 14
+dy = 2
+dx = 1
+moven = 1
+dbufst = 1
+
+' PaintBall(y, x, fillVal): fills 16 rows x 2 bytes at screen row y,
+' byte-column x, with fillVal (0 = erase, 255 = draw). Same pixel
+' address formula as the Spectrum, but with $C0 as the screen's base
+' high byte instead of Spectrum's $40 (zx81sd's screen is fixed at
+' $C000, set once by boot1.asm).
+sub PaintBall(y as ubyte, x as ubyte, fillVal as ubyte)
+ asm
+ PROC
+ LOCAL PB_LOOP, PB_PIXAD, PB_DONE
+
+ ld a, (ix + 5) ; y
+ ld d, a
+ ld a, (ix + 7) ; x
+ ld e, a
+ ld a, (ix + 9) ; fillVal
+ ld c, a
+ ld b, 16
+
+PB_LOOP:
+ push bc
+ push de
+ call PB_PIXAD
+ ld (hl), c
+ inc l
+ ld (hl), c
+ pop de
+ pop bc
+ inc d
+ djnz PB_LOOP
+ jr PB_DONE
+
+PB_PIXAD:
+ ld a, d
+ and $C0
+ rrca
+ rrca
+ rrca
+ ld h, a
+ ld a, d
+ and 7
+ or h
+ or $C0 ; screen base high byte ($C000)
+ ld h, a
+ ld a, d
+ and $38
+ add a, a
+ add a, a
+ or e
+ ld l, a
+ ret
+
+PB_DONE:
+ ENDP
+ end asm
+end sub
+
+' ~6ms busy loop at 3.25MHz: simulates a heavy redraw that crosses the
+' video beam, so the difference between dbuf ON/OFF is visible.
+sub Delay
+ asm
+ PROC
+ LOCAL DL_LOOP
+
+ ld bc, 1024
+DL_LOOP:
+ dec bc
+ ld a, b
+ or c
+ jr nz, DL_LOOP
+
+ ENDP
+ end asm
+end sub
+
+' On bounce, the position is left unchanged for this frame (matching
+' bounce.asm) and only the direction flips; it moves next frame.
+'
+' Kept entirely in ubyte (8-bit, mod-256) arithmetic on purpose, same
+' as bounce.asm's "add a,b / cp 177": mixing ubyte+byte and widening to
+' integer sign-extends the 8-bit sum based on its own bit 7 (bally=126,
+' dy=2 -> 8-bit sum 128 gets read as -128, not +128), which is wrong
+' here since bally is genuinely unsigned. Comparing unsigned in ubyte
+' space sidesteps that entirely.
+sub Move
+ dim newY as ubyte
+ dim newX as ubyte
+
+ if moven <> 0 then
+ newY = bally + dy
+ if newY >= 177 then
+ dy = -dy
+ else
+ bally = newY
+ end if
+
+ newX = ballx + dx
+ if newX >= 31 then
+ dx = -dx
+ else
+ ballx = newX
+ end if
+ end if
+end sub
+
+' Returns 0 if Q was pressed (caller should quit), 1 otherwise.
+' SPACE toggles dbuf live; M freezes/unfreezes movement.
+function Keys() as ubyte
+ k$ = INKEY$
+
+ if k$ = " " then
+ dbufst = 1 - dbufst
+ if dbufst <> 0 then
+ SD81DBufOn(5)
+ border 4
+ else
+ SD81DBufOff()
+ border 2
+ end if
+ do
+ loop while INKEY$ <> ""
+ end if
+
+ if k$ = "m" or k$ = "M" then
+ moven = 1 - moven
+ do
+ loop while INKEY$ <> ""
+ end if
+
+ if k$ = "q" or k$ = "Q" then
+ return 0
+ end if
+
+ return 1
+end function
+
+SD81DBufOn(5)
+border 4
+
+do
+ SD81WaitVSync()
+ PaintBall(bally, ballx, 0)
+ Delay
+ Move
+ PaintBall(bally, ballx, 255)
+loop while Keys() <> 0
+
+SD81DBufOff()
+border 7
diff --git a/examples/sd81/filesystem.bas b/examples/sd81/filesystem.bas
new file mode 100644
index 000000000..b21451717
--- /dev/null
+++ b/examples/sd81/filesystem.bas
@@ -0,0 +1,55 @@
+' ----------------------------------------------------------------------
+' filesystem.bas — SD81 Booster MCU file/directory commands
+'
+' Demonstrates the filesystem-management wrappers in mcu.bas: PWD, CD,
+' DIR, MKDIR/RMDIR, DEL, COPY/MOVE and FREE. All of them return the
+' MCU's status byte (0 = success); see the manual's error table for
+' the rest of the codes.
+' ----------------------------------------------------------------------
+
+#include
+#include
+
+dim st as ubyte
+dim buf(15) as ubyte
+dim i as ubyte
+
+print "SD81 MCU version: "; McuVersion()
+print
+
+print "Current dir: "; McuPwd()
+
+print "CD /: "; McuCd("/")
+print "DIR *.*:"
+McuDirPrint("*.*")
+print
+
+print "MKDIR /SD81DEMO: "; McuMkdir("/SD81DEMO")
+print "CD /SD81DEMO: "; McuCd("/SD81DEMO")
+print "Current dir: "; McuPwd()
+
+' a tiny 16-byte file, just to have something to copy/move/delete
+for i = 0 to 15
+ poke @buf(0) + i, i
+next i
+st = McuSave("DEMO.BIN", @buf(0), 16)
+print "SAVE DEMO.BIN: "; st
+
+print "COPY DEMO.BIN -> COPY.BIN: "; McuCopy("DEMO.BIN", "COPY.BIN")
+print "MOVE COPY.BIN -> RENAMED.BIN: "; McuMove("COPY.BIN", "RENAMED.BIN")
+
+print "DIR *.*:"
+McuDirPrint("*.*")
+
+print "DEL DEMO.BIN: "; McuDel("DEMO.BIN")
+print "DEL RENAMED.BIN: "; McuDel("RENAMED.BIN")
+
+print "CD /: "; McuCd("/")
+print "RMDIR /SD81DEMO: "; McuRmdir("/SD81DEMO")
+
+st = McuFree()
+print "FREE: "; McuFreeFreeKb; "/"; McuFreeTotalKb; " KB, status "; st
+
+print
+print "Press a key..."
+a$ = input(20)
diff --git a/examples/sd81/flights_sd81.bas b/examples/sd81/flights_sd81.bas
new file mode 100644
index 000000000..90bb1f693
--- /dev/null
+++ b/examples/sd81/flights_sd81.bas
@@ -0,0 +1,129 @@
+#include
+
+0 DIM kk, c, wx, wy, gz, gy, gx, rw, y1, y2, y3, y4, pow, gc, rb, ll, yc, ad, st, rl, bc, nc, pt, px, vz, vy, vx, n AS Float
+1 BORDER 1: PAPER 1: INK 7: CLS
+2 LET wy=0: LET wx=0: LET gz=0: LET gy=0: LET gx=0
+5 LET rw=0: LET y1=120: LET y2=120: LET y3=40: LET y4=40: LET pow=0: LET gc=0: LET rb=0: LET ll=0: LET yc=0: LET ad=0: LET st=0: LET rl=0: LET bc=0: LET nc=0: LET pt=0: LET px=0: LET vz=0: LET vy=0: LET vx=0
+110 GO TO 5000
+500 LET ra=ad*c: LET vx=as0*SIN ra
+510 LET vy=as0*COS ra: RETURN
+1000 LET pz=pz+gz: LET py=py+gy: LET px=px+gx
+1025 IF st=1 THEN PRINT OVER 1;AT 4,12;"S T A L L": LET st=0: GO TO 1040: END IF
+1030 IF as0<30 THEN GO SUB 1500: END IF
+1040 LET ad=ad+rl: IF ad<0 THEN LET ad=ad+360: END IF
+1050 IF ad>359 THEN LET ad=ad-360: END IF
+1060 LET vz=as0*SIN (pt*c)-10+as0/15
+1070 LET gz=vz: LET gy=vy+wy: LET gx=vx+wx
+1080 IF vy=0 THEN LET gd=-PI/2: GO TO 1100: END IF
+1090 LET gd=-ATN (vx/vy)/c
+1100 GO SUB 500
+1110 RETURN
+1500 LET st=1: PRINT OVER 1;AT 4,12;"S T A L L": FOR m=1 TO 4: FOR n=20 TO -20 STEP -4: BEEP .01,n: NEXT n: NEXT m
+1510 LET rl=INT (RND*21)-9: LET pt=-21-INT (RND*5)
+1520 RETURN
+2000 IF gc<>0 THEN GO SUB 2200: END IF
+2190 LET as0=as0+16*(tc*30-as0-8*pt)/as0: GO SUB 2200: GO TO 2205
+2200 PLOT 35,50+16: DRAW OVER 1;15*SIN (as0*PI/200),15*COS (as0*PI/200): RETURN
+2205 IF gc<>0 THEN PLOT 155,50+16: DRAW OVER 1;10*SIN (tn*PI/5),10*COS (tn*PI/5): PLOT 155,50+16: DRAW OVER 1;15*SIN (un*PI/500),15*COS (un*PI/500): END IF
+2210 LET tn=pz/1000: LET un=pz-1000*INT tn: PLOT 155,50+16: DRAW OVER 1;10*SIN (tn*PI/5),10*COS (tn*PI/5): PLOT 155,50+16: DRAW OVER 1;15*SIN (un*PI/500),15*COS (un*PI/500)
+2220 IF gc<>0 THEN GO SUB 2230: END IF
+2225 IF pow=-1 AND tc>.2 THEN LET tc=tc-.2: END IF
+2226 IF pow=1 AND tc<8.8 THEN LET tc=tc+.2: END IF
+2228 GO SUB 2230: GO TO 2240
+2230 PLOT 215,50+16: DRAW OVER 1;15*SIN (tc*PI/5),15*COS (tc*PI/5): RETURN
+2240 PRINT AT 21,2;ABS INT ad;" "
+2250 IF py=0 THEN LET rb=0: GO TO 2260: END IF
+2255 LET rb=ATN (px/py)/c: IF py>0 THEN LET rb=rb+180: END IF
+2260 IF rb<0 THEN LET rb=rb+360: END IF
+2270 PRINT AT 21,10;INT rb;" ";AT 21,18;ABS INT px;" "
+2280 PRINT AT 21,25;INT (SQR (py*py+px*px));" "
+2290 IF (y1<=110 AND y2<=110) OR (y1>=130 AND y2>=130) THEN GO TO 2300: END IF
+2295 IF gc<>0 THEN PLOT OVER 1;x1,168+16-y1: DRAW OVER 1;x2-PEEK $8004,168+16-y2-PEEK $8005: END IF
+2300 LET yc=120+(pt/3): LET x1=80: LET x2=110: LET y1=yc+17*TAN (rl*2*c): LET y2=yc-17*TAN (rl*2*c)
+2310 IF (yc<110 OR yc>130) AND rl=0 THEN GO TO 2376: END IF
+2320 IF y1<110 THEN LET x1=95-(95-x1)*(110-yc)/(y1-yc): LET y1=110: GO TO 2340: END IF
+2330 IF y1>130 THEN LET x1=95-(95-x1)*(130-yc)/(y1-yc): LET y1=130: END IF
+2340 IF y2<110 THEN LET x2=95-(95-x2)*(110-yc)/(y2-yc): LET y2=110: GO TO 2360: END IF
+2350 IF y2>130 THEN LET x2=95-(95-x2)*(130-yc)/(y2-yc): LET y2=130: END IF
+2360 IF x1<80 OR x2>110 THEN GO TO 2376: END IF
+2370 PLOT OVER 1;x1,168+16-y1: DRAW OVER 1;x2-PEEK $8004,168+16-y2-PEEK $8005
+2376 IF (rl=rr AND pp=pt) THEN GO TO 2500: END IF
+2377 IF (y3<=2 AND y4<=2) OR (y3>=90 AND y4>=90) THEN GO TO 2380: END IF
+2378 IF gc<>0 THEN PLOT OVER 1;x3,176+16-y3: DRAW OVER 1;x4-PEEK $8004,(176+16-y4)-PEEK $8005: END IF
+2380 LET yc=33+pt*4: LET x3=11: LET x4=244: LET y3=yc+118*TAN (rl*2*c): LET y4=yc-118*TAN (rl*2*c)
+2390 IF (yc<2 OR yc>90) AND rl=0 THEN GO TO 2450: END IF
+2400 IF y3<2 THEN LET x3=128-(128-x3)*(2-yc)/(y3-yc): LET y3=2: GO TO 2420: END IF
+2410 IF y3>90 THEN LET x3=128-(128-x3)*(90-yc)/(y3-yc): LET y3=90: END IF
+2420 IF y4<2 THEN LET x4=128-(128-x4)*(2-yc)/(y4-yc): LET y4=2: GO TO 2440: END IF
+2430 IF y4>90 THEN LET x4=128-(128-x4)*(90-yc)/(y4-yc): LET y4=90: END IF
+2440 IF x3<11 OR x4>244 THEN GO TO 2500: END IF
+2445 OVER 1: PLOT x3,176+16-y3: DRAW x4-PEEK $8004,(176+16-y4)-PEEK $8005: OVER 0
+2450 REM
+2500 GO SUB 8000
+2505 IF gc=0 THEN LET gc=1: END IF
+2510 LET rr=rl: LET pp=pt: RETURN
+3000 LET pow=0: LET k$=INKEY$: IF k$="" THEN RETURN: END IF
+3010 IF k$="s" THEN LET pow=pow-1: END IF
+3020 IF k$="f" THEN LET pow=pow+1: END IF
+3030 IF k$="q" THEN LET pt=pt+1: END IF
+3040 IF k$="a" THEN LET pt=pt-1: END IF
+3050 IF k$="o" AND rl>-30 THEN LET rl=rl-1: END IF
+3060 IF k$="p" AND rl<30 THEN LET rl=rl+1: END IF
+3070 RETURN
+5000 LET pp=-1: LET rr=-1
+5010 LET c=PI/180: LET py=-20000: LET pz=2000: LET as0=150
+5020 PRINT AT 22,0; "INPUT WIND SPEED (1-50) MPH": LET xx0$=INPUT(10): LET x0=VAL(xx0$): PRINT AT 22,0; TAB 30;
+5025 IF x0>50 OR x0<1 THEN GO TO 5020: END IF
+5030 PRINT AT 22,0; "WIND DIRECTION (0-359) DEGREES": LET xx1$=INPUT(10): LET x1=VAL(xx1$): PRINT AT 22,0; TAB 30;
+5035 IF x1>359 OR x1<0 THEN GO TO 5030: END IF
+5040 LET x0=x0/3
+5050 PRINT "WIND SPEED= ";3*x0;" M/S": PRINT "DIRECTION = ";x1;" DEGREES"
+5055 PAUSE 100: CLS
+5060 LET wy=-x0*COS (x1*c)
+5070 LET wx=-x0*SIN (x1*c)
+5080 LET gz=vz: LET gy=vy+wy: LET gx=vx+wx
+5090 LET tc=5
+5100 LET rt=3: LET tp=5: LET wr=50
+5110 PLOT 10,175+16: DRAW 235,0: DRAW 0,-90: DRAW -235,0: DRAW 0,90
+5120 FOR kk=0 TO 3: CIRCLE 35+kk*60,50+16,20: NEXT kk
+5130 PRINT AT 12,2;"SPEED HORZN ALT RPM"
+5150 PRINT AT 20,0;"BEARING RUNWAY DRIFT DISTANCE"
+5170 PLOT 87,50+16: DRAW 5,0: DRAW 3,-3: DRAW 3,3: DRAW 5,0
+5180 LET x=35: LET y=50: GO SUB 7000: LET x=155: GO SUB 7000: LET x=215: GO SUB 7000
+5500 GO SUB 3000: GO SUB 1000
+5510 IF pz<=0 THEN GO TO 6000: END IF
+5520 GO SUB 2000
+5530 GO TO 5500
+6000 IF ABS rl>rt OR pt>tp OR pt<0 OR as0>80 THEN GO TO 6030: END IF
+6010 IF ABS px>wr OR ABS py>1000 THEN GO TO 6060: END IF
+6020 CLS : PRINT "CONGRATULATIONS ON A SUCCESSFUL LANDING!": GO TO 6100
+6030 FOR n=0 TO 20 STEP .5: PLOT 127,130+16: DRAW 120-INT (RND*240),45-INT (RND*90): BEEP .005,20-n: NEXT n
+6040 PAUSE 50
+6050 CLS : PRINT "A CRASH LIKE THAT HAS WRECKED THE AIRCRAFT!": GO TO 6100
+6060 CLS : PRINT "YOU LANDED OFF THE RUNWAY"
+6070 IF as0<40 THEN PRINT "FORTUNATELY YOU WEREN'T GOING FAST ENOUGH TO DO MUCH DAMAGE": GO TO 6100: END IF
+6080 IF as0<80 THEN PRINT "AT THAT SPEED YOU GOT AWAY WITH LIGHT DAMAGE AND A FEW BRUISES": GO TO 6100: END IF
+6090 PRINT "MISSING THE RUNWAY AT THAT SPEED HAS LEFT NO SURVIVORS!"
+6100 PRINT : PRINT : PRINT "FINAL FLIGHT DETAILS:": PRINT
+6110 PRINT "AIRSPEED= ";INT as0;" M/S": PRINT "DISTANCE= ";INT (SQR (py*py+px*px)): PRINT "PITCH = ";pt
+6120 PRINT "ROLL = ";rl: PRINT "RPM = ";INT (10*tc)/10;" X 1000"
+6130 PRINT "DRIFT = ";INT ABS px;" MTRS": PRINT "BEARING = ";ad;" DEGREES"
+6140 PRINT : PRINT "DO YOU WANT ANOTHER GO (Y/N)?"
+6150 LET a$=INKEY$: IF a$<>"y" AND a$<>"n" THEN GO TO 6150: END IF
+6160 IF a$="n" THEN GO TO 6900: END IF
+6170 CLS : GO TO 1
+6900 STOP
+7000 FOR kk=0 TO 2*PI STEP PI/5: PLOT x+17*SIN kk,16+y+17*COS kk: DRAW 2*SIN kk,2*COS kk: NEXT kk: RETURN
+8000 IF gc<>0 THEN PLOT 127,174+16: DRAW OVER 1;ox,oy: END IF
+8010 LET ox=16*SIN (rb*(PI/180)): LET oy=-(16*ABS COS (rb*(PI/180)))
+8020 PLOT 127,174+16: DRAW OVER 1;ox,oy
+8025 LET wb=ad: IF ad>180 THEN LET wb=wb-360: END IF
+8026 IF rb>180 THEN LET wb=wb+360-rb: GO TO 8040: END IF
+8030 LET wb=wb-rb
+8040 IF rw=1 THEN PLOT OVER 1;rdx,175+16-rdy: END IF
+8050 LET rw=0: IF ABS wb>57 THEN RETURN: END IF
+8060 LET rdx=x3+INT (((x4-x3)/2)-SIN (wb*(PI/180))*(x4-x3)*.6)
+8070 LET rdy=y3+((y4-y3)*((rdx-x3)/(x4-x3))+2)
+8080 IF rdy<2 OR rdy>90 OR rdx<11 OR rdx>244 THEN RETURN: END IF
+8090 LET rw=1: PLOT OVER 1;rdx,175+16-rdy
+8100 RETURN
diff --git a/examples/sd81/joystick.bas b/examples/sd81/joystick.bas
new file mode 100644
index 000000000..7f521b32a
--- /dev/null
+++ b/examples/sd81/joystick.bas
@@ -0,0 +1,29 @@
+' ----------------------------------------------------------------------
+' joystick.bas — Programmable joystick via the SD81 Booster MCU
+'
+' The SD81 Booster maps a physical joystick to keypresses. Joy(keys)
+' (joy.bas, cmd 21) configures that mapping: keys must be exactly 5
+' characters, in order UP, DOWN, LEFT, RIGHT, FIRE. Letters (any case),
+' digits and space are valid; space means "no key" for that direction.
+' Returns the MCU's status byte (0 = OK, 14 = invalid parameter).
+'
+' Once configured, the joystick just behaves like a keyboard: read it
+' with INKEY$/INPUT as usual.
+' ----------------------------------------------------------------------
+
+#include
+
+dim st as ubyte
+dim k$ as string
+
+' classic "QAOP + space" layout
+st = Joy("QAOPM")
+print "Joy(QAOPM) -> status "; st
+
+print "Move the joystick (BREAK to exit)"
+do
+ k$ = inkey$
+ if k$ <> "" then
+ print k$;
+ end if
+loop
diff --git a/examples/sd81/maskedsprites_sd81.bas b/examples/sd81/maskedsprites_sd81.bas
new file mode 100644
index 000000000..8c1e3a910
--- /dev/null
+++ b/examples/sd81/maskedsprites_sd81.bas
@@ -0,0 +1,810 @@
+' ----------------------------------------------------------------
+' This file is released under the MIT License
+'
+' Copyright (C) 2026 Conrado Badenas
+'
+' Example for
+' Print Masked (AND+OR) Sprites, version 2026.04.05
+'
+' Version zx81sd: WaitForNewFrame usaba EI+HALT esperando la interrupcion
+' de 50Hz del Spectrum (via ROM, contador FRAMES en 23672). En zx81sd las
+' interrupciones estan permanentemente deshabilitadas (el vector $0038 es
+' solo una trampa DI;HALT, no un manejador real) — ese HALT no despertaria
+' nunca. Sustituido por VSYNC_TICK (runtime/vsync.asm), que sondea el
+' contador de pulsos VSYNC del SD81 Booster por puerto ($AFh) sin usar
+' interrupciones, igual que hace ya PAUSE. Resto del fichero sin cambios.
+' ----------------------------------------------------------------
+
+' DEFINEs to control the library cb/maskedsprites.bas
+ #define NumberofMaskedSprites 10 ' Try 50 here, with #define USE_MSFS
+ #define MaskedSprites_USE_STACK_TRANSFER
+'#include "maskedsprites.bas"
+ #include
+
+' DEFINEs to control this example program
+ #define NUMWAITS -1
+ #define BORDER_WAIT 0
+ #define USE_MSFS
+ #define INTS di
+
+#include
+
+CONST NUMSPRITES as UByte = NumberofMaskedSprites
+
+dim i as Byte
+dim memoryPaging,numberofscrolls,loops,warnings,modulus as UByte
+dim x0(0 to NUMSPRITES-1) as UByte
+dim y0(0 to NUMSPRITES-1) as UByte
+dim x1(0 to NUMSPRITES-1) as UByte
+dim y1(0 to NUMSPRITES-1) as UByte
+dim back(0 to NUMSPRITES-1) as UInteger
+#ifdef USE_MSFS
+ dim regs(0 to NUMSPRITES-1) as UInteger
+ dim regHero0,regFoe00,regFoe20,regFoe21,regFoe22,regFoe23 as UInteger
+#endif
+dim temp as Integer
+dim pointx1,pointx2,pointy1,pointy2 as Integer 'for subtractions
+
+ASM
+ INTS
+END ASM
+
+for i=0 to NUMSPRITES-1
+ y0(i)=255:y1(i)=255:x1(i)=randomLimit(240)
+next i:x1(0)=120
+
+'DIM Rosquilla_Sprite(31) AS UByte => { _
+' $00,$07,$18,$20,$23,$44,$48,$48, _
+' $48,$48,$44,$23,$20,$18,$07,$00, _
+' $00,$E0,$18,$04,$C4,$22,$12,$12, _
+' $12,$12,$22,$C4,$04,$18,$E0,$00 _
+' }
+DIM Rosquilla0(31) AS UByte => { _
+ $00,$07,$19,$21,$23,$44,$48,$48, _
+ $48,$58,$7C,$3B,$20,$18,$07,$00, _
+ $00,$E0,$98,$84,$C4,$22,$12,$12, _
+ $12,$1A,$3E,$DC,$04,$18,$E0,$00 _
+ }
+DIM Rosquilla1(31) AS UByte => { _
+ $00,$07,$1C,$2E,$2F,$44,$48,$48, _
+ $48,$48,$44,$2F,$2E,$1C,$07,$00, _
+ $00,$E0,$18,$04,$C4,$22,$12,$1E, _
+ $1E,$12,$22,$C4,$04,$18,$E0,$00 _
+ }
+DIM Rosquilla2(31) AS UByte => { _
+ $00,$07,$18,$20,$3B,$7C,$58,$48, _
+ $48,$48,$44,$23,$21,$19,$07,$00, _
+ $00,$E0,$18,$04,$DC,$3E,$1A,$12, _
+ $12,$12,$22,$C4,$84,$98,$E0,$00 _
+ }
+DIM Rosquilla3(31) AS UByte => { _
+ $00,$07,$18,$20,$23,$44,$48,$78, _
+ $78,$48,$44,$23,$20,$18,$07,$00, _
+ $00,$E0,$38,$74,$F4,$22,$12,$12, _
+ $12,$12,$22,$F4,$74,$38,$E0,$00 _
+ }
+memoryPaging = CheckMemoryPaging()
+if memoryPaging then SetVisibleScreen(5) '128K
+paper 1:ink 7:border 0:cls
+
+for i=0 to 126
+ pointx1=randomLimit(255):pointy1=randomLimit(191)
+ pointx2=randomLimit(255):pointy2=randomLimit(191)
+ plot pointx1,pointy1:draw pointx2-pointx1,pointy2-pointy1
+next i
+
+#ifdef USE_MSFS
+ print "Init MSFS at ";InitMaskedSpritesFileSystem()
+ print "Free Blocks in MSFS = ";GetNumberofFreeBlocksInMSFS()
+ regHero0 = RegisterSpriteImageInMSFS(@hero0):if regHero0=0 then STOP
+ print "regHero0 = ";regHero0
+ regFoe00 = RegisterSpriteImageInMSFS(@foe00):if regFoe00=0 then STOP
+ print "regFoe00 = ";regFoe00
+ regFoe20 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla0(0),@Rosquilla_Sprite_Mask):if regFoe20=0 then STOP
+ print "regFoe20 = ";regFoe20
+ regFoe21 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla1(0),@Rosquilla_Sprite_Mask):if regFoe21=0 then STOP
+ print "regFoe21 = ";regFoe21
+ regFoe22 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla2(0),@Rosquilla_Sprite_Mask):if regFoe22=0 then STOP
+ print "regFoe22 = ";regFoe22
+ regFoe23 = RegisterSpriteGraphAndMaskInMSFS(@Rosquilla3(0),@Rosquilla_Sprite_Mask):if regFoe23=0 then STOP
+ print "regFoe23 = ";regFoe23
+
+ for i=0 to NUMSPRITES-1: modulus = i mod 2
+ if i=0 then
+ regs(0)=regHero0
+ elseif modulus=1 then
+ regs(i)=regFoe00
+ else ' modulus=0 with i>0
+ regs(i)=regFoe20
+ end if
+ next i
+' pause 0
+#endif
+
+for i=0 to NUMSPRITES-1
+ back(i)=MaskedSpritesBackground(i)
+' print back(i)
+next i
+' print:pause 0
+
+if memoryPaging then '128K
+ dim back0(0 to NUMSPRITES-1) as UInteger
+ ChangeMaskedSpritesBackgroundSet
+ for i=0 to NUMSPRITES-1
+ back0(i)=MaskedSpritesBackground(i)
+' print back0(i)
+ next i
+ ChangeMaskedSpritesBackgroundSet
+' print:print MaskedSpritesFileSystemStart:pause 0
+
+ CopyScreen5ToScreen7()
+' print at 11,8;"This is Screen5"
+ SetDrawingScreen7() 'Bank7 is set at $c000
+' print at 13,8;"This is Screen7"
+ SetDrawingScreen5() 'Bank7 is still set at $c000
+' print at 15,8;"This is Screen5"
+end if
+
+' We start with VisibleScreen = DrawingScreen = 5
+numberofscrolls=0
+warnings=0
+loops=0
+WaitForNewFrame(0)
+do
+
+ ' 128K
+ if memoryPaging then
+ ' The brand-new VisibleScreen is ready for the ULA to show it on TV
+ ' But we wait to the beginning of a frame to avoid tearing
+ ' (unless wanted time-to-wait has already been spent)
+ out 254,BORDER_WAIT:WaitForNewFrame(NUMWAITS):out 254,0
+'GetInterruptStatusInBorder()
+ ToggleVisibleScreen()
+ ' Now we can modify DrawingScreen because it is not Visible
+
+ ' We restore background in x0,y0 if it is saved in MaskedSpritesBackground
+ if MaskedSpritesBackgroundSet then
+ for i=NUMSPRITES-1 to 0 step -1
+ if y0(i)<>255 then RestoreBackground(x0(i),y0(i),back0(i))
+ next i
+ else
+ for i=NUMSPRITES-1 to 0 step -1
+ if y0(i)<>255 then RestoreBackground(x0(i),y0(i),back(i))
+ next i
+ end if
+ end if
+
+ ' After restoring background in a Spectrum 128K, you can use CPU to do something else:
+ '
+ ' 0. Change/scroll background (ONLY NOW!!!)
+ ' 1. Check collisions
+ ' 2. Resolve collisions
+ ' 3. Change images of sprites for:
+ ' a. Animation of regular motion
+ ' b. Explosions and other extraordinary events
+ ' 4. Upgrade the score
+ ' 5. Play sound/music
+ ' ...
+ '
+ ' You have plenty of time to do whatever you want in a Spectrum 128K
+
+ ' 0. Change/scroll background (ONLY NOW!!!)
+ if memoryPaging=1 then '128K
+ if (in($7ffe) bAND 1)=0 then
+ if numberofscrolls=1 then
+ Scroll2()
+ else
+ Scroll1()
+ numberofscrolls=1
+ end if
+ else
+ if numberofscrolls=1 then
+ Scroll1()
+ end if
+ numberofscrolls=0
+ end if
+ end if
+
+#ifdef USE_MSFS
+ ' 3. Change images of sprites for: a. Animation of regular motion
+ for i=2 to NUMSPRITES-1 step 2: modulus = (i+loops) mod 4
+ if modulus=0 then
+ regs(i)=regFoe20
+ elseif modulus=1 then
+ regs(i)=regFoe21
+ elseif modulus=2 then
+ regs(i)=regFoe22
+ else
+ regs(i)=regFoe23
+ end if
+ next i
+#endif
+
+ ' We update x0,y0 and compute new x1,y1
+ for i=1 to NUMSPRITES-1
+ temp=x1(i): x0(i)=temp
+ temp=temp-1
+ if temp<0 then temp=240
+ x1(i)=temp
+ temp=y1(i): y0(i)=temp
+ if temp=255 then temp=randomLimit(176)
+ if randomLimit(3)=0 then temp=temp+randomLimit(2)-1
+ if temp<0 then temp=0
+ if temp>176 then temp=176
+ y1(i)=temp
+ next i
+
+ 'Special code for the hero
+ temp=x1(0): x0(0)=temp
+ temp=temp+1 -2*(in($dffe) bAND 1) +2*((in($dffe)>>1) bAND 1) '-noP+noO
+ if temp<0 then temp=240
+ if temp>240 then temp=0
+ x1(0)=temp
+ temp=y1(0): y0(0)=temp
+ if temp=255 then temp=randomLimit(176)
+ temp=temp -(in($fdfe) bAND 1) +(in($fbfe) bAND 1) '-noA+noQ
+ if temp<0 then temp=0
+ if temp>176 then temp=176
+ y1(0)=temp
+
+ ' 48K We restore background in x0,y0 if it is saved in MaskedSpritesBackground
+ if not memoryPaging then
+ for i=NUMSPRITES-1 to 0 step -1
+ if y0(i)<>255 then RestoreBackground(x0(i),y0(i),back(i))
+ next i
+ end if
+
+#ifdef USE_MSFS
+ ' Print WARNING (if not scrolling) twice if MSFS has run out of free blocks
+ if not numberofscrolls and warnings<2 and not GetNumberofFreeBlocksInMSFS() then
+ print at 23,0;"WARNING! MSFS has NO free blocks";
+ warnings=warnings+1
+ end if
+#endif
+
+ ' After restoring background in a Spectrum 48K, you should do NOTHING
+ ' ...
+ ' You have ABSOLUTELY NO TIME to do anything in a Spectrum 48K
+
+ ' We save background in MaskedSpritesBackground and draw sprites in x,y...
+#ifdef USE_MSFS
+ ' ...using SaveBackgroundAndDrawSpriteRegisteredInMSFS(,,,)
+ if MaskedSpritesBackgroundSet then
+ for i=0 to NUMSPRITES-1
+ SaveBackgroundAndDrawSpriteRegisteredInMSFS(x1(i),y1(i),back0(i),regs(i))
+ next i
+ else
+ for i=0 to NUMSPRITES-1
+ SaveBackgroundAndDrawSpriteRegisteredInMSFS(x1(i),y1(i),back(i),regs(i))
+ next i
+ end if
+#else
+ ' ...using SaveBackgroundAndDrawSprite(,,,)
+ if MaskedSpritesBackgroundSet then
+ SaveBackgroundAndDrawSprite(x1(0),y1(0),back0(0),@hero0)
+ for i=1 to NUMSPRITES-1
+ SaveBackgroundAndDrawSprite(x1(i),y1(i),back0(i),@foe00)
+ next i
+ else
+ SaveBackgroundAndDrawSprite(x1(0),y1(0),back(0),@hero0)
+ for i=1 to NUMSPRITES-1
+ SaveBackgroundAndDrawSprite(x1(i),y1(i),back(i),@foe00)
+ next i
+ end if
+#endif
+
+ ' After drawing sprites, you can use CPU to do something else:
+ '
+ ' 1. Check collisions
+ ' 2. Resolve collisions
+ ' 3. Change images of sprites for:
+ ' a. Animation of regular motion
+ ' b. Explosions and other extraordinary events
+ ' 4. Upgrade the score
+ ' 5. Play sound/music
+ ' ...
+ '
+ ' You have plenty of time to do whatever you want in ANY Spectrum
+
+ ' We change the Set of Backgrounds (128K) for next iteration
+ if memoryPaging then ChangeMaskedSpritesBackgroundSet()
+
+ ' We have finished drawing
+ if memoryPaging then
+ ' We change DrawingScreen (128K) for next iteration
+ ToggleDrawingScreen()
+ else
+ ' ULA shows DrawingScreen (48K) on TV for some frames before going on
+ out 254,BORDER_WAIT:WaitForNewFrame(NUMWAITS):out 254,0
+ end if
+'GetInterruptStatusInBorder()
+ ' Now, VisibleScreen = DrawingScreen, and
+ ' we could see anything we draw whilst it is drawn
+ loops=loops+1
+loop
+
+' SUB/FUNCTIONs that are not used, are "used" here to check compilation is OK
+ SetBankPreservingINTs(0)
+print GetBankPreservingRegs()
+print CheckMemoryPaging()
+ SetVisibleScreen(0)
+print GetVisibleScreen()
+ ToggleVisibleScreen()
+ CopyScreen5ToScreen7()
+ CopyScreen7ToScreen5()
+ SetDrawingScreen5()
+print SetDrawingScreen7()
+ ToggleDrawingScreen()
+print ChangeMaskedSpritesBackgroundSet()
+ SaveBackgroundAndDrawSprite(0,0,0,0)
+ RestoreBackground(0,0,0)
+print InitMaskedSpritesFileSystem()
+print GetNumberofFreeBlocksInMSFS()
+print RegisterSpriteImageInMSFS(0)
+print RegisterSpriteGraphAndMaskInMSFS(0,0)
+ SaveBackgroundAndDrawSpriteRegisteredInMSFS(0,0,0,0)
+GetInterruptStatusInBorder()
+Scroll()
+Scroll1()
+Scroll2()
+' Delete/Comment all these "uses" when used, or compilation checking is not needed
+
+hero0:
+ASM
+ defb %11000000,%00000000,%00001111,%00000000;1
+ defb %10000000,%00011111,%00000000,%11100000;2
+ defb %00000000,%00100000,%00000000,%00111110;3
+ defb %00000000,%01001110,%00000000,%11111000;4
+ defb %00000000,%01011001,%00000001,%00001100;5
+ defb %00000000,%01011010,%00000000,%00010100;6
+ defb %00000000,%00000011,%00000000,%11100010;7
+ defb %00000000,%01111100,%00000000,%00100010;8
+ defb %00000000,%01111100,%00000000,%00100010;8
+ defb %00000000,%00000011,%00000000,%11100010;7
+ defb %00000000,%01011010,%00000000,%00010100;6
+ defb %00000000,%01011001,%00000001,%00001100;5
+ defb %00000000,%01001110,%00000000,%11111000;4
+ defb %00000000,%00100000,%00000000,%00111110;3
+ defb %10000000,%00011111,%00000000,%11100000;2
+ defb %11000000,%00000000,%00001111,%00000000;1
+END ASM
+
+foe00:
+ASM
+ defb %00000011,%00000000,%11000000,%00000000;1
+ defb %00000011,%01111000,%11000000,%00011110;2
+ defb %00000011,%01000000,%11000000,%00010000;3
+ defb %00000001,%01010000,%10000000,%00010100;4
+ defb %00000000,%01001000,%00000000,%00100010;5
+ defb %00000000,%00000010,%00000000,%01110000;6
+ defb %11100000,%00000111,%00000111,%11100000;7
+ defb %11110000,%00000010,%00001111,%01000000;8
+ defb %11110000,%00000010,%00001111,%01000000;8
+ defb %11100000,%00000111,%00000111,%11100000;7
+ defb %00000000,%00001110,%00000000,%01000000;6'
+ defb %00000000,%01110100,%00000000,%00011110;5'
+ defb %00000001,%01000000,%10000000,%00010000;4'
+ defb %00000011,%01010000,%11000000,%00010100;3'
+ defb %00000011,%01001000,%11000000,%00010010;2'
+ defb %00000011,%00000000,%11000000,%00000000;1
+END ASM
+
+Rosquilla_Sprite_Mask:
+ASM
+ db $FF,$F8,$E0,$C0,$C0,$83,$87,$87
+ db $87,$87,$83,$C0,$C0,$E0,$F8,$FF
+ db $FF,$1F,$07,$03,$03,$C1,$E1,$E1
+ db $E1,$E1,$C1,$03,$03,$07,$1F,$FF
+END ASM
+
+
+' ----------------------------------------------------------------
+' READ_IFF2 is a MACRO that reads correctly the IFF2 flip-flop,
+' avoiding a "bug" reported in the Z80 User Manual,
+' which Pedro Picapiedra aka ProgramadorHedonista kindly pointed
+' me to: "If an interrupt occurs during execution of this
+' instruction [LD A,I or LD A,R], the Parity flag contains a 0."
+' ----------------------------------------------------------------
+#define READ_IFF2 \
+ ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even \
+ jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI \
+ ld a,i ; read IFF2 again to ensure that IFF2=0=DI \
+1:
+
+
+' ----------------------------------------------------------------
+' Wait for New Frame — version zx81sd
+'
+' El original usaba EI+HALT esperando la interrupcion IM1 de 50Hz de la
+' ROM del Spectrum, y comparaba contra el contador FRAMES de la ROM en
+' la direccion absoluta 23672. En zx81sd no hay interrupciones (todo el
+' runtime corre con DI permanente; el vector $0038 es una trampa
+' DI;HALT, no un manejador real) ni existe esa direccion de sysvar.
+' Sustituido por VSYNC_TICK (runtime/vsync.asm): sondea por puerto el
+' contador de pulsos VSYNC del SD81 Booster (hardware, sin interrupciones)
+' e incrementa nuestro propio sysvar FRAMES. Se llama una vez por cada
+' frame que haya que esperar, en vez de un unico HALT seguido de un
+' bucle que confiaba en que la interrupcion siguiera incrementando FRAMES
+' en segundo plano mientras el bucle solo comprobaba sin esperar.
+' Parameters:
+' Byte: if =0, you want just one frame wait
+' if >0, minimum number of frames spent since last wait
+' if <0, return without waiting
+' ----------------------------------------------------------------
+SUB FASTCALL WaitForNewFrame(minimumNumberofFramesToWaitSinceLastWait AS Byte)
+ASM
+ push namespace core
+ PROC
+ LOCAL wait,enough,temp
+
+ and a
+ ret m ; return if minimumNumberofFramesToWaitSinceLastWait < 0
+ ld hl,temp
+ ld de, FRAMES
+ ld c,a ; C = minimumNumberofFramesToWaitSinceLastWait
+
+ call VSYNC_TICK ; garantiza al menos un frame esperado (sustituye EI+HALT)
+
+wait:
+ ld a,(de)
+ sub (hl) ; A = number of frames since last RET
+ cp c ; is A >= C? Yes: NoCarry. No: Carry
+ jr nc,enough
+ call VSYNC_TICK ; espera otro frame (FRAMES lo incrementa VSYNC_TICK)
+ jr wait
+enough:
+ ld a,(de)
+ ld (hl),a
+ ret
+temp:
+ defb 0
+ ENDP
+ pop namespace
+END ASM
+END SUB
+#require "vsync.asm"
+
+
+' ----------------------------------------------------------------
+' Get Interrupt Status in Border
+' This SUB uses a hack to ensure a good reading of the IFF2 flip-flop.
+' Hack was found thanks to Pedro Picapiedra aka ProgramadorHedonista.
+' Border results: 2 Red if Interrupts are Disabled IFF2=0
+' 4 Green if Interrupts are Enabled IFF2=1
+'
+' Version zx81sd: se deja sin cambios (no se llama nunca en el bucle
+' principal, solo aparece comentada mas abajo; se mantiene unicamente
+' para que la comprobacion de compilacion del final del fichero no falle
+' por "funcion no usada"). En zx81sd siempre mostraria rojo, ya que las
+' interrupciones estan permanentemente deshabilitadas.
+' ----------------------------------------------------------------
+SUB FASTCALL GetInterruptStatusInBorder()
+ASM
+ READ_IFF2
+ push af
+ pop bc
+ ld a,c
+ and 4 ; A=0/4 iff IFF2=0/1
+ rra ; A=0/2 iff IFF2=0/1
+ add a,2 ; A=2/4 iff IFF2=0/1
+ out (254),a
+END ASM
+END SUB
+
+
+SUB FASTCALL Scroll()
+ASM
+ PROC
+ LOCAL loop
+ ld hl,(.core.SCREEN_ADDR)
+ ld de,31
+ ld b,192
+loop:
+; Here, HL = screen address for first column
+ add hl,de; column 32
+ xor a
+ rl (hl) ; 32
+ dec l
+ rl (hl) ; 31
+ dec l
+ rl (hl) ; 30
+ dec l
+ rl (hl) ; 29
+ dec l
+ rl (hl) ; 28
+ dec l
+ rl (hl) ; 27
+ dec l
+ rl (hl) ; 26
+ dec l
+ rl (hl) ; 25
+ dec l
+ rl (hl) ; 24
+ dec l
+ rl (hl) ; 23
+ dec l
+ rl (hl) ; 22
+ dec l
+ rl (hl) ; 21
+ dec l
+ rl (hl) ; 20
+ dec l
+ rl (hl) ; 19
+ dec l
+ rl (hl) ; 18
+ dec l
+ rl (hl) ; 17
+ dec l
+ rl (hl) ; 16
+ dec l
+ rl (hl) ; 15
+ dec l
+ rl (hl) ; 14
+ dec l
+ rl (hl) ; 13
+ dec l
+ rl (hl) ; 12
+ dec l
+ rl (hl) ; 11
+ dec l
+ rl (hl) ; 10
+ dec l
+ rl (hl) ; 09
+ dec l
+ rl (hl) ; 08
+ dec l
+ rl (hl) ; 07
+ dec l
+ rl (hl) ; 06
+ dec l
+ rl (hl) ; 05
+ dec l
+ rl (hl) ; 04
+ dec l
+ rl (hl) ; 03
+ dec l
+ rl (hl) ; 02
+ dec l
+ rl (hl) ; 01
+ rla ; save CF in A
+ add hl,de; column 32
+ or (hl)
+ ld (hl),a
+ inc hl ; first column
+ djnz loop
+ ENDP
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' NEXT_ROW is a MACRO of ASM code, based on code from
+' https://zonadepruebas.com/viewtopic.php?f=15&t=8372&start=40#p81507
+' and found by Joaquin Ferrero
+' ----------------------------------------------------------------
+#define NEXT_ROW \
+ sub 224 ; 7 A was L, now A = L + 32 (SUB 224 is similar to +32) \
+ ; CF = 0/1 iff E >=/< 224 iff a third is/isn't crossed \
+ ld l,a ; 4 \
+ sbc a,a ; 4 A = 0/255 \
+ and 248 ; 7 A = 0/248 (248 = -8) \
+ add a,h ; 4 A = H/H-8 iff a third is/isn't crossed \
+ ld h,a ; 4 += 30 Ts
+
+
+' ----------------------------------------------------------------
+' Scroll 1 pixel up
+' ----------------------------------------------------------------
+SUB FASTCALL Scroll1()
+ASM
+ PROC
+ LOCAL loop1,loop2,exit
+ push ix
+ xor a
+ ld hl,(.core.SCREEN_ADDR); HL = $4000 or $c000
+ ld de,buffer0
+ call ldi32 ; scanline0
+ ; DEC HL is not needed because row = 0 <> 7, 15, 23
+ ld ixh,24 ; 24 rows (rows 0-23)
+loop1:
+ ld l,a ; A = low byte of screen addresses for this row
+; HL = scanline0 for this row
+ ld d,h
+ ld e,l ; DE = scanline0 of this row
+ inc h ; HL = scanline1 of this row
+ ld ixl,7 ; first 7 scanlines of one row
+loop2:
+; HL,DE = origin,destination screen address
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi ; BC-=32 DE+=32 HL+=32 (if E,L was 224, then D,H changes)
+ dec hl ; 6 in case H changes (because L was 224: rows 7,15,23)
+ ld l,a ; +4 = 10 Ts < 21 Ts (PUSH+POP)
+ ld d,h
+ ld e,l ; scanline_N+0 of this row
+ inc h ; scanline_N+1 of this row
+ dec ixl
+ jp nz,loop2
+ dec ixh
+ jr z,exit ; this JR Z here (instead of an JP NZ later) adds +7 Ts/row = 161 Ts for 23 rows
+; last scanline of first 23 rows: DE is OK, HL must change
+ NEXT_ROW
+ ld a,l
+ call ldi32 ; 539 Ts needed for each row except last row
+ dec hl ; with JR Z,EXIT before, we save 539-161 = 378 Ts for last row
+ jp loop1
+exit:
+ ld hl,buffer0
+ call ldi32 ; DE = scanline7 of last row
+ pop ix
+ ENDP
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' Scroll 2 pixels up
+' ----------------------------------------------------------------
+SUB FASTCALL Scroll2()
+ASM
+ PROC
+ LOCAL loop1,loop2,exit
+ push ix
+ xor a
+ ld hl,(.core.SCREEN_ADDR); HL = $4000 or $c000
+ ld de,buffer0
+ call ldi32 ; scanline0
+ ; DEC HL is not needed because row = 0 <> 7, 15, 23
+ ld l,a
+ inc h
+ call ldi32 ; scanline1
+ ; DEC HL is not needed because row = 0 <> 7, 15, 23
+ ld ixh,24 ; 24 rows (rows 0-23)
+loop1:
+ ld l,a ; A = low byte of screen addresses for this row
+; HL = scanline1 for this row
+ ld d,h
+ ld e,l
+ dec d ; DE = scanline0 of this row
+ inc h ; HL = scanline2 of this row
+ ld ixl,6 ; first 6 scanlines of one row
+loop2:
+; HL,DE = origin,destination screen address
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi ; BC-=32 DE+=32 HL+=32 (if E,L was 224, then D,H changes)
+ dec hl ; 6 in case H changes (because L was 224: rows 7,15,23)
+ ld l,a ; +4 = 10 Ts < 21 Ts (PUSH+POP)
+ ld d,h
+ ld e,l
+ dec d ; DE = scanline_N+0 of this row
+ inc h ; HL = scanline_N+2 of this row
+ dec ixl
+ jp nz,loop2
+ dec ixh
+ jr z,exit ; this JR Z here (instead of an JP NZ later) adds +7 Ts/row = 161 Ts for 23 rows
+; last scanline of first 23 rows: DE is OK, HL must change
+ NEXT_ROW
+ ld c,h ; C=H is larger than 32, so B does not change when BC-=32
+ ld b,e ; DE = scanline6 of this row SAVED in B
+ ld a,l ; HL = scanline0' of next row SAVED in A
+ call ldi32 ; 539 Ts needed for each row except last row
+ dec hl
+ ld l,a
+ dec de ; LD C,H + LD B,E + LD A,L + DEC HL + LD L,A + DEC DE + LD E,B
+ ld e,b ; = 4+4+4+6+4+6+4 = 32 Ts < 42 Ts 2*(PUSH+POP)
+ inc d ; DE = scanline7 of this row
+ inc h ; HL = scanline1' of next row
+ call ldi32 ; 539 Ts needed for each row except last row
+ dec hl ; with JR Z,EXIT before, we save 2*539-161 = 917 Ts for last row
+ jp loop1
+exit:
+ ld hl,buffer0
+ call ldi32 ; DE = scanline6 of last row
+ dec de
+ ld e,a
+ inc d ; DE = scanline7 of last row
+ call ldi32
+ pop ix
+ ret
+ ENDP
+
+buffer0:
+ defs 32
+
+buffer1:
+ defs 32
+
+ldi32:
+ ldi ; 32 LDI = 32*16 = 512 Ts
+ ldi ; CALL + RET = 17+10 = 27 Ts
+ ldi ; call ldi32 (+ret) = 512+27 = 539 Ts
+ ldi
+ ldi ; LD BC,32 + LDIR = 10 + 21*31+16 = 677 Ts
+ ldi
+ ldi ; 677 - 539 = 138 Ts
+ ldi
+ ldi ; then, "call ldi32 (+ret)" is 138 Ts faster than "ld bc,32 + ldir"
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi
+ ldi ; BC-=32 DE+=32 HL+=32 (if E,L was >=224, then D,H changes)
+END ASM
+END SUB
diff --git a/examples/sd81/pegdemo.bas b/examples/sd81/pegdemo.bas
new file mode 100644
index 000000000..83d43f820
--- /dev/null
+++ b/examples/sd81/pegdemo.bas
@@ -0,0 +1,54 @@
+' ----------------------------------------------------------------------
+' pegdemo.bas — PEG (Programmable Effects Generator) demo
+'
+' The PEG is a small virtual machine inside the SD81 Booster's MCU: it
+' runs sound-effect programs on its own, driving the AY registers
+' directly, with zero Z80 CPU cost. Up to 3 threads (0-2) can run at
+' once. See the SD81 Booster manual, Appendix B, for the instruction
+' set; McuLoadPeg's raw byte format is documented in mcu.bas ("2 bytes
+' per instruction, little-endian").
+'
+' This program below is a simple one-shot "beep": set AY channel A's
+' tone period, enable tone on the mixer, set full volume, hold for
+' ~200ms, then silence and halt.
+' LD R0, $30 ; tone period A, low byte
+' LD R1, $00 ; tone period A, high byte
+' LD R7, $3E ; mixer: tone A enabled, everything else disabled
+' LD R8, $0F ; channel A volume = max, no envelope
+' WAIT 200 ; hold for 200 ms
+' LD R8, $00 ; silence
+' HALT
+'
+' NOTE: this exact byte encoding hasn't been confirmed on real hardware
+' yet -- if it doesn't produce sound, double check the byte order
+' against a working .PEB file or the peg.py assembler mentioned in the
+' manual.
+' ----------------------------------------------------------------------
+
+#include
+#include
+
+dim beep$ as string
+
+beep$ = chr($30) + chr($00) + _
+ chr($00) + chr($01) + _
+ chr($3E) + chr($07) + _
+ chr($0F) + chr($08) + _
+ chr($C8) + chr($90) + _
+ chr($00) + chr($08) + _
+ chr($10) + chr($A0)
+
+print "Loading PEG program at address 0..."
+McuLoadPeg(0, beep$)
+
+print "Playing on thread 0..."
+McuPlayPeg(0, 0)
+
+pause 100
+
+print "Stopping thread 0."
+McuStopPeg(0)
+
+print
+print "Press a key..."
+a$ = input(20)
diff --git a/examples/sd81/pong.bas b/examples/sd81/pong.bas
new file mode 100644
index 000000000..cc11a2888
--- /dev/null
+++ b/examples/sd81/pong.bas
@@ -0,0 +1,237 @@
+
+10 REM Pong! Game
+
+14 REM Digit data
+ REM 128 => "\ "
+ REM 131 => "\''"
+ REM 138 => "\: "
+ REM 133 => "\ :"
+ REM 139 => "\:'"
+ REM 141 => "\.:"
+ REM 140 => "\.."
+ REM 135 => "\':"
+ REM 142 => "\:."
+
+15 DIM numbers(9, 2, 1) as Ubyte = { _
+ {{139, 135}, {138, 133}, {142, 141}}, _ ' Number 0
+ {{128, 133}, {128, 133}, {128, 133}}, _ ' Number 1
+ {{131, 135}, {139, 131}, {142, 140}}, _ ' Number 2
+ {{131, 135}, {131, 135}, {140, 141}}, _ ' Number 3
+ {{138, 133}, {131, 135}, {128, 133}}, _ ' Number 4
+ {{139, 131}, {131, 135}, {140, 141}}, _ ' Number 5
+ {{139, 131}, {139, 135}, {142, 141}}, _ ' Number 6
+ {{131, 135}, {128, 133}, {128, 133}}, _ ' Number 7
+ {{139, 135}, {139, 135}, {142, 141}}, _ ' Number 8
+ {{139, 135}, {131, 135}, {140, 141}} _ ' Number 9
+ }
+20 BORDER 0: PAPER 0: INK 7: BRIGHT 1: OVER 1: CLS
+ PRINT AT 10, 5; BOLD 1; "PRESS Q or 4 TO MOVE UP"
+ PRINT AT 11, 4; BOLD 1; "PRESS A or 3 TO MOVE DOWN": PAUSE 0
+ DIM h, xx, yy, p, oldX, oldY As Byte
+ DIM x, y, dx, dy as Fixed
+ DIM px, py, NUM as Byte
+ DIM i, delay as Uinteger
+ DIM diff as Float = 0.5 : REM difficulty level
+ CONST minX As Byte = 0
+ CONST minY As Byte = 0
+ CONST maxX As Byte = 61
+ CONST maxY As Byte = 47
+ CONST comp As Byte = 0: REM Computer player is left
+ CONST user As Byte = 1: REM User player is left
+ CONST startX as Byte = maxX / 2: REM Ball initial X coord
+ CONST startY as Byte = maxY / 2: REM Ball initial Y coord
+ CONST dFast as UInteger = 400
+ CONST dSlow as UInteger = 1000
+ CONST diffRate As Float = 1.125: REM difficulty rate
+ CONST pointsToWin As Byte = 10: REM Points to Win
+
+ DIM score(1) As Byte: REM Scores
+
+30 CLS: LET xx = 31: FOR yy = minY TO maxY STEP 2: GOSUB 2000: NEXT yy
+40 DIM coords(1, 1) As Byte: REM Player 0 (Left) and 1 (Right) coordinates
+50 LET h = 5: REM players height (in "points"). A "point" is 4 pixels
+60 LET x = startX: LET y = startY: REM Screen resolution is 64
+70 LET coords(0, 0) = minX: LET coords(1, 0) = maxX
+80 LET coords(0, 1) = minY + (maxY - minY) / 2: LET coords(1, 1) = coords(0, 1)
+90 LET dx = 1: LET dy = 1: LET delay = dSlow: REM Ball speed
+91 LET score(0) = 0: LET score(1) = 0: REM Initializes scores
+
+93 FOR p = 0 TO 1: GOSUB 1000: NEXT p: REM Draw players
+94 GOSUB 3000: REM Print scores
+
+95 REM Draws Ball. Begin of GAME LOOP
+96 LET xx = x: LET yy = y: LET oldX = x: LET oldY = y: GOSUB 2000
+100 LET x = x + dx
+110 IF x < minX THEN
+ LET x = minX: LET dx = -dx: BEEP 0.02,20
+ LET py = coords(0, 1)
+ IF py > y OR py + h <= y THEN
+ REM Player 2 wins 1 Point
+ LET score(1) = score(1) + 1
+ GOSUB 3000
+ LET y = startY
+ LET x = coords(1, 0)
+ LET dx = -dx
+ LET delay = dSlow
+ IF comp <> 1 THEN
+ LET diff = diff / diffRate
+ ELSE
+ LET diff = diff * diffRate
+ END IF
+ ELSE
+ IF y = py OR y = py + h - 1 THEN
+ delay = dFast
+ ELSE
+ delay = dSlow
+ END IF
+ END IF
+ ELSEIF x > maxX THEN
+ LET x = maxX: LET dx = -dx: BEEP 0.02,20
+ LET py = coords(1, 1)
+ IF py > y OR py + h <= y THEN
+ REM Player 1 wins 1 Point
+ LET score(0) = score(0) + 1
+ GOSUB 3000
+ LET y = startY
+ LET x = coords(0, 0)
+ LET dx = -dx
+ LET delay = dSlow
+ IF comp <> 0 THEN
+ LET diff = diff / diffRate
+ ELSE
+ LET diff = diff * diffRate
+ END IF
+ ELSE
+ IF y = py OR y = py + h - 1 THEN
+ delay = dFast
+ ELSE
+ delay = dSlow
+ END IF
+ END IF
+ END IF
+
+120 LET y = y + dy
+130 IF y < minY THEN
+ LET y = minY: BEEP 0.02,10: LET dy = -dy
+ ELSEIF y > maxY THEN
+ LET y = maxY: BEEP 0.02,10: LET dy = -dy
+ END IF
+
+140 REM Checks if computer must move
+150 LET px = coords(comp, 0): LET py = coords(comp, 1)
+160 IF py > y AND RND > diff THEN REM Must go up
+ LET p = comp
+ GOSUB 1500: REM Updates player padel (up)
+ ELSEIF py + h - 1 < y AND RND > diff THEN
+ LET p = comp
+ GOSUB 1600: REM Updates player padel (down)
+ END IF
+
+200 REM Checks if Player moves ("4", "3")
+210 LET px = coords(user, 0): LET py = coords(user, 1)
+220 if py > minY AND (INKEY$ = "4" OR INKEY$ = "q") THEN REM Must go up
+ LET p = user
+ GOSUB 1500: REM Updates player padel (up)
+ ELSEIF py + h < maxY AND (INKEY$ = "3" OR INKEY$ = "a") THEN REM Must go down
+ LET p = user
+ GOSUB 1600: REM Updates player padel (down)
+ END IF
+
+250 FOR i = 0 TO delay: NEXT i
+
+500 LET xx = oldX: LET yy = oldY
+ ASM
+ call .core.VSYNC_TICK ; garantiza al menos un frame esperado (sustituye EI+HALT)
+ ; halt ; Avoids screen flickering
+ END ASM
+ GOSUB 2000: REM erases the ball
+ GOTO 95: REM Game loop
+
+0999 END
+1000 REM Draws player p at p(p, 0), p(p, 1)
+1010 LET xx = coords(p, 0)
+1020 LET yy = coords(p, 1)
+1030 FOR i = 1 TO h: GOSUB 2000: LET yy = yy + 1: NEXT i
+1040 RETURN
+
+1500 REM Moves up the padel for user p (p = 0 => Left, p = 1 = Right)
+1510 LET xx = px
+1520 FOR i = 0 TO 1: IF py <= minY THEN EXIT FOR: END IF
+1530 LET py = py - 1
+1540 LET yy = py: GOSUB 2000: REM Draws padel top
+1550 LET yy = py + h: GOSUB 2000: REM Erases padel bottom
+1560 NEXT i
+1570 LET coords(p, 1) = py: RETURN
+
+1600 REM Moves down the padel for user p (p = 0 => Left, p = 1 = Right)
+1610 LET xx = px: LET yy = py
+1620 FOR i = 0 TO 1: IF py + h > maxY THEN EXIT FOR: END IF
+1630 LET yy = py: GOSUB 2000: REM Erases padel top
+1640 LET yy = py + h: GOSUB 2000: REM Draws padel bottom
+1650 LET py = py + 1
+1660 NEXT i
+1670 LET coords(p, 1) = py: RETURN
+
+2000 REM Draws a "Point" at xx,yy
+2010 LET cx1 = xx bAND 1: LET cy1 = yy bAND 1
+2020 LET cx2 = xx SHR 1: LET cy2 = yy SHR 1
+2040 IF cx1 THEN
+ IF cy1 THEN
+ LET c$ = "\ ."
+ ELSE
+ LET c$ = "\ '"
+ END IF
+ ELSE
+ IF cy1 THEN
+ LET c$ = "\. "
+ ELSE
+ LET c$ = "\' "
+ END IF
+ END IF
+
+2050 PRINT AT cy2, cx2; c$;
+2060 RETURN
+
+3000 REM Prints SCORES
+3010 FOR player = 0 TO 1
+3020 LET atY = minY + 1: LET atX = minX + 4 + player * (maxX - minX) / 4: LET sc = score(player)
+3030 IF sc < 10 THEN REM erases left digit
+ FOR i = 0 TO 2: PRINT OVER 0; AT atY + i, atX; " ";:NEXT i
+ ELSE
+ LET NUM = sc / 10: REM Left digit
+ GOSUB 5000
+ END IF
+3040 LET atX = atX + 3
+3050 LET NUM = sc Mod 10
+3060 GOSUB 5000
+3070 NEXT player
+3080 GOSUB 6000
+3090 RETURN
+
+5000 REM Prints Number NUM at atY, atX
+5010 FOR ny = 0 TO 2: FOR nx = 0 TO 1
+5020 PRINT OVER 0; AT atY + ny, atX + nx; CHR$(numbers(NUM, ny, nx));
+5030 NEXT nx: NEXT ny
+5040 RETURN
+
+6000 REM Check scores
+6010 FOR player = 0 TO 1
+6020 IF score(player) >= pointsToWin THEN
+ CLS
+ PRINT AT 10, 10;
+ IF player = comp THEN
+ PRINT BOLD 1; "I WIN!"
+ ELSE
+ PRINT BOLD 1; "YOU WIN!"
+ END IF
+ DO LOOP WHILE INKEY$ <> ""
+ PAUSE 500
+ PRINT AT 21, 3; BOLD 1; "PRESS ANY KEY TO CONTINUE";
+ PAUSE 0
+ GOTO 30
+ END IF
+6030 NEXT player
+6040 RETURN
+
+#require "vsync.asm"
+
diff --git a/examples/sd81/print4264.bas b/examples/sd81/print4264.bas
new file mode 100644
index 000000000..99c60d5f0
--- /dev/null
+++ b/examples/sd81/print4264.bas
@@ -0,0 +1,13 @@
+#include
+#include
+
+CLS
+PRINT AT 0, 0; "print42/print64 test"
+
+printat42(3, 0)
+print42("HELLO 42 COLUMNS TEST 0123456789")
+
+printat64(10, 0)
+print64("HELLO 64 COLUMNS TEST 0123456789")
+
+PAUSE 0
diff --git a/examples/sd81/putcharstile.bas b/examples/sd81/putcharstile.bas
new file mode 100644
index 000000000..3ed652596
--- /dev/null
+++ b/examples/sd81/putcharstile.bas
@@ -0,0 +1,48 @@
+' ----------------------------------------------------------------------
+' putcharstile.bas — putchars.bas (putChars/paint) and puttile.bas (putTile)
+'
+' putchars.bas: audited, no ROM dependency (everything reads
+' SCREEN_ADDR/SCREEN_ATTR_ADDR dynamically) -- no zx81sd override
+' needed, used as-is from the shared zx48k stdlib.
+'
+' puttile.bas: DID need a zx81sd override -- the original hardcodes the
+' screen/attribute base as immediate constants (add a,64 / add a,88,
+' the Spectrum ROM's fixed $4000/$5800 high bytes) instead of reading
+' SCREEN_ADDR/SCREEN_ATTR_ADDR. See src/lib/arch/zx81sd/stdlib/puttile.bas.
+' ----------------------------------------------------------------------
+
+#include
+#include
+#include
+
+dim i as ubyte
+dim charData(95) as ubyte
+dim tileData(35) as ubyte
+
+' 12 character cells (4 wide x 3 tall), 8 bytes each: a houndstooth fill
+for i = 0 to 95
+ poke @charData(0) + i, 170
+next i
+
+' 16x16 tile: 16 pixel-rows (2 bytes each, one per column) + top/bottom attrs
+for i = 0 to 15
+ poke @tileData(0) + i * 2, 255
+ poke @tileData(0) + i * 2 + 1, 129
+next i
+poke @tileData(0) + 32, 56
+poke @tileData(0) + 33, 56
+poke @tileData(0) + 34, 63
+poke @tileData(0) + 35, 63
+
+ink 7: paper 1: cls
+
+putChars(1, 1, 4, 3, @charData(0))
+paint(6, 1, 4, 3, 71)
+
+putTile(10, 10, @tileData(0))
+
+' fixed-row PRINT away from the last screen row: avoids triggering a
+' scroll (input()'s own cursor handling can push past row 23) that
+' would shift the drawing above right before you get to look at it
+print at 20, 0; "putChars/paint/putTile - press a key";
+a$ = input(20)
diff --git a/examples/sd81/randomaccess.bas b/examples/sd81/randomaccess.bas
new file mode 100644
index 000000000..e2ee44e97
--- /dev/null
+++ b/examples/sd81/randomaccess.bas
@@ -0,0 +1,70 @@
+' ----------------------------------------------------------------------
+' randomaccess.bas — Random file access via the SD81 Booster MCU
+'
+' Demonstrates the F_* handle-based commands in mcu.bas: F_OPEN,
+' F_SEEK, F_READ, F_WRITE and F_CLOSE. Unlike McuLoad/McuSave (which
+' always transfer the whole file), these let a program seek to an
+' arbitrary offset and read or write just part of a file -- useful for
+' things like save-game slots, tile/level data files, or databases too
+' big to keep fully in RAM.
+' ----------------------------------------------------------------------
+
+#include
+#include
+
+dim h as ubyte
+dim st as ubyte
+dim buf(63) as ubyte
+dim i as uinteger
+dim ok as ubyte
+
+print "Creating a 64-byte test file..."
+for i = 0 to 63
+ poke @buf(0) + i, cast(ubyte, 200 - i)
+next i
+st = McuSave("RNDACC.BIN", @buf(0), 64)
+print "SAVE: status "; st
+
+print "Opening it..."
+h = McuFOpenZx("RNDACC.BIN")
+print "F_OPEN: handle "; h
+
+print "Seeking to offset 32 and reading 8 bytes..."
+st = McuFSeek(h, 32)
+for i = 0 to 7
+ poke @buf(0) + i, 0
+next i
+st = McuFRead(h, @buf(0), 8)
+ok = 1
+for i = 0 to 7
+ if peek(@buf(0) + i) <> 200 - (32 + i) then
+ ok = 0
+ end if
+next i
+if ok = 1 then
+ print "Read back correctly."
+else
+ print "Read verification FAILED."
+end if
+
+print "Overwriting the first 4 bytes and reading them back..."
+st = McuFSeek(h, 0)
+poke @buf(32), 11
+poke @buf(33), 22
+poke @buf(34), 33
+poke @buf(35), 44
+st = McuFWrite(h, @buf(32), 4)
+st = McuFSeek(h, 0)
+st = McuFRead(h, @buf(40), 4)
+if peek(@buf(40)) = 11 and peek(@buf(43)) = 44 then
+ print "Overwrite verified."
+else
+ print "Overwrite verification FAILED."
+end if
+
+print "F_CLOSE: status "; McuFClose(h)
+print "Deleting test file: status "; McuDel("RNDACC.BIN")
+
+print
+print "Press a key..."
+a$ = input(20)
diff --git a/examples/sd81/snake_sd81.bas b/examples/sd81/snake_sd81.bas
new file mode 100644
index 000000000..8b8231953
--- /dev/null
+++ b/examples/sd81/snake_sd81.bas
@@ -0,0 +1,165 @@
+1 REM ********************************************************************
+2 REM ZXSnake by Federico J. Alvarez Valero (05-02-2003)
+10 REM This program is free software; you can redistribute it and/or modify
+11 REM it under the terms of the GNU General Public License as published by
+12 REM the Free Software Foundation; either version 2 of the License, or
+13 REM (at your option) any later version.
+14 REM This program is distributed in the hope that it will be useful,
+15 REM but WITHOUT ANY WARRANTY; without even the implied warranty of
+16 REM MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+17 REM GNU General Public License for more details.
+18 REM You should have received a copy of the GNU General Public License
+19 REM along with this program; if not, write to the Free Software
+20 REM Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+30 REM ENGLISH VERSION
+40 REM ********************************************************************
+
+50 BORDER 7 : PAPER 7 : INK 0 : CLS
+51 PRINT AT 3,13 ; PAPER 1 ; INK 7 ; "ZXSnake"
+52 PRINT AT 5,9 ; PAPER 7 ; INK 0 ; "Q - Up"
+53 PRINT AT 6,9 ; PAPER 7 ; INK 0 ; "A - Down"
+54 PRINT AT 7,9 ; PAPER 7 ; INK 0 ; "O - Left"
+55 PRINT AT 8,9 ; PAPER 7 ; INK 0 ; "P - Right"
+56 PRINT AT 10,3 ; PAPER 7 ; INK 0 ; "You have to pick up every"
+57 PRINT AT 11,3 ; PAPER 7 ; INK 0 ; "fruit on the screen while"
+58 PRINT AT 12,3 ; PAPER 7 ; INK 0 ; "growing up more and more..."
+59 PRINT AT 15,3 ; PAPER 7 ; INK 0 ; "Press any key to start"
+60 LET j$ = INKEY$
+61 IF j$ = "" THEN GOTO 60: END IF
+
+70 REM UDG
+71 DIM udg(1, 7) AS uByte => { {60, 66, 129, 129, 129, 129, 66, 60}, _
+ {24, 60, 60, 60, 126, 251, 247, 126}}
+73 POKE UINTEGER $8002, @udg(0, 0): REM sysvar UDG de zx81sd (en Spectrum era 23675)
+74 LET S$ = CHR$(144): LET F$ = CHR$(145)
+
+75 REM Variable declaration
+76 DIM p(23,34) AS UBYTE: REM Screen
+77 DIM x(23,34) AS UBYTE: REM Xorientations
+78 DIM y(23,34) AS UBYTE: REM Yorientations
+
+79 DIM c, f AS UBYTE
+80 DIM headx, heady AS UBYTE : REM Head coordinates
+81 DIM tailx, taily AS UBYTE : REM Tail coordinates
+90 DIM score, eaten as ULONG
+95 DIM maxx, maxy, minx, miny as UByte
+
+100 REM Variable definition
+110 LET headx = 11 : REM head x coordinate
+120 LET heady = 5 : REM head y coordinate
+130 LET tailx = 5 : REM tail x coordinate
+140 LET taily = 5 : REM tail y coordinate
+150 LET orientationx = 1
+160 LET orientationy = 0
+
+165 REM Clear arrays p, x and y
+170 FOR c = 1 to 23: FOR f = 1 to 34
+180 LET p(c, f) = 0: LET x(c, f) = 0: LET y(c, f) = 0
+190 NEXT f: NEXT c
+
+200 LET score = 0
+210 LET eaten = 0
+220 LET maxx = 33
+230 LET maxy = 22
+240 LET minx = 0
+250 LET miny = 0
+
+1000 REM Screen Initialization
+1010 BORDER 1
+1015 CLS
+1020 PRINT AT 21,0 ; PAPER 1 ; INK 7 ; " SCORE : "
+1030 FOR c = minx TO maxx
+1040 LET p(miny+1,c+1) = 4
+1050 LET p(maxy+1,c+1) = 4
+1060 NEXT c
+1070 FOR f = miny TO maxy
+1080 LET p(f+1,minx+1) = 4
+1090 LET p(f+1,maxx+1) = 4
+1100 NEXT f
+
+1500 GOSUB 8000 : REM Place first fruit
+
+2000 REM Draw snake in its initial position
+2001 PAPER 7 : INK 0
+2005 REM Draw the body
+2010 FOR c = tailx TO headx-1
+2020 PRINT AT taily,c ; INK 0 ; "O"
+2025 LET p(taily+2,c+2) = 3
+2026 LET x(taily+2,c+2) = 1
+2027 LET y(taily+2,c+2) = 0
+2030 NEXT c
+2040 REM Draw the Head
+2050 PRINT AT heady,headx ; INK 0 ; S$;
+2055 LET p(heady+2,headx+2) = 2
+2056 LET x(heady+2,headx+2) = 1
+2057 LET y(heady+2,headx+2) = 0
+
+3000 REM Update snake position
+3005 INK 0
+3010 REM Change the orientation if needed
+3015 LET x(heady+2,headx+2) = orientationx
+3020 LET y(heady+2,headx+2) = orientationy
+3025 REM Erase previous head
+3030 PRINT AT heady,headx ; "O"
+3035 LET p(heady+2,headx+2) = 3
+3040 LET headx = headx + orientationx
+3045 LET heady = heady + orientationy
+3050 IF p(heady+2,headx+2) > 1 THEN GOTO 9900: END IF
+3051 IF p(heady+2,headx+2) = 1 THEN
+ LET score = score + 10 : PRINT AT 21,10 ; _
+ PAPER 1 ; INK 7 ; score : LET eaten = 1 : GOSUB 8000: END IF
+3055 REM Print the new head
+3060 PRINT AT heady,headx ; S$;
+3065 LET p(heady+2,headx+2) = 2
+3070 IF eaten = 0 THEN GOSUB 8100: END IF
+3080 LET eaten = 0
+
+3200 REM Read the keyboard
+3210 LET a$ = INKEY$
+3220 IF orientationx < 1 AND (a$ = "O" OR a$ = "o") THEN
+ LET orientationx = -1 : LET orientationy = 0: END IF
+3230 IF orientationx > -1 AND (a$ = "P" OR a$ = "p") THEN
+ LET orientationx = 1 : LET orientationy = 0: END IF
+3240 IF orientationy < 1 AND (a$ = "Q" OR a$ = "q") THEN
+ LET orientationx = 0 : LET orientationy = -1: END IF
+3250 IF orientationy > -1 AND (a$ = "A" OR a$ = "a") THEN
+ LET orientationx = 0 : LET orientationy = 1: END IF
+
+3500 REM Pausa / Delay
+3505 BEEP 0.005, 0
+3510 PAUSE 5
+
+7998 GOTO 3000
+
+8000 REM Fruit placement
+8010 LET fruitx = INT(RND*30)+1
+8020 LET fruity = INT(RND*20)+1
+8030 IF p(fruity+2,fruitx+2) = 0 THEN GOTO 8050: END IF
+8040 GOTO 8010
+8050 PRINT AT fruity,fruitx ; INK 2 ; F$;
+8060 LET p(fruity+2,fruitx+2) = 1
+8070 RETURN
+
+8100 REM Erase tail
+8110 PRINT AT taily,tailx ; " "
+8120 LET newtailx = tailx + x(taily+2,tailx+2)
+8130 LET newtaily = taily + y(taily+2,tailx+2)
+8140 LET p(taily+2,tailx+2) = 0
+8150 LET x(taily+2,tailx+2) = 0
+8160 LET y(taily+2,tailx+2) = 0
+8170 LET tailx = newtailx
+8180 LET taily = newtaily
+8190 RETURN
+
+9900 REM Game over
+9910 PRINT AT 10,12 ; INK 2 ; "GAME OVER..."
+9920 PRINT AT 11,10 ; INK 2 ; "SCORE : " ; score
+9930 PRINT AT 13,10 ; INK 0 ; "Press any key"
+9931 REM Mondatory pause so the msg. can be read
+9932 FOR i = 0 TO 100
+
+9933 NEXT i
+9940 LET j$ = INKEY$
+9950 IF j$ <> "" THEN GOTO 100: END IF
+9960 GOTO 9940
+
diff --git a/examples/sd81/vgmplayer.bas b/examples/sd81/vgmplayer.bas
new file mode 100644
index 000000000..cddb821ae
--- /dev/null
+++ b/examples/sd81/vgmplayer.bas
@@ -0,0 +1,50 @@
+' ----------------------------------------------------------------------
+' vgmplayer.bas — VGM music playback via the SD81 Booster MCU
+'
+' The MCU's built-in VGM player runs entirely in the background (only
+' AY-3-8910/8912 data is supported): the Z80 program keeps running
+' while the tune plays. McuPlayVgm() opens/prepares the file (adding
+' .vgm if no extension is given); playback only actually starts once
+' McuContVgm() is called.
+'
+' Change VGM_NAME below to a file that actually exists on your SD card.
+' ----------------------------------------------------------------------
+
+#include
+#include
+
+const VGM_NAME as string = "MUSIC.VGM"
+
+dim st as ubyte
+dim i as ubyte
+
+print "Loading "; VGM_NAME; "..."
+st = McuPlayVgm(VGM_NAME)
+print "MCU status: "; st
+
+print "Starting playback..."
+McuContVgm()
+
+' let it play for a few seconds while the Z80 keeps doing its own thing
+for i = 1 to 5
+ print "playing... "; i
+ pause 50
+next i
+
+print "Pausing 2 seconds..."
+McuPauseVgm()
+pause 100
+print "Resuming..."
+McuContVgm()
+
+for i = 1 to 5
+ print "playing... "; i
+ pause 50
+next i
+
+print "Stopping."
+McuStopVgm()
+
+print
+print "Press a key..."
+a$ = input(20)
diff --git a/examples/sd81/wavplayer.bas b/examples/sd81/wavplayer.bas
new file mode 100644
index 000000000..c4dc197aa
--- /dev/null
+++ b/examples/sd81/wavplayer.bas
@@ -0,0 +1,25 @@
+' ----------------------------------------------------------------------
+' wavplayer.bas — WAV audio playback via the SD81 Booster MCU
+'
+' The MCU's LOAD command (cmd 9, wrapped here as McuLoad) recognises
+' the .WAV extension automatically: instead of loading the file into
+' memory it plays it directly (uncompressed PCM, 8-bit mono, 11025 Hz
+' per the manual). McuLoad returns 0 bytes in that case; the real
+' result is in McuStatus.
+'
+' Change WAV_NAME below to a file that actually exists on your SD card
+' (with or without the .WAV extension).
+' ----------------------------------------------------------------------
+
+#include
+#include
+
+const WAV_NAME as string = "DEMO.WAV"
+
+print "Playing "; WAV_NAME; " ..."
+McuLoad(WAV_NAME, 0)
+print "Done. MCU status: "; McuStatus
+
+print
+print "Press a key..."
+a$ = input(20)
diff --git a/poetry.lock b/poetry.lock
index ffab4fdd8..8c580b3b3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,844 +1,1102 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
-
-[[package]]
-name = "bump2version"
-version = "1.0.1"
-description = "Version-bump your software with a single command!"
-optional = false
-python-versions = ">=3.5"
-files = [
- {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"},
- {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"},
-]
-
-[[package]]
-name = "cfgv"
-version = "3.4.0"
-description = "Validate configuration and produce human readable error messages."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
- {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
-]
-
-[[package]]
-name = "click"
-version = "8.2.1"
-description = "Composable command line interface toolkit"
-optional = false
-python-versions = ">=3.10"
-files = [
- {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"},
- {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"},
-]
-
-[package.dependencies]
-colorama = {version = "*", markers = "platform_system == \"Windows\""}
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-description = "Cross-platform colored terminal text."
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
-files = [
- {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
- {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
-]
-
-[[package]]
-name = "coverage"
-version = "7.9.2"
-description = "Code coverage measurement for Python"
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "coverage-7.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66283a192a14a3854b2e7f3418d7db05cdf411012ab7ff5db98ff3b181e1f912"},
- {file = "coverage-7.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e01d138540ef34fcf35c1aa24d06c3de2a4cffa349e29a10056544f35cca15f"},
- {file = "coverage-7.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f22627c1fe2745ee98d3ab87679ca73a97e75ca75eb5faee48660d060875465f"},
- {file = "coverage-7.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b1c2d8363247b46bd51f393f86c94096e64a1cf6906803fa8d5a9d03784bdbf"},
- {file = "coverage-7.9.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c10c882b114faf82dbd33e876d0cbd5e1d1ebc0d2a74ceef642c6152f3f4d547"},
- {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de3c0378bdf7066c3988d66cd5232d161e933b87103b014ab1b0b4676098fa45"},
- {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1e2f097eae0e5991e7623958a24ced3282676c93c013dde41399ff63e230fcf2"},
- {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28dc1f67e83a14e7079b6cea4d314bc8b24d1aed42d3582ff89c0295f09b181e"},
- {file = "coverage-7.9.2-cp310-cp310-win32.whl", hash = "sha256:bf7d773da6af9e10dbddacbf4e5cab13d06d0ed93561d44dae0188a42c65be7e"},
- {file = "coverage-7.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:0c0378ba787681ab1897f7c89b415bd56b0b2d9a47e5a3d8dc0ea55aac118d6c"},
- {file = "coverage-7.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7a56a2964a9687b6aba5b5ced6971af308ef6f79a91043c05dd4ee3ebc3e9ba"},
- {file = "coverage-7.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123d589f32c11d9be7fe2e66d823a236fe759b0096f5db3fb1b75b2fa414a4fa"},
- {file = "coverage-7.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333b2e0ca576a7dbd66e85ab402e35c03b0b22f525eed82681c4b866e2e2653a"},
- {file = "coverage-7.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326802760da234baf9f2f85a39e4a4b5861b94f6c8d95251f699e4f73b1835dc"},
- {file = "coverage-7.9.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e7be4cfec248df38ce40968c95d3952fbffd57b400d4b9bb580f28179556d2"},
- {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b4a4cb73b9f2b891c1788711408ef9707666501ba23684387277ededab1097c"},
- {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2c8937fa16c8c9fbbd9f118588756e7bcdc7e16a470766a9aef912dd3f117dbd"},
- {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42da2280c4d30c57a9b578bafd1d4494fa6c056d4c419d9689e66d775539be74"},
- {file = "coverage-7.9.2-cp311-cp311-win32.whl", hash = "sha256:14fa8d3da147f5fdf9d298cacc18791818f3f1a9f542c8958b80c228320e90c6"},
- {file = "coverage-7.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:549cab4892fc82004f9739963163fd3aac7a7b0df430669b75b86d293d2df2a7"},
- {file = "coverage-7.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2667a2b913e307f06aa4e5677f01a9746cd08e4b35e14ebcde6420a9ebb4c62"},
- {file = "coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0"},
- {file = "coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3"},
- {file = "coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1"},
- {file = "coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615"},
- {file = "coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b"},
- {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9"},
- {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f"},
- {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d"},
- {file = "coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355"},
- {file = "coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0"},
- {file = "coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b"},
- {file = "coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038"},
- {file = "coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d"},
- {file = "coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3"},
- {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14"},
- {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6"},
- {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b"},
- {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d"},
- {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868"},
- {file = "coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a"},
- {file = "coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b"},
- {file = "coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694"},
- {file = "coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5"},
- {file = "coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b"},
- {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3"},
- {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8"},
- {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46"},
- {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584"},
- {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e"},
- {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac"},
- {file = "coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926"},
- {file = "coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd"},
- {file = "coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb"},
- {file = "coverage-7.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddc39510ac922a5c4c27849b739f875d3e1d9e590d1e7b64c98dadf037a16cce"},
- {file = "coverage-7.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a535c0c7364acd55229749c2b3e5eebf141865de3a8f697076a3291985f02d30"},
- {file = "coverage-7.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df0f9ef28e0f20c767ccdccfc5ae5f83a6f4a2fbdfbcbcc8487a8a78771168c8"},
- {file = "coverage-7.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f3da12e0ccbcb348969221d29441ac714bbddc4d74e13923d3d5a7a0bebef7a"},
- {file = "coverage-7.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a17eaf46f56ae0f870f14a3cbc2e4632fe3771eab7f687eda1ee59b73d09fe4"},
- {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:669135a9d25df55d1ed56a11bf555f37c922cf08d80799d4f65d77d7d6123fcf"},
- {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9d3a700304d01a627df9db4322dc082a0ce1e8fc74ac238e2af39ced4c083193"},
- {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71ae8b53855644a0b1579d4041304ddc9995c7b21c8a1f16753c4d8903b4dfed"},
- {file = "coverage-7.9.2-cp39-cp39-win32.whl", hash = "sha256:dd7a57b33b5cf27acb491e890720af45db05589a80c1ffc798462a765be6d4d7"},
- {file = "coverage-7.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f65bb452e579d5540c8b37ec105dd54d8b9307b07bcaa186818c104ffda22441"},
- {file = "coverage-7.9.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:8a1166db2fb62473285bcb092f586e081e92656c7dfa8e9f62b4d39d7e6b5050"},
- {file = "coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4"},
- {file = "coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b"},
-]
-
-[package.extras]
-toml = ["tomli"]
-
-[[package]]
-name = "distlib"
-version = "0.4.0"
-description = "Distribution utilities"
-optional = false
-python-versions = "*"
-files = [
- {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"},
- {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"},
-]
-
-[[package]]
-name = "execnet"
-version = "2.1.1"
-description = "execnet: rapid multi-Python deployment"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"},
- {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"},
-]
-
-[package.extras]
-testing = ["hatch", "pre-commit", "pytest", "tox"]
-
-[[package]]
-name = "filelock"
-version = "3.18.0"
-description = "A platform independent file lock."
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"},
- {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"},
-]
-
-[package.extras]
-docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"]
-typing = ["typing-extensions (>=4.12.2)"]
-
-[[package]]
-name = "ghp-import"
-version = "2.1.0"
-description = "Copy your docs directly to the gh-pages branch."
-optional = false
-python-versions = "*"
-files = [
- {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
- {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
-]
-
-[package.dependencies]
-python-dateutil = ">=2.8.1"
-
-[package.extras]
-dev = ["flake8", "markdown", "twine", "wheel"]
-
-[[package]]
-name = "identify"
-version = "2.6.12"
-description = "File identification library for Python"
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2"},
- {file = "identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6"},
-]
-
-[package.extras]
-license = ["ukkonen"]
-
-[[package]]
-name = "iniconfig"
-version = "2.1.0"
-description = "brain-dead simple config-ini parsing"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
- {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
-]
-
-[[package]]
-name = "jinja2"
-version = "3.1.6"
-description = "A very fast and expressive template engine."
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
- {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
-]
-
-[package.dependencies]
-MarkupSafe = ">=2.0"
-
-[package.extras]
-i18n = ["Babel (>=2.7)"]
-
-[[package]]
-name = "markdown"
-version = "3.8.2"
-description = "Python implementation of John Gruber's Markdown."
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"},
- {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"},
-]
-
-[package.extras]
-docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
-testing = ["coverage", "pyyaml"]
-
-[[package]]
-name = "markupsafe"
-version = "3.0.2"
-description = "Safely add untrusted strings to HTML/XML markup."
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
- {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
-]
-
-[[package]]
-name = "mergedeep"
-version = "1.3.4"
-description = "A deep merge function for 🐍."
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
- {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
-]
-
-[[package]]
-name = "mkdocs"
-version = "1.6.1"
-description = "Project documentation with Markdown."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"},
- {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"},
-]
-
-[package.dependencies]
-click = ">=7.0"
-colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
-ghp-import = ">=1.0"
-jinja2 = ">=2.11.1"
-markdown = ">=3.3.6"
-markupsafe = ">=2.0.1"
-mergedeep = ">=1.3.4"
-mkdocs-get-deps = ">=0.2.0"
-packaging = ">=20.5"
-pathspec = ">=0.11.1"
-pyyaml = ">=5.1"
-pyyaml-env-tag = ">=0.1"
-watchdog = ">=2.0"
-
-[package.extras]
-i18n = ["babel (>=2.9.0)"]
-min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"]
-
-[[package]]
-name = "mkdocs-get-deps"
-version = "0.2.0"
-description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"},
- {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"},
-]
-
-[package.dependencies]
-mergedeep = ">=1.3.4"
-platformdirs = ">=2.2.0"
-pyyaml = ">=5.1"
-
-[[package]]
-name = "mypy"
-version = "1.17.0"
-description = "Optional static typing for Python"
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6"},
- {file = "mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d"},
- {file = "mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b"},
- {file = "mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a"},
- {file = "mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f"},
- {file = "mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937"},
- {file = "mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be"},
- {file = "mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61"},
- {file = "mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f"},
- {file = "mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d"},
- {file = "mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3"},
- {file = "mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70"},
- {file = "mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb"},
- {file = "mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d"},
- {file = "mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8"},
- {file = "mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e"},
- {file = "mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8"},
- {file = "mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d"},
- {file = "mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06"},
- {file = "mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a"},
- {file = "mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889"},
- {file = "mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba"},
- {file = "mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658"},
- {file = "mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c"},
- {file = "mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab"},
- {file = "mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad"},
- {file = "mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c"},
- {file = "mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8"},
- {file = "mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97"},
- {file = "mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4"},
- {file = "mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496"},
- {file = "mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03"},
-]
-
-[package.dependencies]
-mypy_extensions = ">=1.0.0"
-pathspec = ">=0.9.0"
-typing_extensions = ">=4.6.0"
-
-[package.extras]
-dmypy = ["psutil (>=4.0)"]
-faster-cache = ["orjson"]
-install-types = ["pip"]
-mypyc = ["setuptools (>=50)"]
-reports = ["lxml"]
-
-[[package]]
-name = "mypy-extensions"
-version = "1.1.0"
-description = "Type system extensions for programs checked with the mypy type checker."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
- {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
-]
-
-[[package]]
-name = "nodeenv"
-version = "1.9.1"
-description = "Node.js virtual environment builder"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
-files = [
- {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
- {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
-]
-
-[[package]]
-name = "packaging"
-version = "25.0"
-description = "Core utilities for Python packages"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
- {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
-]
-
-[[package]]
-name = "pastel"
-version = "0.2.1"
-description = "Bring colors to your terminal."
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-files = [
- {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},
- {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"},
-]
-
-[[package]]
-name = "pathspec"
-version = "0.12.1"
-description = "Utility library for gitignore style pattern matching of file paths."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
- {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
-]
-
-[[package]]
-name = "platformdirs"
-version = "4.3.8"
-description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"},
- {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"},
-]
-
-[package.extras]
-docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
-test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"]
-type = ["mypy (>=1.14.1)"]
-
-[[package]]
-name = "pluggy"
-version = "1.6.0"
-description = "plugin and hook calling mechanisms for python"
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
- {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
-]
-
-[package.extras]
-dev = ["pre-commit", "tox"]
-testing = ["coverage", "pytest", "pytest-benchmark"]
-
-[[package]]
-name = "poethepoet"
-version = "0.36.0"
-description = "A task runner that works well with poetry and uv."
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "poethepoet-0.36.0-py3-none-any.whl", hash = "sha256:693e3c1eae9f6731d3613c3c0c40f747d3c5c68a375beda42e590a63c5623308"},
- {file = "poethepoet-0.36.0.tar.gz", hash = "sha256:2217b49cb4e4c64af0b42ff8c4814b17f02e107d38bc461542517348ede25663"},
-]
-
-[package.dependencies]
-pastel = ">=0.2.1,<0.3.0"
-pyyaml = ">=6.0.2,<7.0"
-
-[package.extras]
-poetry-plugin = ["poetry (>=1.2.0,<3.0.0)"]
-
-[[package]]
-name = "pre-commit"
-version = "4.2.0"
-description = "A framework for managing and maintaining multi-language pre-commit hooks."
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"},
- {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"},
-]
-
-[package.dependencies]
-cfgv = ">=2.0.0"
-identify = ">=1.0.0"
-nodeenv = ">=0.11.1"
-pyyaml = ">=5.1"
-virtualenv = ">=20.10.0"
-
-[[package]]
-name = "pygments"
-version = "2.19.2"
-description = "Pygments is a syntax highlighting package written in Python."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
- {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
-]
-
-[package.extras]
-windows-terminal = ["colorama (>=0.4.6)"]
-
-[[package]]
-name = "pytest"
-version = "8.4.1"
-description = "pytest: simple powerful testing with Python"
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"},
- {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"},
-]
-
-[package.dependencies]
-colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
-iniconfig = ">=1"
-packaging = ">=20"
-pluggy = ">=1.5,<2"
-pygments = ">=2.7.2"
-
-[package.extras]
-dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
-
-[[package]]
-name = "pytest-cov"
-version = "6.2.1"
-description = "Pytest plugin for measuring coverage."
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"},
- {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"},
-]
-
-[package.dependencies]
-coverage = {version = ">=7.5", extras = ["toml"]}
-pluggy = ">=1.2"
-pytest = ">=6.2.5"
-
-[package.extras]
-testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
-
-[[package]]
-name = "pytest-xdist"
-version = "3.8.0"
-description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"},
- {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"},
-]
-
-[package.dependencies]
-execnet = ">=2.1"
-pytest = ">=7.0.0"
-
-[package.extras]
-psutil = ["psutil (>=3.0)"]
-setproctitle = ["setproctitle"]
-testing = ["filelock"]
-
-[[package]]
-name = "python-dateutil"
-version = "2.9.0.post0"
-description = "Extensions to the standard Python datetime module"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
-files = [
- {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
- {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
-]
-
-[package.dependencies]
-six = ">=1.5"
-
-[[package]]
-name = "pyyaml"
-version = "6.0.2"
-description = "YAML parser and emitter for Python"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
- {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
- {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
- {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
- {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
- {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
- {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
- {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
- {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
- {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
- {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
- {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
- {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
- {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
- {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
- {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
- {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
- {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
- {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
- {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
- {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
- {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
- {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
- {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
- {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
- {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
- {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
- {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
- {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
- {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
- {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
- {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
- {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
- {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
- {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
- {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
- {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
- {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
- {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
- {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
- {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
- {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
- {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
- {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
- {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
- {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
- {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
- {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
- {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
- {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
- {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
- {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
- {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
-]
-
-[[package]]
-name = "pyyaml-env-tag"
-version = "1.1"
-description = "A custom YAML tag for referencing environment variables in YAML files."
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"},
- {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"},
-]
-
-[package.dependencies]
-pyyaml = "*"
-
-[[package]]
-name = "ruff"
-version = "0.12.4"
-description = "An extremely fast Python linter and code formatter, written in Rust."
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "ruff-0.12.4-py3-none-linux_armv6l.whl", hash = "sha256:cb0d261dac457ab939aeb247e804125a5d521b21adf27e721895b0d3f83a0d0a"},
- {file = "ruff-0.12.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:55c0f4ca9769408d9b9bac530c30d3e66490bd2beb2d3dae3e4128a1f05c7442"},
- {file = "ruff-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a8224cc3722c9ad9044da7f89c4c1ec452aef2cfe3904365025dd2f51daeae0e"},
- {file = "ruff-0.12.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9949d01d64fa3672449a51ddb5d7548b33e130240ad418884ee6efa7a229586"},
- {file = "ruff-0.12.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be0593c69df9ad1465e8a2d10e3defd111fdb62dcd5be23ae2c06da77e8fcffb"},
- {file = "ruff-0.12.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7dea966bcb55d4ecc4cc3270bccb6f87a337326c9dcd3c07d5b97000dbff41c"},
- {file = "ruff-0.12.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afcfa3ab5ab5dd0e1c39bf286d829e042a15e966b3726eea79528e2e24d8371a"},
- {file = "ruff-0.12.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c057ce464b1413c926cdb203a0f858cd52f3e73dcb3270a3318d1630f6395bb3"},
- {file = "ruff-0.12.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64b90d1122dc2713330350626b10d60818930819623abbb56535c6466cce045"},
- {file = "ruff-0.12.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abc48f3d9667fdc74022380b5c745873499ff827393a636f7a59da1515e7c57"},
- {file = "ruff-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2b2449dc0c138d877d629bea151bee8c0ae3b8e9c43f5fcaafcd0c0d0726b184"},
- {file = "ruff-0.12.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:56e45bb11f625db55f9b70477062e6a1a04d53628eda7784dce6e0f55fd549eb"},
- {file = "ruff-0.12.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:478fccdb82ca148a98a9ff43658944f7ab5ec41c3c49d77cd99d44da019371a1"},
- {file = "ruff-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0fc426bec2e4e5f4c4f182b9d2ce6a75c85ba9bcdbe5c6f2a74fcb8df437df4b"},
- {file = "ruff-0.12.4-py3-none-win32.whl", hash = "sha256:4de27977827893cdfb1211d42d84bc180fceb7b72471104671c59be37041cf93"},
- {file = "ruff-0.12.4-py3-none-win_amd64.whl", hash = "sha256:fe0b9e9eb23736b453143d72d2ceca5db323963330d5b7859d60d101147d461a"},
- {file = "ruff-0.12.4-py3-none-win_arm64.whl", hash = "sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e"},
- {file = "ruff-0.12.4.tar.gz", hash = "sha256:13efa16df6c6eeb7d0f091abae50f58e9522f3843edb40d56ad52a5a4a4b6873"},
-]
-
-[[package]]
-name = "setuptools"
-version = "78.1.1"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"},
- {file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"},
-]
-
-[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"]
-core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
-cover = ["pytest-cov"]
-doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
-enabler = ["pytest-enabler (>=2.2)"]
-test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
-type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"]
-
-[[package]]
-name = "six"
-version = "1.17.0"
-description = "Python 2 and 3 compatibility utilities"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
-files = [
- {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
- {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.14.1"
-description = "Backported and Experimental Type Hints for Python 3.9+"
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"},
- {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"},
-]
-
-[[package]]
-name = "virtualenv"
-version = "20.31.2"
-description = "Virtual Python Environment builder"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"},
- {file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"},
-]
-
-[package.dependencies]
-distlib = ">=0.3.7,<1"
-filelock = ">=3.12.2,<4"
-platformdirs = ">=3.9.1,<5"
-
-[package.extras]
-docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
-test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
-
-[[package]]
-name = "watchdog"
-version = "6.0.0"
-description = "Filesystem events monitoring"
-optional = false
-python-versions = ">=3.9"
-files = [
- {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
- {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
- {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"},
- {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"},
- {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"},
- {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"},
- {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"},
- {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"},
- {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"},
- {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"},
- {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"},
- {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"},
- {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"},
- {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"},
- {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"},
- {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"},
- {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"},
- {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"},
- {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"},
- {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"},
- {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"},
- {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"},
- {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"},
- {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"},
- {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"},
- {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"},
- {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"},
- {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"},
- {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"},
- {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"},
-]
-
-[package.extras]
-watchmedo = ["PyYAML (>=3.10)"]
-
-[metadata]
-lock-version = "2.0"
-python-versions = "^3.11"
-content-hash = "f58f04f170bd72f913d33992224112b9fc24a3bd06cd2dc2120e99d29e043955"
+# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand.
+
+[[package]]
+name = "bump2version"
+version = "1.0.1"
+description = "Version-bump your software with a single command!"
+optional = false
+python-versions = ">=3.5"
+groups = ["dev"]
+files = [
+ {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"},
+ {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"},
+]
+
+[[package]]
+name = "cfgv"
+version = "3.5.0"
+description = "Validate configuration and produce human readable error messages."
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"},
+ {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"},
+]
+
+[[package]]
+name = "click"
+version = "8.3.3"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"},
+ {file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["dev"]
+markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.13.5"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"},
+ {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"},
+ {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"},
+ {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"},
+ {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"},
+ {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"},
+ {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"},
+ {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"},
+ {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"},
+ {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"},
+ {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"},
+ {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"},
+ {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"},
+ {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"},
+ {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"},
+ {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"},
+ {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"},
+ {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"},
+ {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"},
+ {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"},
+ {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"},
+ {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"},
+ {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"},
+ {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"},
+ {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"},
+ {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"},
+ {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"},
+ {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"},
+ {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"},
+ {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"},
+ {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"},
+ {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"},
+ {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"},
+ {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"},
+ {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"},
+ {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"},
+ {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"},
+ {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"},
+ {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"},
+ {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"},
+ {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"},
+ {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"},
+ {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"},
+ {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"},
+ {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"},
+ {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"},
+ {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"},
+ {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"},
+ {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"},
+ {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"},
+ {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"},
+ {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"},
+ {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"},
+ {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"},
+ {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"},
+ {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"},
+ {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"},
+ {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"},
+ {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"},
+ {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"},
+ {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"},
+ {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"},
+ {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"},
+ {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"},
+ {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"},
+ {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"},
+ {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"},
+ {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"},
+ {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"},
+ {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"},
+ {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"},
+ {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"},
+ {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"},
+ {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"},
+ {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"},
+ {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"},
+ {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"},
+ {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"},
+ {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"},
+ {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"},
+ {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"},
+ {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"},
+ {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"},
+ {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"},
+ {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"},
+ {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"},
+ {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"},
+ {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"},
+ {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"},
+ {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"},
+ {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"},
+ {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"},
+ {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"},
+ {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"},
+ {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"},
+ {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"},
+ {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"},
+ {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"},
+ {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"},
+ {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"},
+ {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"},
+ {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"},
+ {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"},
+ {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"},
+ {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"},
+ {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"},
+]
+
+[package.extras]
+toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
+
+[[package]]
+name = "distlib"
+version = "0.4.0"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+groups = ["dev"]
+files = [
+ {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"},
+ {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"},
+]
+
+[[package]]
+name = "execnet"
+version = "2.1.2"
+description = "execnet: rapid multi-Python deployment"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"},
+ {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"},
+]
+
+[package.extras]
+testing = ["hatch", "pre-commit", "pytest", "tox"]
+
+[[package]]
+name = "filelock"
+version = "3.29.0"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258"},
+ {file = "filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90"},
+]
+
+[[package]]
+name = "ghp-import"
+version = "2.1.0"
+description = "Copy your docs directly to the gh-pages branch."
+optional = false
+python-versions = "*"
+groups = ["dev"]
+files = [
+ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
+ {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
+]
+
+[package.dependencies]
+python-dateutil = ">=2.8.1"
+
+[package.extras]
+dev = ["flake8", "markdown", "twine", "wheel"]
+
+[[package]]
+name = "identify"
+version = "2.6.19"
+description = "File identification library for Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a"},
+ {file = "identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
+ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
+ {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "librt"
+version = "0.10.0"
+description = "Mypyc runtime library"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+markers = "platform_python_implementation != \"PyPy\""
+files = [
+ {file = "librt-0.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dc99f9642100b86e5f6bb14cdc9970009e31a9ef7d64df6704b7018451524a3"},
+ {file = "librt-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8298cedfcfaff3790000bd057aaaa3df1b0ab54cf7b48eeab16184cbb1bc66b9"},
+ {file = "librt-0.10.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7dbe312dbf76468255b79a7ba311236fde620f2f7055fc09d421e31340314e"},
+ {file = "librt-0.10.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:56ed90c48c19249012dadfd79a1bc13bd5168ea60a70722d330a3a600c0b1852"},
+ {file = "librt-0.10.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d74ca0f4b2b09c117f913d4df01f6b934dff8a271096b35167d5264a31649f0"},
+ {file = "librt-0.10.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8eb2daa9375f93c0e55ff5e44a4bbe98f39e5fe52e1abf9c97acb67743b61bf8"},
+ {file = "librt-0.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7b09b90e634e6dff57978cd358070046071e2b120501f10787aeb35425f504f6"},
+ {file = "librt-0.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2cf22fd379d60c739b800d4295ed34045f8b04aa8df9c12bd2f8f43f7fe672b7"},
+ {file = "librt-0.10.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:74c798793fcf29a84d442278ebe0bb1fff79fe58ac4106eeff7019cbba861423"},
+ {file = "librt-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc4f1573401e8dbe6c26511fe027620b0fb30ae9a7ab814e02e510626b8b5f9c"},
+ {file = "librt-0.10.0-cp310-cp310-win32.whl", hash = "sha256:e1428275f5fe3d4db6822e58d8b005a5b28ffca55e8433ebc051247fbe46429f"},
+ {file = "librt-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:0708e9408f585b0f065081680583a577652099680ccf820c7538904322b679c3"},
+ {file = "librt-0.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01b4500ca3a625450c032a9142a8e843923ce263fa8a92ad1b38927cabe2fe72"},
+ {file = "librt-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b7e42d1b3e300d20bfc87e72ffd62f0a92a2cb3c35f7bf90df90c9d2a49f74c"},
+ {file = "librt-0.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8ef7b8c61ce3a1b597cd3e15348ff1574325165c2e7ce09a718154cde2a7950"},
+ {file = "librt-0.10.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e73c84f72d1fa0d6eaa7a1930b436ba8d2c90c58d77bfabb09995a69ad35f6c0"},
+ {file = "librt-0.10.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9728cb98713bd862fb8f4fd6a642d1896c86058a41d77c70f3d5cee75e725275"},
+ {file = "librt-0.10.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:648b7e941d20acd72f9652115e0e53facd98156d61f9ebf7a812bdef8bdccea9"},
+ {file = "librt-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c3e33747c068e86a9007c20fdb777eb5ba8d3d19136d7812f88e69a713041b6f"},
+ {file = "librt-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d509c745bf7e77d1107cf05e6abb249dc03fad13eb39f2286a49deedaeb2bcd7"},
+ {file = "librt-0.10.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:786ad5a15e99d0e0e74f3adbeecc198a5ac58f340be07e984723d1e0074838de"},
+ {file = "librt-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:075582d877a97ee3d8e77bda3689dbe617b14f6469224a2d80b4b6c38e3951aa"},
+ {file = "librt-0.10.0-cp311-cp311-win32.whl", hash = "sha256:75ecdc3f5a90065aa2af2e574706c5495adc392520762dcf10b1aa716f0b8090"},
+ {file = "librt-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:b6f6084884131d8a52cb9d7095ff2aa52c1e786d9fdaefab1fb4515415e9e083"},
+ {file = "librt-0.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:0140bd62151160047e89b2730cb6f8506cdac5127baa1afb9231e4dd3fe7f681"},
+ {file = "librt-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4b58a44b407e91f633dafee008de9ddea6aa2a555ed94929c099260910bd0ba"},
+ {file = "librt-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:950b79b11762531bdf45a9df909d2f9a2a8445c70c88665c01d14c8511a27dc5"},
+ {file = "librt-0.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4538453f51be197633b425912c150e25b0667252d3741c53e8368176d98d9d37"},
+ {file = "librt-0.10.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:70b955f091beac93e994a0b7ec616934f63b3ea5c3d6d7af847562f935aceca7"},
+ {file = "librt-0.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:483e685e06b6163728ba6c85d74315176be7190f432ec2a41226e5e14355d5f0"},
+ {file = "librt-0.10.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ac53d946a009d1a38c44a60812708c9458fb2a239a5f630d8e625571386650f"},
+ {file = "librt-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc8771c9fcf0ea894ca41fdc2abd83572c2fbda221f232d86e718614e57ff513"},
+ {file = "librt-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:70805dbc5257892ac572f86290a61e3c8d90224ecce1a8b2d1f7ed51965417f4"},
+ {file = "librt-0.10.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d3b4f300f7bcba6e2ff73fb8bef1898479e9772bfa2682998c636391633ec826"},
+ {file = "librt-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:943bc943f92f4fb3408fae62485c6a3ad68ce4f2ee205643a39641525c19a276"},
+ {file = "librt-0.10.0-cp312-cp312-win32.whl", hash = "sha256:6065c1a758fba1010b41401013903d3d5d2750eab425ddedd584abac31d0630e"},
+ {file = "librt-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:d788ecbe208ab352dab0e105cc06057bf9a2fc7e58cabb0d751ad9e30062b9e2"},
+ {file = "librt-0.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:6003d1f295bdba02656dc81308208fc060d0a51d8c0d0a6db70f7f3c57b9ba0a"},
+ {file = "librt-0.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0ede79d682e73f91c1b599a76d78b7464b9b5d213754cedb13372d9df36e596"},
+ {file = "librt-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0ba0b131fdb336c8b9c948e397f4a7e649d0f783b529f07b647bf4961df392e"},
+ {file = "librt-0.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2728117da2afb96fb957768725ee43dc9a2d73b031e02da424b818a3cdd3a275"},
+ {file = "librt-0.10.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:723ba80594c49cdf0584196fc430752262605dc9449902fc9bd3d9b79976cb77"},
+ {file = "librt-0.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7292edaaca294a61a978c53a3c7d6130d099b0dfbc8f0a65916cdc6b891b9852"},
+ {file = "librt-0.10.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89fe9d539f2c10a1666633eeeac507ce95dd06d9ecc58de3c6390dba156a3d3a"},
+ {file = "librt-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4efa7b9587503fa5b67f40593302b9c8836d211d222ff9f7cafe67be5f8f0b10"},
+ {file = "librt-0.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22dc982ef59df0136df36092ccbdbb570ced8aafb33e49585739b2f1de1c13b6"},
+ {file = "librt-0.10.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6f2e5f3606253a84cea719c94a3bb1c54487b5d617d0254d46e0920d8a06be3f"},
+ {file = "librt-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40884bfaa1e29f6b6a9be255007d8f359bfc9e61d68bdef8ed3158bfcbc95df9"},
+ {file = "librt-0.10.0-cp313-cp313-win32.whl", hash = "sha256:3cd34cd8254eba756660bff6c2da91278248184301054fe3e4feb073bdd49b14"},
+ {file = "librt-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:7baac5313e2d8dce1386f97777a8d03ab28f5fe1e780b3b9ac2ee7544551fedc"},
+ {file = "librt-0.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:afc5b4406c8e2515698d922a5c7823a009312835ea58196671fff40e35cb8166"},
+ {file = "librt-0.10.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f09588a30e6a22ec624090d72a3ab1a6d4d5485c3ed739603e76aa3c16efa688"},
+ {file = "librt-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:131ade118d12bd7a0adc4e655474a553f1b76cf78385868885944d21d51e45e0"},
+ {file = "librt-0.10.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8b9ab28e40d011c373a189eae900c916e66d6fbecf7983e9e4883089ee085ef"},
+ {file = "librt-0.10.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:67c39bb30da73bae1f293d1ed8bc2f8f6642649dd0928d3600aeff3041ac23d6"},
+ {file = "librt-0.10.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c3273c6b774614f093c8927c2bf1b077d0fefde988fe98f46a333734e5597ab"},
+ {file = "librt-0.10.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9dd7c1b86a4baa583ab5db977484b93a2c474e69e96ef3e9538387ea54229cb9"},
+ {file = "librt-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a77385c5a202e831149f7ad03be9e67cf80e957e52c614e83dcb822c95222eb8"},
+ {file = "librt-0.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6a5eafa74b5655bad59886138ed68426f098a6beb8cb95a71f2cc3cd8bb33fe"},
+ {file = "librt-0.10.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1fc93d0439204c50ab4d1512611ce2c206f1b369b419f69c7c27c761561e3291"},
+ {file = "librt-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:79e713c178bc7a744adfbee6b4619a288eecc0c914da2a9313a20255abe2f0cf"},
+ {file = "librt-0.10.0-cp314-cp314-win32.whl", hash = "sha256:2eba9d955a68c41d9f326be3da42f163ec3518b7ab20f1c826224e7bed71e0bf"},
+ {file = "librt-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbfaf7f5145e9917f5d18bffa298eff6a19d74e7b8b11dabdca95785befe8dbf"},
+ {file = "librt-0.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:8d6d385d1969849a6b1397114df22714b6ded917bada98668e3e974dc663477e"},
+ {file = "librt-0.10.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:6c3a82d3bd32631ef5c79922dfc028520c9ad840255979ab4d908271818039ee"},
+ {file = "librt-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d64cc66005dc324c9bb1fa3fc2841f529002f6eb15966d55e46d430f56955a6a"},
+ {file = "librt-0.10.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb562cd28c88cd2c6a9a6c78f99dc39348d6b16c94adc25de0e574acf1176e9"},
+ {file = "librt-0.10.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b809aa2854d019c28773b03605df22adc675ee4f3f4402d673581313e8906119"},
+ {file = "librt-0.10.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc15acabdd519bd4176fdadc2119e5e3093485d86f89138daf47e5b4cedb983a"},
+ {file = "librt-0.10.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b1b2d835307d08ddadd94568e2369648ec9173bd3eea6d7f52a1abe717c81f98"},
+ {file = "librt-0.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d261c6a2f93335a5167887fb0223e8b98ffce20ee3fde242e8e58a37ece6d0e5"},
+ {file = "librt-0.10.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e2ffd44963f8e7f68995504d90f9881d64e94dc1d8e310039b9526108fc0c0f7"},
+ {file = "librt-0.10.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f285f6455ed495791c4d8630e5af732960adea93cac4c893d15619f2eae53e8"},
+ {file = "librt-0.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6034ff52e663d34c7b82ef2aa2f94ad7c1d939e2368e63b06844bc4d127d2e1"},
+ {file = "librt-0.10.0-cp314-cp314t-win32.whl", hash = "sha256:657860fd877fba6a241ea088ef99f63ca819945d3c715265da670bad56c37ebe"},
+ {file = "librt-0.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56ded2d66010203a0cb5af063b609e3f079531a0e5e576d618dece859fd2e1af"},
+ {file = "librt-0.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1ee63f30abf18ed4830fdbaf87b2b6f4bba1e198d46085c314edde4045e56715"},
+ {file = "librt-0.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83628c28545a5f4d860b48fae7f62367c006ab7405898573f34af8b7dcb178a2"},
+ {file = "librt-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bcf57b4de07e2d4bd093636ee59dc1b64298f304148dd9c4f001f7c7897650d"},
+ {file = "librt-0.10.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2236c16bdb7c527eb671e4b599eec2c4229fddf80573de2bde529924f46db971"},
+ {file = "librt-0.10.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:c1efa2f494811b245427095225a4d0251aee33ba4cf6ba2b7a6a9a619bc1a2ff"},
+ {file = "librt-0.10.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d14626d350af79eed4b4f8886530052e3f78a62e9e53d2699f726f99c3d1d122"},
+ {file = "librt-0.10.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b609f3461beae5608ca5219131ae5cdfea2e369818030abfc6ba7086830cde42"},
+ {file = "librt-0.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0e2338b67c8e72755ccd1ab77b027e3701b375a1e12b4576fdefdf9c46448274"},
+ {file = "librt-0.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:17cadff57139ff49beea0b17e50b28dfc3f9687126399696de4d2d8ae86ba7ff"},
+ {file = "librt-0.10.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:5496102c8ed065c128d0f0fd10dcb3f9f3fd9b346954462d62af623f1b1ec7cd"},
+ {file = "librt-0.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:537e1bfa459c1c92a263768a8a0c6fd0558049fa6c1b866d791eea711ae64114"},
+ {file = "librt-0.10.0-cp39-cp39-win32.whl", hash = "sha256:85aca5a7ddc5f2d4cba24eba35667d83893ff2980dbd5884be16f538a24351e4"},
+ {file = "librt-0.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:e45e46ff5fdfc690e77bb8557d5ba56974c4006b744ddbd70cce99fec6bfbeb8"},
+ {file = "librt-0.10.0.tar.gz", hash = "sha256:1aba1e8aa4e3307a7be68a74149545fde7451964dc0235a8bec5704a17bdda42"},
+]
+
+[[package]]
+name = "markdown"
+version = "3.10.2"
+description = "Python implementation of John Gruber's Markdown."
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"},
+ {file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"},
+]
+
+[package.extras]
+docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"]
+testing = ["coverage", "pyyaml"]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
+ {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"},
+ {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"},
+ {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"},
+ {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"},
+ {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"},
+ {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"},
+ {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"},
+ {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"},
+ {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"},
+ {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"},
+ {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"},
+ {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
+]
+
+[[package]]
+name = "mergedeep"
+version = "1.3.4"
+description = "A deep merge function for 🐍."
+optional = false
+python-versions = ">=3.6"
+groups = ["dev"]
+files = [
+ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
+ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
+]
+
+[[package]]
+name = "mkdocs"
+version = "1.6.1"
+description = "Project documentation with Markdown."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"},
+ {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"},
+]
+
+[package.dependencies]
+click = ">=7.0"
+colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
+ghp-import = ">=1.0"
+jinja2 = ">=2.11.1"
+markdown = ">=3.3.6"
+markupsafe = ">=2.0.1"
+mergedeep = ">=1.3.4"
+mkdocs-get-deps = ">=0.2.0"
+packaging = ">=20.5"
+pathspec = ">=0.11.1"
+pyyaml = ">=5.1"
+pyyaml-env-tag = ">=0.1"
+watchdog = ">=2.0"
+
+[package.extras]
+i18n = ["babel (>=2.9.0)"]
+min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"]
+
+[[package]]
+name = "mkdocs-get-deps"
+version = "0.2.2"
+description = "An extra command for MkDocs that infers required PyPI packages from `plugins` in mkdocs.yml"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650"},
+ {file = "mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1"},
+]
+
+[package.dependencies]
+mergedeep = ">=1.3.4"
+platformdirs = ">=2.2.0"
+pyyaml = ">=5.1"
+
+[[package]]
+name = "mypy"
+version = "1.20.2"
+description = "Optional static typing for Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4"},
+ {file = "mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997"},
+ {file = "mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14"},
+ {file = "mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99"},
+ {file = "mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c"},
+ {file = "mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd"},
+ {file = "mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2"},
+ {file = "mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c"},
+ {file = "mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3"},
+ {file = "mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254"},
+ {file = "mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98"},
+ {file = "mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac"},
+ {file = "mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67"},
+ {file = "mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100"},
+ {file = "mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b"},
+ {file = "mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4"},
+ {file = "mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6"},
+ {file = "mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066"},
+ {file = "mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102"},
+ {file = "mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9"},
+ {file = "mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58"},
+ {file = "mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026"},
+ {file = "mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943"},
+ {file = "mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517"},
+ {file = "mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15"},
+ {file = "mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee"},
+ {file = "mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f"},
+ {file = "mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330"},
+ {file = "mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30"},
+ {file = "mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924"},
+ {file = "mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb"},
+ {file = "mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc"},
+ {file = "mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558"},
+ {file = "mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8"},
+ {file = "mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3"},
+ {file = "mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609"},
+ {file = "mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2"},
+ {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c"},
+ {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744"},
+ {file = "mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6"},
+ {file = "mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec"},
+ {file = "mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382"},
+ {file = "mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563"},
+ {file = "mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665"},
+]
+
+[package.dependencies]
+librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""}
+mypy_extensions = ">=1.0.0"
+pathspec = ">=1.0.0"
+typing_extensions = [
+ {version = ">=4.6.0", markers = "python_version < \"3.15\""},
+ {version = ">=4.14.0", markers = "python_version >= \"3.15\""},
+]
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+faster-cache = ["orjson"]
+install-types = ["pip"]
+mypyc = ["setuptools (>=50)"]
+native-parser = ["ast-serialize (>=0.1.1,<1.0.0)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
+ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.10.0"
+description = "Node.js virtual environment builder"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["dev"]
+files = [
+ {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"},
+ {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"},
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
+ {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
+]
+
+[[package]]
+name = "pastel"
+version = "0.2.1"
+description = "Bring colors to your terminal."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+groups = ["dev"]
+files = [
+ {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},
+ {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"},
+]
+
+[[package]]
+name = "pathspec"
+version = "1.1.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"},
+ {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"},
+]
+
+[package.extras]
+hyperscan = ["hyperscan (>=0.7)"]
+optional = ["typing-extensions (>=4)"]
+re2 = ["google-re2 (>=1.1)"]
+
+[[package]]
+name = "platformdirs"
+version = "4.9.6"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"},
+ {file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"},
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
+ {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["coverage", "pytest", "pytest-benchmark"]
+
+[[package]]
+name = "poethepoet"
+version = "0.45.0"
+description = "A task runner that works well with poetry and uv."
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "poethepoet-0.45.0-py3-none-any.whl", hash = "sha256:8e25f6e834ecf25fe2ddca676a4e0207eeb2e19def0a8709fc5c7f18e86cd68c"},
+ {file = "poethepoet-0.45.0.tar.gz", hash = "sha256:cf2c2df4c185d33b619c2771de2b28c2b81c733f072226030c7fa4c273c040f8"},
+]
+
+[package.dependencies]
+pastel = ">=0.2.1,<0.3.0"
+pyyaml = ">=6.0.3,<7.0"
+
+[package.extras]
+poetry-plugin = ["poetry (>=1.2.0,<3.0.0) ; python_version < \"4.0\""]
+
+[[package]]
+name = "pre-commit"
+version = "4.6.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b"},
+ {file = "pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
+ {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pytest"
+version = "9.0.3"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
+ {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
+iniconfig = ">=1.0.1"
+packaging = ">=22"
+pluggy = ">=1.5,<2"
+pygments = ">=2.7.2"
+
+[package.extras]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "7.1.0"
+description = "Pytest plugin for measuring coverage."
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"},
+ {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"},
+]
+
+[package.dependencies]
+coverage = {version = ">=7.10.6", extras = ["toml"]}
+pluggy = ">=1.2"
+pytest = ">=7"
+
+[package.extras]
+testing = ["process-tests", "pytest-xdist", "virtualenv"]
+
+[[package]]
+name = "pytest-xdist"
+version = "3.8.0"
+description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"},
+ {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"},
+]
+
+[package.dependencies]
+execnet = ">=2.1"
+pytest = ">=7.0.0"
+
+[package.extras]
+psutil = ["psutil (>=3.0)"]
+setproctitle = ["setproctitle"]
+testing = ["filelock"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["dev"]
+files = [
+ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+ {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "python-discovery"
+version = "1.3.0"
+description = "Python interpreter discovery"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f"},
+ {file = "python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0"},
+]
+
+[package.dependencies]
+filelock = ">=3.15.4"
+platformdirs = ">=4.3.6,<5"
+
+[package.extras]
+docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"},
+ {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
+ {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"},
+ {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"},
+ {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"},
+ {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"},
+ {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"},
+ {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
+ {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
+ {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
+ {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
+ {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
+ {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
+ {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
+ {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
+ {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
+ {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
+ {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
+ {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
+ {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
+ {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
+ {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
+ {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
+ {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
+ {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
+ {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
+ {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
+ {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
+ {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
+ {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
+ {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
+ {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
+ {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
+ {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
+ {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
+ {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"},
+ {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"},
+ {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"},
+ {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"},
+ {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"},
+ {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"},
+ {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"},
+ {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"},
+ {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"},
+ {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"},
+ {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"},
+ {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"},
+ {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"},
+ {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"},
+ {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"},
+ {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"},
+ {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"},
+ {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"},
+ {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"},
+ {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"},
+ {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"},
+ {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"},
+ {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"},
+ {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"},
+ {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"},
+ {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"},
+ {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"},
+ {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"},
+ {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"},
+ {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"},
+ {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"},
+ {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"},
+ {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"},
+ {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"},
+ {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"},
+ {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"},
+ {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"},
+ {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
+]
+
+[[package]]
+name = "pyyaml-env-tag"
+version = "1.1"
+description = "A custom YAML tag for referencing environment variables in YAML files."
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"},
+ {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"},
+]
+
+[package.dependencies]
+pyyaml = "*"
+
+[[package]]
+name = "ruff"
+version = "0.15.12"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+ {file = "ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c"},
+ {file = "ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c"},
+ {file = "ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5"},
+ {file = "ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002"},
+ {file = "ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5"},
+ {file = "ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6"},
+ {file = "ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33"},
+ {file = "ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847"},
+ {file = "ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0"},
+ {file = "ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339"},
+ {file = "ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5"},
+ {file = "ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd"},
+ {file = "ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b"},
+ {file = "ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e"},
+ {file = "ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20"},
+ {file = "ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d"},
+ {file = "ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f"},
+ {file = "ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6"},
+]
+
+[[package]]
+name = "setuptools"
+version = "83.0.0"
+description = "Most extensible Python build backend with support for C/C++ extension modules"
+optional = false
+python-versions = ">=3.10"
+groups = ["dev"]
+files = [
+ {file = "setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"},
+ {file = "setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef"},
+]
+
+[package.extras]
+check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""]
+core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
+cover = ["pytest-cov"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
+enabler = ["pytest-enabler (>=3.4)"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
+type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["dev"]
+files = [
+ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
+ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+description = "Backported and Experimental Type Hints for Python 3.9+"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
+ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
+]
+
+[[package]]
+name = "virtualenv"
+version = "21.3.1"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35"},
+ {file = "virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""}
+platformdirs = ">=3.9.1,<5"
+python-discovery = ">=1.2.2"
+
+[[package]]
+name = "watchdog"
+version = "6.0.0"
+description = "Filesystem events monitoring"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
+ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
+ {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"},
+ {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"},
+ {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"},
+ {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"},
+ {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"},
+ {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"},
+ {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"},
+ {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"},
+ {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"},
+ {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"},
+ {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"},
+ {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"},
+ {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"},
+ {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"},
+ {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"},
+ {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"},
+ {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"},
+ {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"},
+ {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"},
+ {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"},
+ {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"},
+]
+
+[package.extras]
+watchmedo = ["PyYAML (>=3.10)"]
+
+[metadata]
+lock-version = "2.1"
+python-versions = ">=3.12"
+content-hash = "0dadb3244ff0bd21e830b22407be7ee5a72b697363ed7ff102321e826971a64a"
diff --git a/pyproject.toml b/pyproject.toml
index 2a931a4e4..076e133f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,166 +1,190 @@
-[tool.poetry]
-name = "zxbasic"
-version = "1.18.2"
-description = "Boriel's ZX BASIC Compiler"
-authors = ["Jose Rodriguez "]
-license = "AGPL-3.0-or-later"
-documentation = "https://zxbasic.readthedocs.io"
-repository = "https://github.com/boriel-basic/zxbasic"
-homepage = "https://github.com/boriel-basic"
-readme = "README.md"
-
-classifiers = [
- 'Development Status :: 5 - Production/Stable',
- 'Intended Audience :: Developers',
- 'Topic :: Software Development :: Build Tools',
- 'License :: OSI Approved :: GNU Affero General Public License v3',
- 'Programming Language :: Python :: 3.11',
- ]
-
-keywords = ['compiler', 'zxspectrum', 'BASIC', 'z80'] # arbitrary keywords
-
-packages = [
- { include = "src/**/*" }
-]
-
-[tool.poetry.scripts]
-zxbc = 'src.zxbc:main'
-zxbasm = 'src.zxbasm:main'
-zxbpp = 'src.zxbpp:entry_point'
-
-[tool.poetry.dependencies]
-python = "^3.11"
-
-[tool.poetry.group.dev.dependencies]
-pytest = "*"
-bump2version = "^1.0.0"
-pre-commit = "*"
-mkdocs = "^1.2.2"
-poethepoet = "*"
-pytest-cov = "*"
-ruff = "*"
-mypy = "^1.8.0"
-pytest-xdist = "*"
-setuptools = ">=70.1.1,<79.0.0"
-
-[build-system]
-requires = ["poetry>=0.12"]
-build-backend = "poetry.masonry.api"
-
-[tool.poe.tasks]
-[[tool.poe.tasks.lint]]
-help = "Check code style and typing..."
-shell = """
- ruff check . &&
- ruff format --check . &&
- mypy .
- """
-
-[[tool.poe.tasks.test]]
-help = "Run tests"
-shell = "pytest tests"
-
-[[tool.poe.tasks.test-no-cov]]
-help = "Run tests faster (with no coverage)"
-shell = "pytest tests --no-cov"
-
-[[tool.poe.tasks.format]]
-help = "Formats code"
-shell = """
- ruff check --select I --fix .
- ruff format .
- """
-
-[tool.coverage.run]
-branch = true
-omit = [
- "tests/*",
- "src/ply/*"
-]
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-norecursedirs = ["test_*tmp", "runtime"]
-addopts = "--cov=src -n auto --dist=loadgroup --color=yes"
-
-[tool.mypy]
-check_untyped_defs = true
-disable_error_code = [
- "arg-type",
- "assignment",
- "attr-defined",
- "call-overload",
- "import-not-found",
- "list-item",
- "misc",
- "name-defined",
- "operator",
- "union-attr",
- "var-annotated",
-]
-exclude = [
- 'src/ply/.*\.py$'
-]
-
-[[tool.mypy.overrides]]
-module = [
- "src.ply.*",
-]
-follow_imports = "skip"
-
-[tool.ruff]
-line-length = 120
-target-version = "py311"
-exclude = [
- "src/ply" # PLY, external 3rd party tool
-]
-
-[tool.ruff.lint]
-select = [
- "C4",
- "E",
- "EXE",
- "F",
- "I",
- "PLR",
- "PLW",
- "RET",
- "RUF",
- "UP",
-]
-ignore = [
- "E713",
- "E722",
- "E731",
- "E741",
- "EXE002",
- "PLR0124",
- "PLR0911",
- "PLR0912",
- "PLR0913",
- "PLR0915",
- "PLW1641",
- "PLR1714",
- "PLR1730",
- "PLR2004",
- "PLR5501",
- "PLW0603",
- "PLW2901",
- "PLW0602",
- "RET501",
- "RET503",
- "RET504",
- "RET507",
- "RUF005",
- "RUF012",
- "RUF013",
- "RUF021",
- "RUF022", # isort sorting style is not alphabetical and seems not natural either (despite the doc)
- "UP008",
- "UP015", # KEEP: redundant-open-modes
- "UP024",
- "UP028",
- "UP030",
- "UP031",
- "UP038",
-]
+[project]
+name = "zxbasic"
+version = "1.18.7"
+description = "Boriel's ZX BASIC Compiler"
+authors = [
+ { name = "Jose Rodriguez", email = "zxbasic@boriel.com" }
+]
+license = "AGPL-3.0-or-later"
+license-files = [
+ "LICENSE.txt"
+]
+readme = "README.md"
+requires-python = ">=3.12"
+keywords = ["compiler", "zxspectrum", "BASIC", "z80"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Topic :: Software Development :: Build Tools",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+
+[project.urls]
+Homepage = "https://github.com/boriel-basic"
+Repository = "https://github.com/boriel-basic/zxbasic"
+Documentation = "https://zxbasic.readthedocs.io"
+
+[project.scripts]
+zxbc = "src.zxbc:main"
+zxbasm = "src.zxbasm:main"
+zxbpp = "src.zxbpp:entry_point"
+
+# Poetry-specific packaging configuration that is not part of PEP 621.
+# Keep this minimal section to preserve the current package discovery behavior.
+[tool.poetry]
+packages = [
+ { include = "src/**/*" }
+]
+
+[tool.poetry.group.dev.dependencies]
+pytest = "*"
+bump2version = "^1.0.0"
+pre-commit = "*"
+mkdocs = "^1.2.2"
+poethepoet = "*"
+pytest-cov = "*"
+ruff = "*"
+mypy = "^1.8.0"
+pytest-xdist = "*"
+setuptools = ">=83.0.0,<84.0.0"
+
+[build-system]
+requires = ["poetry-core>=2.4.1"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poe.tasks]
+[[tool.poe.tasks.lint]]
+help = "Check code style and typing..."
+shell = """
+ ruff check . &&
+ ruff format --check . &&
+ mypy .
+ """
+
+[[tool.poe.tasks.test]]
+help = "Run tests"
+shell = "pytest tests"
+
+[[tool.poe.tasks.test-no-cov]]
+help = "Run tests faster (with no coverage)"
+shell = "pytest tests --no-cov"
+
+[[tool.poe.tasks.format]]
+help = "Formats code"
+shell = """
+ ruff check --fix
+ ruff check --select I --fix .
+ ruff format .
+ """
+
+[[tool.poe.tasks.clean]]
+help = "Clean up the project working tree (i.e. remove __pycache__ folders)"
+shell = """
+ find . -name "__pycache__" | xargs rm -rf
+ find . -name "*.pyc" -delete
+ rm -rf .mypy_cache .pytest_cache .ruff_cache dist/ .coverage.*
+ """
+
+[tool.coverage.run]
+branch = true
+omit = [
+ "tests/*",
+ "src/**/*_standalone.py",
+]
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+norecursedirs = ["test_*tmp", "runtime"]
+addopts = "--cov=src -n auto --dist=loadgroup --color=yes"
+
+[tool.mypy]
+check_untyped_defs = true
+disable_error_code = [
+ "arg-type",
+ "assignment",
+ "attr-defined",
+ "call-overload",
+ "import-not-found",
+ "list-item",
+ "misc",
+ "name-defined",
+ "operator",
+ "union-attr",
+ "var-annotated",
+]
+exclude = [
+ 'scratch/.*',
+ '.venv/.*',
+ 'venv/.*',
+ 'src/.*_standalone\.py$',
+]
+
+[[tool.mypy.overrides]]
+module = [
+ "src.zxbasm.asmparse_standalone",
+ "src.zxbasm.asmparse_zxnext_standalone",
+ "src.zxbc.zxbparser_standalone",
+ "src.zxbpp.zxbpp_standalone",
+]
+follow_imports = "skip"
+
+[tool.ruff]
+line-length = 120
+target-version = "py314"
+exclude = [
+ ".venv/",
+ "venv/",
+ "src/**/*standalone.py",
+]
+
+[tool.ruff.lint]
+select = [
+ "C4",
+ "E",
+ "EXE",
+ "F",
+ "I",
+ "PLR",
+ "PLW",
+ "RET",
+ "RUF",
+ "UP",
+]
+ignore = [
+ "E713",
+ "E722",
+ "E731",
+ "E741",
+ "EXE002",
+ "PLR0124",
+ "PLR0911",
+ "PLR0912",
+ "PLR0913",
+ "PLR0915",
+ "PLW1641",
+ "PLR1714",
+ "PLR1730",
+ "PLR2004",
+ "PLR5501",
+ "PLW0603",
+ "PLW2901",
+ "PLW0602",
+ "RET501",
+ "RET503",
+ "RET504",
+ "RET507",
+ "RUF005",
+ "RUF012",
+ "RUF013",
+ "RUF021",
+ "RUF022", # isort sorting style is not alphabetical and seems not natural either (despite the doc)
+ "UP008",
+ "UP015", # KEEP: redundant-open-modes
+ "UP024",
+ "UP028",
+ "UP030",
+ "UP031",
+]
diff --git a/src/__init__.py b/src/__init__.py
index e69de29bb..906667a8d 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -0,0 +1,6 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
diff --git a/src/api/__init__.py b/src/api/__init__.py
index e69de29bb..906667a8d 100644
--- a/src/api/__init__.py
+++ b/src/api/__init__.py
@@ -0,0 +1,6 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
diff --git a/src/api/check.py b/src/api/check.py
index 228cddb04..b52092332 100644
--- a/src/api/check.py
+++ b/src/api/check.py
@@ -1,12 +1,10 @@
-# vim:ts=4:sw=4:et:
-
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
-
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+from typing import Any
from src.api import config, errmsg, global_
from src.api.constants import CLASS, SCOPE
@@ -85,13 +83,13 @@ def check_is_callable(lineno: int, id_: str) -> bool:
def check_type_is_explicit(lineno: int, id_: str, type_):
- assert isinstance(type_, symbols.TYPE)
+ assert isinstance(type_, symbols.TYPEREF)
if type_.implicit:
if config.OPTIONS.strict:
errmsg.syntax_error_undeclared_type(lineno, id_)
-def check_call_arguments(lineno: int, id_: str, args, filename: str):
+def check_call_arguments(lineno: int, id_: str, args: symbols.ARGLIST, filename: str) -> bool:
"""Check arguments against function signature.
Checks every argument in a function call against a function.
@@ -131,8 +129,10 @@ def check_call_arguments(lineno: int, id_: str, args, filename: str):
for param in entry.ref.params:
if param.name in named_args:
continue
+
if param.default_value is None:
break
+
arg = symbols.ARGUMENT(param.default_value, lineno=lineno, byref=False, name=param.name)
symbols.ARGLIST.make_node(args, arg)
named_args[arg.name] = arg
@@ -154,13 +154,13 @@ def check_call_arguments(lineno: int, id_: str, args, filename: str):
if arg.class_ in (CLASS.var, CLASS.array) and param.class_ != arg.class_:
errmsg.error(lineno, f"Invalid argument '{arg.value}'", fname=arg.filename)
- return None
+ return False
if not arg.typecast(param.type_):
return False
if param.byref:
- if not isinstance(arg.value, (symbols.ID, symbols.ARRAYLOAD)):
+ if not isinstance(arg.value, symbols.ID | symbols.ARRAYLOAD):
errmsg.error(
lineno, "Expected a variable name, not an expression (parameter By Reference)", fname=arg.filename
)
@@ -255,26 +255,31 @@ def check_and_make_label(lbl: str | int | float, lineno):
# ----------------------------------------------------------------------
# Function for checking some arguments
# ----------------------------------------------------------------------
-def is_null(*symbols_):
+def is_null(*symbols_: Any) -> bool:
"""True if no nodes or all the given nodes are either
None, NOP or empty blocks. For blocks this applies recursively
"""
for sym in symbols_:
if sym is None:
continue
+
if not isinstance(sym, symbols.SYMBOL):
return False
+
if sym.token == "NOP":
continue
+
if sym.token == "BLOCK":
if not is_null(*sym.children):
return False
continue
+
return False
+
return True
-def is_SYMBOL(token: str, *symbols_: symbols.SYMBOL):
+def is_SYMBOL(token: str, *symbols_: symbols.SYMBOL) -> bool:
"""Returns True if ALL the given argument are AST nodes
of the given token (e.g. 'BINARY')
"""
@@ -282,41 +287,66 @@ def is_SYMBOL(token: str, *symbols_: symbols.SYMBOL):
return all(sym.token == token for sym in symbols_)
-def is_LABEL(*p):
+def is_LABEL(*p: symbols.SYMBOL) -> bool:
return is_SYMBOL("LABEL", *p)
-def is_string(*p):
+def is_string(*p: symbols.SYMBOL) -> bool:
"""Returns True if ALL the arguments are AST nodes
containing STRING or string CONSTANTS
"""
return all(is_SYMBOL("STRING", x) or is_const(x) and is_type(Type.string, x) for x in p)
-def is_const(*p):
+def is_const(*p: symbols.SYMBOL) -> bool:
"""A constant in the program, like CONST a = 5"""
return is_SYMBOL("CONST", *p)
-def is_CONST(*p):
+def is_CONST(*p: symbols.SYMBOL) -> bool:
"""Not to be confused with the above.
Check it's a CONSTant EXPRession
"""
return is_SYMBOL("CONSTEXPR", *p)
-def is_static(*p):
+def _is_static_unary(x: symbols.SYMBOL) -> bool:
+ if x.token != "UNARY":
+ return False
+
+ if x.operator != "ADDRESS":
+ return False
+
+ if x.operand.token == "LABEL":
+ return True
+
+ if x.operand.token in ("ID", "VAR"):
+ return x.operand.scope == SCOPE.global_
+
+ if x.operand.token == "ARRAYACCESS":
+ return is_static(*(arg.value for arg in x.operand.args))
+
+ return False
+
+
+def is_static(*p: symbols.SYMBOL) -> bool:
"""A static value (does not change at runtime)
which is known at compile time
"""
- return all(is_CONST(x) or is_number(x) or is_const(x) for x in p)
+ assert all(isinstance(x, symbols.SYMBOL) for x in p)
+ return all(is_CONST(x) or is_number(x) or is_const(x) or _is_static_unary(x) for x in p)
def is_number(*p):
"""Returns True if ALL the arguments are AST nodes
containing NUMBER or numeric CONSTANTS
"""
- return all(isinstance(i, Symbol) and i.token in ("NUMBER", "CONST") and Type.is_numeric(i.type_) for i in p)
+ return all(
+ isinstance(i, Symbol)
+ and i.token in ("NUMBER", "CONST")
+ and Type.is_numeric(i.type_.type_ if isinstance(i.type_, symbols.TYPEREF) else i.type_)
+ for i in p
+ )
def is_static_str(*p):
@@ -333,8 +363,8 @@ def is_var(*p):
def is_unsigned(*p):
"""Returns false unless all types in p are unsigned"""
try:
- return all(i.type_.is_basic and Type.is_unsigned(i.type_) for i in p)
- except Exception:
+ return all(i.type_.final.is_basic and Type.is_unsigned(i.type_.final) for i in p)
+ except (Exception,):
pass
return False
@@ -344,7 +374,7 @@ def is_signed(*p):
"""Returns false unless all types in p are signed"""
try:
return all(i.type_.is_basic and Type.is_signed(i.type_) for i in p)
- except Exception:
+ except (Exception,):
pass
return False
@@ -353,8 +383,8 @@ def is_signed(*p):
def is_numeric(*p):
"""Returns false unless all elements in p are of numerical type"""
try:
- return all(i.type_.is_basic and Type.is_numeric(i.type_) for i in p)
- except Exception:
+ return all(i.type_.final.is_basic and Type.is_numeric(i.type_.final) for i in p)
+ except (Exception,):
pass
return False
@@ -364,7 +394,7 @@ def is_type(type_, *p):
"""True if all args have the same type"""
try:
return all(i.type_ == type_ for i in p)
- except Exception:
+ except (Exception,):
pass
return False
@@ -376,7 +406,7 @@ def is_dynamic(*p): # TODO: Explain this better
"""
try:
return not any(i.scope == SCOPE.global_ and i.is_basic and i.type_ != Type.string for i in p)
- except Exception:
+ except (Exception,):
pass
return False
@@ -387,11 +417,22 @@ def is_callable(*p):
return all(x.token == "FUNCTION" for x in p)
-def is_block_accessed(block):
- """Returns True if a block is "accessed". A block of code is accessed if
- it has a LABEL and it is used in a GOTO, GO SUB or @address access
- :param block: A block of code (AST node)
- :return: True / False depending if it has labels accessed or not
+def is_block_accessed(block: Symbol):
+ """
+ Checks if a code block or any of its nested children has been accessed.
+
+ This function evaluates whether a specific block has been accessed. If the
+ block is not directly accessed, it recursively checks its child blocks.
+
+ Args:
+ block: The code block to be evaluated. The block is assumed to have
+ attributes `accessed` (a boolean indicating if the block has
+ been accessed) and `children` (an iterable of nested child
+ blocks).
+
+ Returns:
+ bool: True if the block or at least one of its nested child blocks has
+ been accessed; False otherwise.
"""
if is_LABEL(block) and block.accessed:
return True
@@ -404,17 +445,17 @@ def is_temporary_value(node) -> bool:
return node.token not in ("STRING", "VAR") and node.t[0] not in ("_", "#")
-def common_type(a: symbols.TYPE | Type | None, b: symbols.TYPE | Type | None) -> symbols.TYPE | Type | None:
+def common_type(a: symbols.TYPING, b: symbols.TYPING) -> symbols.TYPE | None:
"""Returns a type which is common for both a and b types.
Returns None if no common types allowed.
"""
- if a is None or b is None:
- return None
+ assert isinstance(a, symbols.TYPING)
+ assert isinstance(b, symbols.TYPING)
- if not isinstance(a, symbols.TYPE):
+ if isinstance(a, symbols.TYPEREF):
a = a.type_
- if not isinstance(b, symbols.TYPE):
+ if isinstance(b, symbols.TYPEREF):
b = b.type_
if a == b: # Both types are the same?
@@ -452,7 +493,7 @@ def common_type(a: symbols.TYPE | Type | None, b: symbols.TYPE | Type | None) ->
return result
-def is_ender(node) -> bool:
+def is_ender(node: symbols.SYMBOL) -> bool:
"""Returns whether this node ends a block, that is, the following instruction won't be
executed after this one
"""
@@ -471,7 +512,7 @@ def is_ender(node) -> bool:
}
-def check_class(node, class_: CLASS, lineno: int) -> bool:
+def check_class(node: symbols.ID, class_: CLASS, lineno: int) -> bool:
"""Returns whether the given node has CLASS.unknown or the given class_.
It False, it will emit a syntax error
"""
diff --git a/src/api/config.py b/src/api/config.py
index 66361b27d..7321fed46 100644
--- a/src/api/config.py
+++ b/src/api/config.py
@@ -1,11 +1,9 @@
-# vim:ts=4:et:sw=4:
-
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
import configparser
import enum
@@ -117,7 +115,7 @@ def load_config_from_file(
try:
cfg = configparser.ConfigParser()
cfg.read(filename, encoding="utf-8")
- except (configparser.DuplicateSectionError, configparser.DuplicateOptionError):
+ except configparser.DuplicateSectionError, configparser.DuplicateOptionError:
errmsg.msg_output(f"Invalid config file '{filename}': it has duplicated fields")
if stop_on_error:
sys.exit(1)
@@ -159,7 +157,7 @@ def save_config_into_file(
if os.path.exists(filename):
try:
cfg.read(filename, encoding="utf-8")
- except (configparser.DuplicateSectionError, configparser.DuplicateOptionError):
+ except configparser.DuplicateSectionError, configparser.DuplicateOptionError:
errmsg.msg_output(f"Invalid config file '{filename}': it has duplicated fields")
if stop_on_error:
sys.exit(1)
diff --git a/src/api/constants.py b/src/api/constants.py
index f5907072a..d02632c0b 100644
--- a/src/api/constants.py
+++ b/src/api/constants.py
@@ -1,27 +1,24 @@
-# vim: ts=4:et:sw=4:
-
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+from __future__ import annotations
import enum
import os
from enum import StrEnum
-from typing import Final, Optional, Union
+from typing import Final
-from .decorator import classproperty
+from .decorator import cached_function, classproperty
# -------------------------------------------------
# Global constants
# -------------------------------------------------
-# Path to main ZX Basic compiler executable
-ZXBASIC_ROOT = os.path.abspath(
- os.path.join(os.path.abspath(os.path.dirname(os.path.abspath(__file__))), os.path.pardir)
-)
+# Path to main Boriel Basic compiler executable
+ZXBASIC_ROOT = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir))
# ----------------------------------------------------------------------
@@ -43,18 +40,18 @@ class CLASS(StrEnum):
type = "type" # 7 # type
@classproperty
- def classes(cls):
+ def classes(cls) -> tuple[CLASS, ...]:
return cls.unknown, cls.var, cls.array, cls.function, cls.sub, cls.const, cls.label
@classmethod
- def is_valid(cls, class_: Union[str, "CLASS"]):
+ def is_valid(cls, class_: str | CLASS) -> bool:
"""Whether the given class is
valid or not.
"""
return class_ in set(CLASS)
@classmethod
- def to_string(cls, class_: "CLASS"):
+ def to_string(cls, class_: CLASS):
assert cls.is_valid(class_)
return class_.value
@@ -71,7 +68,7 @@ class ARRAY:
class TYPE(enum.IntEnum):
"""Enums primary type constants"""
- unknown = 0
+ unknown = 0 # Denotes a type that is not yet known
byte = 1
ubyte = 2
integer = 3
@@ -82,9 +79,10 @@ class TYPE(enum.IntEnum):
float = 8
string = 9
boolean = 10
+ nil = 11
@classmethod
- def type_size(cls, type_: "TYPE") -> int:
+ def type_size(cls, type_: TYPE) -> int:
type_sizes = {
cls.boolean: 1,
cls.byte: 1,
@@ -97,54 +95,55 @@ def type_size(cls, type_: "TYPE") -> int:
cls.float: 5,
cls.string: 2,
cls.unknown: 0,
+ cls.nil: 0,
}
return type_sizes[type_]
@classproperty
- def types(cls) -> set["TYPE"]:
+ def types(cls) -> set[TYPE]:
return set(TYPE)
@classmethod
- def size(cls, type_: "TYPE") -> int:
+ def size(cls, type_: TYPE) -> int:
return cls.type_size(type_)
@classproperty
- def integral(cls) -> set["TYPE"]:
+ def integral(cls) -> set[TYPE]:
return {cls.boolean, cls.byte, cls.ubyte, cls.integer, cls.uinteger, cls.long, cls.ulong}
@classproperty
- def signed(cls) -> set["TYPE"]:
+ def signed(cls) -> set[TYPE]:
return {cls.byte, cls.integer, cls.long, cls.fixed, cls.float}
@classproperty
- def unsigned(cls) -> set["TYPE"]:
+ def unsigned(cls) -> set[TYPE]:
return {cls.boolean, cls.ubyte, cls.uinteger, cls.ulong}
@classproperty
- def decimals(cls) -> set["TYPE"]:
+ def decimals(cls) -> set[TYPE]:
return {cls.fixed, cls.float}
@classproperty
- def numbers(cls) -> set["TYPE"]:
+ def numbers(cls) -> set[TYPE]:
return cls.integral | cls.decimals
@classmethod
- def is_valid(cls, type_: "TYPE") -> bool:
+ def is_valid(cls, type_: TYPE) -> bool:
"""Whether the given type is
valid or not.
"""
return type_ in cls.types
@classmethod
- def is_signed(cls, type_: "TYPE") -> bool:
+ def is_signed(cls, type_: TYPE) -> bool:
return type_ in cls.signed
@classmethod
- def is_unsigned(cls, type_: "TYPE") -> bool:
+ def is_unsigned(cls, type_: TYPE) -> bool:
return type_ in cls.unsigned
@classmethod
- def to_signed(cls, type_: "TYPE") -> "TYPE":
+ def to_signed(cls, type_: TYPE) -> TYPE:
"""Return signed type or equivalent"""
if type_ in cls.unsigned:
return {
@@ -160,12 +159,13 @@ def to_signed(cls, type_: "TYPE") -> "TYPE":
return cls.unknown
@staticmethod
- def to_string(type_: "TYPE") -> str:
+ def to_string(type_: TYPE) -> str:
"""Return ID representation (string) of a type"""
return type_.name
+ @cached_function
@staticmethod
- def to_type(typename: str) -> Optional["TYPE"]:
+ def to_type(typename: str) -> TYPE | None:
"""Converts a type ID to name. On error returns None"""
for t in TYPE:
if t.name == typename:
@@ -175,7 +175,7 @@ def to_type(typename: str) -> Optional["TYPE"]:
@enum.unique
-class SCOPE(str, enum.Enum):
+class SCOPE(StrEnum):
"""Enum scopes"""
global_ = "global"
@@ -183,33 +183,33 @@ class SCOPE(str, enum.Enum):
parameter = "parameter"
@staticmethod
- def is_valid(scope: Union[str, "SCOPE"]) -> bool:
+ def is_valid(scope: str | SCOPE) -> bool:
return scope in set(SCOPE)
@staticmethod
- def to_string(scope: "SCOPE") -> str:
+ def to_string(scope: SCOPE) -> str:
assert SCOPE.is_valid(scope)
return scope.value
@enum.unique
-class CONVENTION(str, enum.Enum):
+class CONVENTION(StrEnum):
unknown = "unknown"
fastcall = "__fastcall__"
stdcall = "__stdcall__"
@staticmethod
- def is_valid(convention: Union[str, "CONVENTION"]):
+ def is_valid(convention: str | CONVENTION):
return convention in set(CONVENTION)
@staticmethod
- def to_string(convention: "CONVENTION"):
+ def to_string(convention: CONVENTION):
assert CONVENTION.is_valid(convention)
return convention.value
@enum.unique
-class LoopType(str, enum.Enum):
+class LoopType(StrEnum):
DO = "DO"
FOR = "FOR"
WHILE = "WHILE"
diff --git a/src/api/dataref.py b/src/api/dataref.py
index 9dbd79ce1..942b3e755 100644
--- a/src/api/dataref.py
+++ b/src/api/dataref.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from dataclasses import dataclass
from typing import Any
diff --git a/src/api/debug.py b/src/api/debug.py
index b305ab1fa..434158cc5 100644
--- a/src/api/debug.py
+++ b/src/api/debug.py
@@ -1,6 +1,9 @@
-# vim:ts=4:sw=4:et:
-
-# Simple debugging module
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
import inspect
import os
diff --git a/src/api/decorator.py b/src/api/decorator.py
index a9bc6bb83..e2573df65 100644
--- a/src/api/decorator.py
+++ b/src/api/decorator.py
@@ -1,4 +1,12 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from collections.abc import Callable
+from functools import wraps
class ClassProperty:
@@ -17,3 +25,14 @@ def __get__(self, owner_self, owner_cls: type):
def classproperty(fget: Callable[[type], Callable]) -> ClassProperty:
"""Use this function as the decorator in lowercase to follow Python conventions."""
return ClassProperty(fget)
+
+
+def cached_function(f: Callable) -> Callable:
+ """Decorator to cache the return value of a function."""
+ cache = {}
+
+ @wraps(f)
+ def wrapper(*args, **kwargs):
+ return cache.setdefault(args, f(*args, **kwargs))
+
+ return wrapper
diff --git a/src/api/errmsg.py b/src/api/errmsg.py
index 13241294c..97ccbca98 100644
--- a/src/api/errmsg.py
+++ b/src/api/errmsg.py
@@ -1,11 +1,9 @@
-# vim: ts=4:et:sw=4:
-
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
import sys
from collections.abc import Callable
@@ -21,6 +19,7 @@
"register_warning",
"warning",
"warning_not_used",
+ "warning_uninitalized_string_var",
)
@@ -44,6 +43,9 @@ def info(msg: str) -> None:
def error(lineno: int, msg: str, fname: str | None = None) -> None:
"""Generic syntax error routine"""
+ if getattr(global_, "syntax_error_occurred", False) and not getattr(global_, "reporting_syntax_error", False):
+ return
+
if fname is None:
fname = global_.FILENAME
@@ -61,6 +63,8 @@ def error(lineno: int, msg: str, fname: str | None = None) -> None:
def warning(lineno: int, msg: str, fname: str | None = None) -> None:
"""Generic warning error routine"""
+ if getattr(global_, "syntax_error_occurred", False):
+ return
global_.has_warnings += 1
if global_.has_warnings <= config.OPTIONS.expected_warnings:
return
@@ -129,6 +133,12 @@ def warning_implicit_type(lineno: int, id_: str, type_: str = None):
warning(lineno, "Using default implicit type '%s' for '%s'" % (type_, id_))
+@register_warning("101")
+def warning_uninitalized_string_var(lineno: int, id_: str):
+ """Warning: Accessing uninitialized string variable"""
+ warning(lineno, f"Accessing uninitialized string variable '{id_}'")
+
+
@register_warning("110")
def warning_condition_is_always(lineno: int, cond: bool = False):
"""Warning: Condition is always false/true"""
diff --git a/src/api/exception.py b/src/api/exception.py
index 97544bb40..ca353394a 100644
--- a/src/api/exception.py
+++ b/src/api/exception.py
@@ -1,14 +1,11 @@
-# vim:ts=4:et:sw=4:
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
-
-
-# ------------------------- ERROR exception classes ---------------------------
+from __future__ import annotations
__all__ = [
"Error",
@@ -62,3 +59,10 @@ class TempAlreadyFreedError(InternalError):
def __init__(self, label):
super().__init__(f"Label '{label}' already freed")
self.label = label
+
+
+class InvalidTypeError(InternalError):
+ """Raised when a type is invalid."""
+
+ def __init__(self, msg: str):
+ super().__init__(msg)
diff --git a/src/api/fp.py b/src/api/fp.py
index 478271000..5e0475e71 100644
--- a/src/api/fp.py
+++ b/src/api/fp.py
@@ -1,4 +1,9 @@
-# Floating point converter
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
def fp(x: float) -> tuple[str, str]:
diff --git a/src/api/global_.py b/src/api/global_.py
index 01856fa5a..c432a4ed8 100644
--- a/src/api/global_.py
+++ b/src/api/global_.py
@@ -1,11 +1,10 @@
-# vim:ts=4:et:sw=4:
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
from __future__ import annotations
from typing import TYPE_CHECKING, Final, NamedTuple
@@ -60,6 +59,11 @@ class LoopInfo(NamedTuple):
# ----------------------------------------------------------------------
has_errors = 0 # Number of errors
has_warnings = 0 # Number of warnings
+syntax_error_occurred = False # True if a syntax error occurred during parse
+reporting_syntax_error = False # True if we are actively reporting a syntax error
+tokens_yielded = 0
+tokens_rejected = 0
+shifted_at_last_error = 0
# ----------------------------------------------------------------------
# Default var type when not specified (implicit) an can't be guessed
diff --git a/src/api/lex.py b/src/api/lex.py
new file mode 100644
index 000000000..15b208e4f
--- /dev/null
+++ b/src/api/lex.py
@@ -0,0 +1,245 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
+import re
+import sys
+from collections.abc import Callable
+from typing import Any
+
+
+class LexError(Exception):
+ pass
+
+
+class LexToken:
+ def __init__(self) -> None:
+ self.type: str | None = None
+ self.value: Any = None
+ self.lineno: int = 1
+ self.lexpos: int = 0
+ self.lexer: Lexer | None = None
+
+ def __repr__(self) -> str:
+ return f"LexToken({self.type},{self.value!r},{self.lineno},{self.lexpos})"
+
+
+class Lexer:
+ def __init__(self, obj: Any) -> None:
+ self._object = obj
+ self.lexdata: str = ""
+ self.lexpos: int = 0
+ self.lineno: int = 1
+ self.statestack: list[str] = ["INITIAL"]
+ self.next_token: LexToken | None = None
+
+ # Parse states
+ states = get_attr(obj, "states", ())
+ state_names = {"INITIAL"}
+ for s in states:
+ state_names.add(s[0])
+
+ # Parse tokens
+ self.tokens = get_attr(obj, "tokens", ())
+
+ # Collect rules
+ functions: list[tuple[str, list[str], str, str, Callable[[LexToken], Any]]] = []
+ strings: list[tuple[str, list[str], str, str]] = []
+ self.error_handlers: dict[str, Callable[[LexToken], Any]] = {}
+
+ # dir(obj) preserves definition order in modern Python,
+ # but we want to be safe and sort functions by co_firstlineno
+ for name in get_dir(obj):
+ if name.startswith("t_"):
+ val = get_attr(obj, name)
+ if name.endswith("_error"):
+ parts = name[2:-6].split("_")
+ target_states = [p for p in parts if p in state_names]
+ if not target_states:
+ target_states = ["INITIAL"]
+ for s in target_states:
+ self.error_handlers[s] = val
+ else:
+ parts = name[2:].split("_")
+ target_states = []
+ i = 0
+ # Consume state names from the left, leaving at least one part for the rule name
+ while i < len(parts) - 1 and parts[i] in state_names:
+ target_states.append(parts[i])
+ i += 1
+
+ if not target_states:
+ target_states = ["INITIAL"]
+ rule_name = "_".join(parts)
+ else:
+ rule_name = "_".join(parts[i:])
+
+ if callable(val):
+ pattern = val.__doc__
+ if pattern:
+ functions.append((name, target_states, rule_name, pattern, val))
+ elif isinstance(val, str):
+ strings.append((name, target_states, rule_name, val))
+
+ # Sort functions by co_firstlineno to match definition order in file
+ def get_line_no(item: tuple[str, list[str], str, str, Callable[[LexToken], Any]]) -> int:
+ func = item[4]
+ try:
+ if hasattr(func, "__code__"):
+ return func.__code__.co_firstlineno
+ if hasattr(func, "__func__") and hasattr(func.__func__, "__code__"):
+ return func.__func__.__code__.co_firstlineno
+ except Exception:
+ pass
+ return 0
+
+ functions.sort(key=get_line_no)
+
+ # Sort strings by pattern length descending
+ strings.sort(key=lambda x: len(x[3]), reverse=True)
+
+ # Build rules per state
+ state_rules: dict[str, list[tuple[str, re.Pattern[str], Callable[[LexToken], Any] | None]]] = {
+ s: [] for s in state_names
+ }
+
+ # Add functions first
+ for name, target_states, rule_name, pattern, func in functions:
+ try:
+ rx = re.compile(pattern)
+ for s in target_states:
+ state_rules[s].append((rule_name, rx, func))
+ except Exception as e:
+ print(f"Error compiling pattern {pattern!r} for rule {name}: {e}", file=sys.stderr)
+ raise e
+
+ # Add strings second
+ for name, target_states, rule_name, pattern in strings:
+ try:
+ rx = re.compile(pattern)
+ for s in target_states:
+ state_rules[s].append((rule_name, rx, None))
+ except Exception as e:
+ print(f"Error compiling pattern {pattern!r} for rule {name}: {e}", file=sys.stderr)
+ raise e
+
+ self.compiled_rules = state_rules
+
+ def input(self, data: str) -> None:
+ self.lexdata = data
+ self.lexpos = 0
+ self.lineno = 1
+ self.statestack = ["INITIAL"]
+ self.next_token = None
+
+ def begin(self, state: str) -> None:
+ if state not in self.compiled_rules:
+ raise LexError(f"Undefined state: {state}")
+ self.statestack[-1] = state
+
+ def push_state(self, state: str) -> None:
+ if state not in self.compiled_rules:
+ raise LexError(f"Undefined state: {state}")
+ self.statestack.append(state)
+
+ def pop_state(self) -> None:
+ if len(self.statestack) > 1:
+ self.statestack.pop()
+ else:
+ self.statestack = ["INITIAL"]
+
+ def skip(self, n: int) -> None:
+ self.lexpos += n
+
+ def clone(self) -> Lexer:
+ c = Lexer(self._object)
+ c.lexdata = self.lexdata
+ c.lexpos = self.lexpos
+ c.lineno = self.lineno
+ c.statestack = list(self.statestack)
+ c.next_token = self.next_token
+ return c
+
+ def token(self) -> LexToken | None:
+ if self.next_token is not None:
+ t = self.next_token
+ self.next_token = None
+ return t
+
+ while self.lexpos < len(self.lexdata):
+ state = self.statestack[-1]
+ rules = self.compiled_rules.get(state, [])
+
+ matched = False
+ for rule_name, rx, val in rules:
+ m = rx.match(self.lexdata, self.lexpos)
+ if m:
+ matched = True
+ matched_text = m.group(0)
+
+ t = LexToken()
+ t.type = rule_name
+ t.value = matched_text
+ t.lineno = self.lineno
+ t.lexpos = self.lexpos
+ t.lexer = self
+
+ # Advance position BEFORE calling function
+ self.lexpos += len(matched_text)
+
+ if val:
+ res = val(t)
+ if res is None:
+ # Ignored token
+ break
+ return res
+ # Convert to token type (which is rule_name)
+ return t
+
+ if matched:
+ continue
+
+ # No rule matched
+ err_handler = self.error_handlers.get(state)
+ if err_handler:
+ t = LexToken()
+ t.type = "error"
+ t.value = self.lexdata[self.lexpos :]
+ t.lineno = self.lineno
+ t.lexpos = self.lexpos
+ t.lexer = self
+
+ old_pos = self.lexpos
+ res = err_handler(t)
+ if self.lexpos == old_pos:
+ self.lexpos += 1
+ if res is not None:
+ return res
+ else:
+ raise LexError(
+ f"Lexical error: Illegal character {self.lexdata[self.lexpos]!r} at position {self.lexpos}"
+ )
+
+ return None
+
+
+def get_attr(obj: Any, name: str, default: Any = None) -> Any:
+ if isinstance(obj, dict):
+ return obj.get(name, default)
+ return getattr(obj, name, default)
+
+
+def get_dir(obj: Any) -> list[str]:
+ if isinstance(obj, dict):
+ return list(obj.keys())
+ return dir(obj)
+
+
+def lex(object: Any = None) -> Lexer:
+ if object is None:
+ frame = sys._getframe(1)
+ object = frame.f_globals
+ return Lexer(object)
diff --git a/src/api/opcodestemps.py b/src/api/opcodestemps.py
index a17aa27b3..89417c516 100644
--- a/src/api/opcodestemps.py
+++ b/src/api/opcodestemps.py
@@ -1,9 +1,9 @@
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
__all__ = "OpcodesTemps", "init"
diff --git a/src/api/optimize.py b/src/api/optimize.py
index 1f7fdeca4..41284cb6c 100644
--- a/src/api/optimize.py
+++ b/src/api/optimize.py
@@ -1,5 +1,12 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import symtable
-from collections.abc import Generator
+from collections.abc import Callable, Generator
from typing import Any, NamedTuple
import src.api.check as chk
@@ -10,28 +17,17 @@
from src.api import errmsg
from src.api.config import OPTIONS
from src.api.constants import CLASS, CONVENTION, SCOPE, TYPE
-from src.api.debug import __DEBUG__
from src.api.errmsg import warning_not_used
-from src.ast import Ast, NodeVisitor
+from src.ast_ import Ast, NodeVisitor
from src.symbols import sym as symbols
from src.symbols.id_ import ref
-class ToVisit(NamedTuple):
- """Used just to signal an object to be
- traversed.
- """
-
- obj: symbols.SYMBOL
-
-
class GenericVisitor(NodeVisitor):
"""A slightly different visitor, that just traverses an AST, but does not return
a translation of it. Used to examine the AST or do transformations
"""
- node_type = ToVisit
-
@property
def O_LEVEL(self):
return OPTIONS.optimization_level
@@ -51,36 +47,42 @@ def TYPE(type_):
assert TYPE.is_valid(type_)
return gl.SYMBOL_TABLE.basic_types[type_]
- def visit(self, node):
- return super().visit(ToVisit(node))
-
- def _visit(self, node: ToVisit):
- if node.obj is None:
- return None
-
- __DEBUG__(f"Optimizer: Visiting node {node.obj!s}[{node.obj.token}]", 1)
- meth = getattr(self, f"visit_{node.obj.token}", self.generic_visit)
- return meth(node.obj)
-
- def generic_visit(self, node: Ast) -> Generator[Ast | None, Any, None]:
- for i, child in enumerate(node.children):
- node.children[i] = yield self.visit(child)
-
- yield node
-
class UniqueVisitor(GenericVisitor):
def __init__(self):
super().__init__()
self.visited = set()
- def _visit(self, node: ToVisit):
- if node.obj in self.visited:
- return node.obj
+ def _visit(self, node: Ast):
+ if node in self.visited:
+ return node
- self.visited.add(node.obj)
+ self.visited.add(node)
return super()._visit(node)
+ def filter_inorder(
+ self,
+ node,
+ filter_func: Callable[[Any], bool],
+ child_selector: Callable[[Ast], bool] = lambda x: True,
+ ) -> Generator[Ast]:
+ """Visit the tree inorder, but only those that return true for filter_func and visiting children which
+ return true for child_selector.
+ """
+ visited = set()
+ stack = [node]
+ while stack:
+ node = stack.pop()
+ if node in visited:
+ continue
+
+ visited.add(node)
+ if filter_func(node):
+ yield self.visit(node)
+
+ if isinstance(node, Ast) and child_selector(node):
+ stack.extend(node.children[::-1])
+
class UnreachableCodeVisitor(UniqueVisitor):
"""Visitor to optimize unreachable code (and prune it)."""
@@ -97,10 +99,10 @@ def visit_FUNCTION(self, node: symbols.ID):
lineno = node.lineno if not node.body else node.body[-1].lineno
errmsg.warning_function_should_return_a_value(lineno, node.name, node.filename)
type_ = node.type_
- if type_ is not None and type_ == self.TYPE(TYPE.string):
+ if type_ is not None and type_ == self.TYPE(TYPE.string) and node.convention != CONVENTION.fastcall:
node.body.append(symbols.ASM("\nld hl, 0\n", lineno, node.filename, is_sentinel=True))
- yield (yield self.generic_visit(node))
+ yield self.generic_visit(node)
def visit_BLOCK(self, node):
# Remove CHKBREAK after labels
@@ -148,7 +150,7 @@ def visit_BLOCK(self, node):
yield self.NOP
return
- yield (yield self.generic_visit(node))
+ yield self.generic_visit(node)
class FunctionGraphVisitor(UniqueVisitor):
@@ -158,6 +160,7 @@ def _get_calls_from_children(self, node: symtable.Symbol):
return list(self.filter_inorder(node, lambda x: x.token in ("CALL", "FUNCCALL")))
def _set_children_as_accessed(self, node: symbols.SYMBOL):
+ """ "Traverse only those"""
parent = node.get_parent(symbols.FUNCDECL)
if parent is None: # Global scope?
for symbol in self._get_calls_from_children(node):
@@ -307,23 +310,27 @@ def visit_FUNCDECL(self, node):
if self.O_LEVEL > 1 and node.params_size == node.locals_size == 0:
node.entry.ref.convention = CONVENTION.fastcall
- node.children[1] = yield ToVisit(node.entry)
+ node.children[1] = yield self.visit(node.entry)
yield node
def visit_LET(self, node):
lvalue = node.children[0]
if self.O_LEVEL > 1 and not lvalue.accessed:
warning_not_used(lvalue.lineno, lvalue.name, fname=lvalue.filename)
- block = symbols.BLOCK(
- *[
- symbols.CALL(x.entry, x.args, x.lineno, lvalue.filename)
- for x in self.filter_inorder(
- node.children[1],
- lambda x: x.token == "FUNCCALL",
- lambda x: x.token != "FUNCTION",
- )
- ]
- )
+ nodes = [
+ symbols.CALL(x.entry, x.args, x.lineno, lvalue.filename) if x.token == "FUNCCALL" else x
+ for x in self.filter_inorder(
+ node.children[1],
+ lambda x: x.token in ("FUNCCALL", "BUILTIN"),
+ lambda x: x.token != "FUNCTION",
+ )
+ if x.token == "FUNCCALL" or getattr(x, "fname") in {"IN", "RND", "USR"}
+ ]
+ for node_ in nodes:
+ if node_.token == "BUILTIN":
+ node_.discard_result = True
+
+ block = symbols.BLOCK(*nodes)
yield block
else:
yield (yield self.generic_visit(node))
@@ -359,19 +366,20 @@ def visit_RETURN(self, node):
might cause infinite recursion.
"""
if len(node.children) == 2:
- node.children[1] = yield ToVisit(node.children[1])
+ node.children[1] = yield self.visit(node.children[1])
+
yield node
def visit_UNARY(self, node):
if node.operator == "ADDRESS":
- yield (yield self.visit_ADDRESS(node))
+ yield self.visit_ADDRESS(node)
else:
- yield (yield self.generic_visit(node))
+ yield self.generic_visit(node)
def visit_IF(self, node):
- expr_ = yield ToVisit(node.children[0])
- then_ = yield ToVisit(node.children[1])
- else_ = (yield ToVisit(node.children[2])) if len(node.children) == 3 else self.NOP
+ expr_ = yield self.visit(node.children[0])
+ then_ = yield self.visit(node.children[1])
+ else_ = (yield self.visit(node.children[2])) if len(node.children) == 3 else self.NOP
if self.O_LEVEL >= 1:
if chk.is_null(then_, else_):
@@ -394,6 +402,7 @@ def visit_IF(self, node):
for i in range(len(node.children)):
node.children[i] = (expr_, then_, else_)[i]
+
yield node
def visit_WHILE(self, node):
@@ -408,6 +417,7 @@ def visit_WHILE(self, node):
for i, child in enumerate((expr_, body_)):
node.children[i] = child
+
yield node
def visit_FOR(self, node):
@@ -422,6 +432,7 @@ def visit_FOR(self, node):
if from_.value > to_.value and step_.value > 0:
yield self.NOP
return
+
if from_.value < to_.value and step_.value < 0:
yield self.NOP
return
@@ -435,12 +446,6 @@ def _visit_LABEL(self, node):
else:
yield node
- def generic_visit(self, node: Ast):
- for i, child in enumerate(node.children):
- node.children[i] = yield ToVisit(child)
-
- yield node
-
def _check_if_any_arg_is_an_array_and_needs_lbound_or_ubound(
self, params: symbols.PARAMLIST, args: symbols.ARGLIST
):
@@ -491,10 +496,7 @@ class VariableVisitor(GenericVisitor):
def generic_visit(self, node: Ast):
if node not in VariableVisitor._visited:
VariableVisitor._visited.add(node)
- for i in range(len(node.children)):
- node.children[i] = yield ToVisit(node.children[i])
-
- yield node
+ yield super().generic_visit(node)
def has_circular_dependency(self, var_dependency: VarDependency) -> bool:
if var_dependency.dependency == VariableVisitor._original_variable:
@@ -521,7 +523,7 @@ def visit_var(entry):
if entry.token != "VAR":
for child in entry.children:
visit_var(child)
- if child.token in ("FUNCTION", "LABEL", "VAR", "VARARRAY"):
+ if child.token in {"FUNCTION", "LABEL", "VAR", "VARARRAY"}:
result.add(VarDependency(parent=VariableVisitor._parent_variable, dependency=child))
return
diff --git a/src/api/options.py b/src/api/options.py
index 4e0c7c71b..943be16d3 100644
--- a/src/api/options.py
+++ b/src/api/options.py
@@ -1,11 +1,9 @@
-# vim: ts=4:sw=4:et:
-
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
import enum
import json
diff --git a/src/api/python_version_check.py b/src/api/python_version_check.py
index c54154767..78f31577f 100644
--- a/src/api/python_version_check.py
+++ b/src/api/python_version_check.py
@@ -1,7 +1,14 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import sys
from typing import Final
-MINIMUM_REQUIRED_PYTHON_VERSION: Final[tuple[int, int]] = (3, 11)
+MINIMUM_REQUIRED_PYTHON_VERSION: Final[tuple[int, int]] = (3, 12)
def init():
diff --git a/src/api/string_labels.py b/src/api/string_labels.py
index 35a87841c..f86a92dbc 100644
--- a/src/api/string_labels.py
+++ b/src/api/string_labels.py
@@ -1,9 +1,9 @@
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
__doc__ = """This module is a singleton instance that contains
a mapping of constant Strings to Labels.
diff --git a/src/api/symboltable/__init__.py b/src/api/symboltable/__init__.py
index e69de29bb..906667a8d 100644
--- a/src/api/symboltable/__init__.py
+++ b/src/api/symboltable/__init__.py
@@ -0,0 +1,6 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
diff --git a/src/api/symboltable/scope.py b/src/api/symboltable/scope.py
index 2ef7deefc..621d98903 100644
--- a/src/api/symboltable/scope.py
+++ b/src/api/symboltable/scope.py
@@ -1,14 +1,11 @@
-# vim: ts=4:et:sw=4:
-
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from collections import OrderedDict
-from typing import Optional
from src.api.config import OPTIONS
from src.symbols.id_ import SymbolID
@@ -39,7 +36,7 @@ class Scope:
myFunct will be output as _myFunct_a.
"""
- def __init__(self, namespace: str = "", parent_scope: Optional["Scope"] = None):
+ def __init__(self, namespace: str = "", parent_scope: Scope | None = None):
from src.symbols.funcdecl import SymbolFUNCDECL
self.symbols: dict[str, SymbolID] = OrderedDict()
diff --git a/src/api/symboltable/symboltable.py b/src/api/symboltable/symboltable.py
index 176a6a4eb..507b3731e 100644
--- a/src/api/symboltable/symboltable.py
+++ b/src/api/symboltable/symboltable.py
@@ -1,12 +1,9 @@
-# vim: ts=4:et:sw=4:
-
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
-
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from src.api import check as check
from src.api import errmsg, global_
@@ -61,7 +58,7 @@ def __init__(self):
# Initialize canonical types
for type_ in TYPE.types:
- self.basic_types[type_] = self.declare_type(symbols.BASICTYPE(type_))
+ self.basic_types[type_] = self.declare_type(name=type_.name, lineno=0, type_=symbols.BASICTYPE(type_))
@property
def current_scope(self) -> Scope:
@@ -71,39 +68,54 @@ def current_scope(self) -> Scope:
def global_scope(self) -> Scope:
return self.table[0]
- def get_entry(self, id_: str, scope: Scope | None = None) -> symbols.ID | None:
+ def get_entry(self, id_: str, scope: Scope | None = None, lineno: int | None = None) -> symbols.ID | None:
"""Returns the ID entry stored in self.table, starting
by the first one. Returns None if not found.
If scope is not None, only the given scope is searched.
"""
+ suffix = None
if id_[-1] in DEPRECATED_SUFFIXES:
+ suffix = id_[-1]
id_ = id_[:-1] # Remove it
+ entry = None
if scope is not None:
- return scope[id_]
-
- for s in self:
- if s[id_] is not None:
- return s[id_]
+ entry = scope[id_]
+ else:
+ for s in self:
+ if s[id_] is not None:
+ entry = s[id_]
+ break
+
+ if entry is not None and suffix is not None and lineno is not None:
+ if isinstance(entry, symbols.ID) and entry.type_ is not None:
+ expected_type = symbols.TYPEREF(self.basic_types[SUFFIX_TYPE[suffix]], lineno)
+ if entry.type_ != expected_type:
+ if suffix == "$":
+ syntax_error(lineno, "Invalid $ suffix (or sigil) for numeric variable")
+ else:
+ syntax_error(lineno, f"Invalid {suffix} suffix (or sigil) for {entry.type_.name} variable")
- return None # Not found
+ return entry
- def get_existing_entry(self, id_: str, scope: Scope | None = None) -> symbols.ID:
- result = self.get_entry(id_, scope)
+ def get_existing_entry(self, id_: str, scope: Scope | None = None, lineno: int | None = None) -> symbols.ID:
+ result = self.get_entry(id_, scope, lineno=lineno)
assert result is not None
return result
- def declare(self, id_: str, lineno: int, entry: symbols.ID) -> None | symbols.ID | symbols.TYPE:
+ def declare(self, id_: str, lineno: int, entry: symbols.ID | symbols.TYPE) -> None | symbols.ID | symbols.TYPE:
"""Check there is no 'id' already declared in the current scope, and
creates and returns it. Otherwise, returns None,
and the caller function raises the syntax/semantic error.
Parameter entry is the SymbolVAR, SymbolVARARRAY, etc. instance
- The entry 'declared' field is leave untouched. Setting it if on
+ The entry 'declared' field is left untouched. Setting it if on
behalf of the caller.
"""
+ assert isinstance(entry, symbols.ID | symbols.TYPE)
+
id2 = id_
- type_ = entry.type_
+ type_ = entry.type_ if isinstance(entry, symbols.ID) else entry
if id2[-1] in DEPRECATED_SUFFIXES:
id2 = id2[:-1] # Remove it
@@ -133,7 +145,7 @@ def declare(self, id_: str, lineno: int, entry: symbols.ID) -> None | symbols.ID
return entry
- def declare_safe(self, id_: str, lineno: int, entry: symbols.ID):
+ def declare_safe(self, id_: str, lineno: int, entry: symbols.ID) -> symbols.ID | symbols.TYPE:
"""Like declare, but never returns None"""
result = self.declare(id_, lineno, entry)
assert result is not None
@@ -163,7 +175,7 @@ def check_is_declared(
Note: classname is not the class attribute, but the name of
the class as it would appear on compiler messages.
"""
- result = self.get_entry(id_, scope)
+ result = self.get_entry(id_, scope, lineno=lineno)
if isinstance(result, symbols.TYPE):
return True
@@ -177,13 +189,21 @@ def check_is_declared(
return False
return True
- def check_is_undeclared(self, id_: str, lineno: int, classname="identifier", scope=None, show_error=False) -> bool:
+ def check_is_undeclared(
+ self,
+ id_: str,
+ lineno: int,
+ classname: str = "identifier",
+ scope: Scope | None = None,
+ *,
+ show_error=False,
+ ) -> bool:
"""The reverse of the above.
Check the given identifier is not already declared. Returns True
if OK, False otherwise.
"""
- result = self.get_entry(id_, scope)
+ result = self.get_entry(id_, scope, lineno=lineno)
if result is None or not result.declared:
return True
@@ -197,7 +217,7 @@ def check_is_undeclared(self, id_: str, lineno: int, classname="identifier", sco
)
return False
- def check_class(self, id_: str, class_: CLASS, lineno: int, scope: Scope = None, show_error=True) -> bool:
+ def check_class(self, id_: str, class_: CLASS, lineno: int, scope: Scope | None = None, show_error=True) -> bool:
"""Check the id is either undefined or defined with
the given class.
@@ -210,7 +230,7 @@ def check_class(self, id_: str, class_: CLASS, lineno: int, scope: Scope = None,
Otherwise, fails returning False.
"""
assert CLASS.is_valid(class_)
- entry = self.get_entry(id_, scope)
+ entry = self.get_entry(id_, scope, lineno=lineno)
if entry is None or entry.class_ in (CLASS.unknown, class_): # Undeclared yet
return True
@@ -331,10 +351,11 @@ def access_id(
id_: str,
lineno: int,
scope=None,
- default_type=None,
- default_class=CLASS.unknown,
+ default_type: symbols.TYPING | None = None,
+ default_class: CLASS = CLASS.unknown,
+ *,
ignore_explicit_flag=False,
- ):
+ ) -> symbols.ID | None:
"""Access a symbol by its identifier and checks if it exists.
If not, it's supposed to be an implicitly declared variable.
@@ -347,13 +368,13 @@ def access_id(
if not ignore_explicit_flag:
check.check_is_declared_explicit(lineno, id_)
- result = self.get_entry(id_, scope)
+ result = self.get_entry(id_, scope, lineno=lineno)
if result is None:
if default_type is None:
default_type = symbols.TYPEREF(self.basic_types[global_.DEFAULT_IMPLICIT_TYPE], lineno, implicit=True)
result = self.declare_safe(
- id_, lineno, entry=symbols.ID(id_, lineno, type_=default_type, class_=default_class)
+ id_, lineno, entry=symbols.ID(id_, lineno, type_ref=default_type, class_=default_class)
)
return result
@@ -361,7 +382,7 @@ def access_id(
# update its type.
if default_type is not None and result.type_ == self.basic_types[TYPE.unknown]:
if default_type == self.basic_types[TYPE.boolean]:
- default_type = self.basic_types[TYPE.ubyte]
+ default_type = symbols.TYPEREF(self.basic_types[TYPE.ubyte], 0)
result.type_ = default_type
warning_implicit_type(lineno, id_, default_type.name)
@@ -418,7 +439,7 @@ def access_func(self, id_: str, lineno: int, scope=None, default_type=None):
"""
assert default_type is None or isinstance(default_type, symbols.TYPEREF)
- result = self.get_entry(id_, scope)
+ result = self.get_entry(id_, scope, lineno=lineno)
if result is None:
if default_type is None:
if global_.DEFAULT_IMPLICIT_TYPE == TYPE.unknown:
@@ -434,10 +455,9 @@ def access_func(self, id_: str, lineno: int, scope=None, default_type=None):
return result
- def access_call(self, id_: str, lineno: int, scope=None, type_=None):
+ def access_call(self, id_: str, lineno: int, scope=None, type_=None) -> symbols.ID | None:
"""Creates a func/array/string call. Checks if id is callable or not.
- An identifier is "callable" if it can be followed by a list of para-
- meters.
+ An identifier is "callable" if it can be followed by a list of parameters.
This does not mean the id_ is a function, but that it allows the same
syntax a function does:
@@ -465,7 +485,7 @@ def access_call(self, id_: str, lineno: int, scope=None, type_=None):
return entry
def access_label(self, id_: str, lineno: int, scope: Scope | None = None):
- result = self.get_entry(id_, scope)
+ result = self.get_entry(id_, scope, lineno=lineno)
if result is None:
result = self.declare_label(id_, lineno)
result.declared = False
@@ -478,7 +498,9 @@ def access_label(self, id_: str, lineno: int, scope: Scope | None = None):
return result
- def declare_variable(self, id_: str, lineno: int, type_, default_value=None, class_: CLASS = CLASS.var):
+ def declare_variable(
+ self, id_: str, lineno: int, type_: symbols.TYPEREF, default_value=None, class_: CLASS = CLASS.var
+ ):
"""Like the above, but checks that entry.declared is False.
Otherwise, raises an error.
@@ -488,7 +510,7 @@ def declare_variable(self, id_: str, lineno: int, type_, default_value=None, cla
assert class_ in (CLASS.const, CLASS.var, CLASS.unknown)
if not self.check_is_undeclared(id_, lineno, scope=self.current_scope, show_error=False):
- entry_ = self.get_existing_entry(id_)
+ entry_ = self.get_existing_entry(id_, lineno=lineno)
if entry_.scope == SCOPE.parameter:
syntax_error(
lineno,
@@ -501,9 +523,9 @@ def declare_variable(self, id_: str, lineno: int, type_, default_value=None, cla
if not self.check_class(id_, class_, lineno, scope=self.current_scope):
return None
- entry = self.get_entry(id_, scope=self.current_scope)
+ entry = self.get_entry(id_, scope=self.current_scope, lineno=lineno)
if entry is None:
- entry = self.declare(id_, lineno, symbols.ID(name=id_, lineno=lineno, type_=type_))
+ entry = self.declare(id_, lineno, symbols.ID(name=id_, lineno=lineno, type_ref=type_))
assert entry is not None
if entry.class_ == CLASS.unknown:
@@ -525,7 +547,8 @@ def declare_variable(self, id_: str, lineno: int, type_, default_value=None, cla
if entry.type_ != type_:
if not type_.implicit and entry.type_ is not None:
syntax_error(
- lineno, "'%s' suffix is for type '%s' but it was declared as '%s'" % (id_, entry.type_, type_)
+ lineno,
+ f"'{id_!s}' suffix is for type '{entry.type_.name}' but it was declared as '{type_.name}'",
)
return None
@@ -536,7 +559,7 @@ def declare_variable(self, id_: str, lineno: int, type_, default_value=None, cla
if default_value is not None and entry.type_ != default_value.type_:
if check.is_number(default_value):
- default_value = symbols.TYPECAST.make_node(entry.type_, default_value, lineno)
+ default_value = symbols.TYPECAST.make_node(entry.type_.type_, default_value, lineno)
if default_value is None:
return None
else:
@@ -549,29 +572,33 @@ def declare_variable(self, id_: str, lineno: int, type_, default_value=None, cla
return entry
- def declare_type(self, type_):
- """Declares a type.
+ def declare_type(self, name: str, lineno: int, type_: symbols.TYPE) -> symbols.TYPE | None:
+ """Declares a type. The Type object must be already instantiated, as it's not
+ a normal ID Symbol.
+
Checks its name is not already used in the current scope,
and that it's not a basic type.
Returns the given type_ Symbol, or None on error.
"""
assert isinstance(type_, symbols.TYPE)
+
# Checks it's not a basic type
if not type_.is_basic and type_.name.lower() in TYPE.TYPE_NAMES.values():
syntax_error(type_.lineno, "'%s' is a basic type and cannot be redefined" % type_.name)
return None
- if not self.check_is_undeclared(type_.name, type_.lineno, scope=self.current_scope, show_error=True):
+ if not self.check_is_undeclared(name, lineno, scope=self.current_scope, show_error=True):
return None
entry = self.declare(type_.name, type_.lineno, type_)
+ assert isinstance(entry, symbols.TYPE)
return entry
def declare_const(self, id_: str, lineno: int, type_, default_value):
"""Similar to the above. But declares a Constant."""
if not self.check_is_undeclared(id_, lineno, scope=self.current_scope, show_error=False):
- entry = self.get_entry(id_)
+ entry = self.get_entry(id_, lineno=lineno)
if entry.scope == SCOPE.parameter:
syntax_error(
lineno,
@@ -593,11 +620,11 @@ def declare_label(self, id_: str, lineno: int) -> symbols.ID | None:
"""
# TODO: consider to make labels private
if not self.check_is_undeclared(id_, lineno, "label"):
- e = self.get_existing_entry(id_)
+ e = self.get_existing_entry(id_, lineno=lineno)
syntax_error(lineno, f"Label '{id_}' already used at {e.filename}:{e.lineno}")
return e
- entry = self.get_entry(id_)
+ entry = self.get_entry(id_, lineno=lineno)
if entry is not None and entry.declared:
if entry.is_line_number:
syntax_error(lineno, "Duplicated line number '%s'. Previous was at %i" % (entry.name, entry.lineno))
@@ -606,8 +633,8 @@ def declare_label(self, id_: str, lineno: int) -> symbols.ID | None:
return None
entry = (
- self.get_entry(id_, scope=self.current_scope)
- or self.get_entry(id_, scope=self.global_scope)
+ self.get_entry(id_, scope=self.current_scope, lineno=lineno)
+ or self.get_entry(id_, scope=self.global_scope, lineno=lineno)
or self.declare(id_, lineno, symbols.ID(id_, lineno, class_=CLASS.label))
)
if entry is None:
@@ -627,11 +654,17 @@ def declare_label(self, id_: str, lineno: int) -> symbols.ID | None:
self.move_to_global_scope(id_) # Labels are always global # TODO: not in the future
entry.declared = True
- entry.type_ = self.basic_types[global_.PTR_TYPE]
+ entry.type_ = symbols.TYPEREF(self.basic_types[global_.PTR_TYPE], lineno, implicit=False)
return entry
def declare_param(
- self, id_: str, lineno: int, type_=None, is_array=False, default_value: Symbol | None = None
+ self,
+ id_: str,
+ lineno: int,
+ type_: symbols.TYPEREF | None = None,
+ default_value: Symbol | None = None,
+ *,
+ is_array: bool = False,
) -> symbols.ID | None:
"""Declares a parameter
Check if entry.declared is False. Otherwise, raises an error.
@@ -644,23 +677,35 @@ def declare_param(
syntax_error_cannot_define_default_array_argument(lineno)
return None
- entry = self.declare_safe(id_, lineno, symbols.ID(name=id_, lineno=lineno, type_=type_)).to_vararray(
- bounds=symbols.BOUNDLIST()
- )
+ entry = self.declare_safe(
+ id_,
+ lineno,
+ symbols.ID(name=id_, lineno=lineno, type_ref=type_),
+ ).to_vararray(bounds=symbols.BOUNDLIST())
else:
- entry = self.declare_safe(id_, lineno, symbols.ID(name=id_, lineno=lineno, type_=type_)).to_var(
- default_value=default_value
- )
+ entry = self.declare_safe(
+ id_,
+ lineno,
+ symbols.ID(name=id_, lineno=lineno, type_ref=type_),
+ ).to_var(default_value=default_value)
entry.scope = SCOPE.parameter
entry.declared = True
if entry.type_.implicit:
- warning_implicit_type(lineno, id_, type_)
+ warning_implicit_type(lineno, id_, type_.name)
return entry
- def declare_array(self, id_: str, lineno: int, type_, bounds, default_value=None, addr=None):
+ def declare_array(
+ self,
+ id_: str,
+ lineno: int,
+ type_: symbols.TYPEREF,
+ bounds: symbols.BOUNDLIST,
+ default_value=None,
+ addr: str | None = None,
+ ):
"""Declares an array in the symbol table (VARARRAY). Error if already
exists.
The optional parameter addr specifies if the array elements must be placed at an specific
@@ -672,9 +717,9 @@ def declare_array(self, id_: str, lineno: int, type_, bounds, default_value=None
if not self.check_class(id_, CLASS.array, lineno, scope=self.current_scope):
return None
- entry = self.get_entry(id_, self.current_scope)
+ entry = self.get_entry(id_, self.current_scope, lineno=lineno)
if entry is None:
- entry = self.declare(id_, lineno, symbols.ID(name=id_, lineno=lineno, type_=type_))
+ entry = self.declare(id_, lineno, symbols.ID(name=id_, lineno=lineno, type_ref=type_))
assert entry is not None
if not entry.declared:
@@ -702,7 +747,7 @@ def declare_array(self, id_: str, lineno: int, type_, bounds, default_value=None
type_ = entry.type_
if type_.implicit:
- warning_implicit_type(lineno, id_, type_)
+ warning_implicit_type(lineno, id_, type_.name)
if entry.class_ != CLASS.array:
entry = symbols.ID.to_vararray(entry, bounds)
@@ -727,7 +772,7 @@ def declare_func(self, id_: str, lineno: int, type_=None, class_=CLASS.function)
if not self.check_class(id_, class_, lineno):
return None
- entry = self.get_entry(id_) # Must not exist or have _class = None or Function and declared = False
+ entry = self.get_entry(id_, lineno=lineno) # Must not exist or have CLASS.unknown/function undeclared
if entry is not None:
if entry.declared:
assert entry.token == "FUNCTION"
@@ -743,7 +788,7 @@ def declare_func(self, id_: str, lineno: int, type_=None, class_=CLASS.function)
entry.mangled = f"{self.current_namespace}_{entry.name}" # HINT: mangle for nexted scopes
else:
- entry = self.declare(id_, lineno, symbols.ID(id_, lineno, type_=type_).to_function(class_=class_))
+ entry = self.declare(id_, lineno, symbols.ID(id_, lineno, type_ref=type_).to_function(class_=class_))
assert entry.token == "FUNCTION"
if entry.forwarded:
@@ -821,7 +866,7 @@ def __getitem__(self, level: int):
return self.table[level]
def __iter__(self):
- """Iterates through scopes, from current one (innermost) to global
+ """Iterates through scopes, from the current one (innermost) to global
(outermost)
"""
for scope in self.table[::-1]:
diff --git a/src/api/tmp_labels.py b/src/api/tmp_labels.py
index a6a608dd4..d3bcd029f 100644
--- a/src/api/tmp_labels.py
+++ b/src/api/tmp_labels.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.api import global_ as gl
LABEL_COUNTER = 0
diff --git a/src/parsetab/__init__.py b/src/api/typing/__init__.py
similarity index 100%
rename from src/parsetab/__init__.py
rename to src/api/typing/__init__.py
diff --git a/src/api/typing/system.py b/src/api/typing/system.py
new file mode 100644
index 000000000..5883c66a5
--- /dev/null
+++ b/src/api/typing/system.py
@@ -0,0 +1,63 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
+from src.api import global_ as gl
+from src.api.constants import TYPE
+
+
+class Type:
+ """Represents a type in the system."""
+
+ def __init__(self, name: str, size: int):
+ self._name = name
+ self._size = size
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def size(self) -> int:
+ return self._size
+
+ @property
+ def is_primitive(self) -> bool:
+ return True
+
+
+BASIC_TYPES: dict[str, Type] = {x.name: Type(x.name, TYPE.size(x)) for x in TYPE.types}
+
+
+class CompositeType(Type):
+ def __init__(self, name: str, size: int):
+ super().__init__(name, size)
+
+ @property
+ def is_primitive(self) -> bool:
+ return False
+
+
+class AliasType(CompositeType):
+ def __init__(self, name: str, base: Type):
+ super().__init__(name, base.size)
+ self._base = base
+
+ @property
+ def base(self) -> Type:
+ return self._base
+
+
+class PointerType(CompositeType):
+ def __init__(self, name: str, base: Type):
+ super().__init__(name, TYPE.size(gl.PTR_TYPE))
+ self._base = base
+
+
+class ArrayType(CompositeType):
+ def __init__(self, name: str, base: Type):
+ super().__init__(name, base.size)
+ self._base = base
diff --git a/src/api/utils.py b/src/api/utils.py
index 0a9561cb8..2cdc07e5b 100644
--- a/src/api/utils.py
+++ b/src/api/utils.py
@@ -1,13 +1,19 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import errno
import os
-import shelve
import signal
from collections.abc import Callable, Iterable
from contextlib import contextmanager
from functools import wraps
from typing import IO, Any, TypeVar
-from src.api import constants, errmsg, global_
+from src.api import errmsg, global_
__all__ = (
"chdir",
@@ -23,19 +29,16 @@
like reading files or path management"""
-SHELVE_PATH = os.path.join(constants.ZXBASIC_ROOT, "parsetab", "tabs.dbm")
-SHELVE = shelve.open(SHELVE_PATH)
-
T = TypeVar("T")
-def first(iter_: Iterable[T], default: T | None = None) -> T | None:
+def first[T](iter_: Iterable[T], default: T | None = None) -> T | None:
"""Return the first element of an Iterable, or None if it's empty or
there are no more elements to return."""
return next(iter(iter_), default)
-def sfirst(iter_: Iterable[T]) -> T:
+def sfirst[T](iter_: Iterable[T]) -> T:
"""Return the first element of an Iterable, or fails if it's empty"""
return next(iter(iter_))
@@ -168,29 +171,15 @@ def eval_to_num(expr: str) -> int | float | None:
if it was non-numeric."""
try:
result = eval(expr, {}, {})
- except (NameError, SyntaxError, ValueError):
+ except NameError, SyntaxError, ValueError:
return None
- if isinstance(result, (int, float)):
+ if isinstance(result, int | float):
return result
return None
-def load_object(key: str) -> Any:
- return SHELVE[key] if key in SHELVE else None
-
-
-def save_object(key: str, obj: Any) -> Any:
- SHELVE[key] = obj
- SHELVE.sync()
- return obj
-
-
-def get_or_create(key: str, fn: Callable[[], Any]) -> Any:
- return load_object(key) or save_object(key, fn())
-
-
def timeout(seconds: Callable[[], int] | int = 10, error_message=os.strerror(errno.ETIME)):
def decorator(func):
def _handle_timeout(signum, frame):
diff --git a/src/arch/__init__.py b/src/arch/__init__.py
index 2344039d2..241f40820 100755
--- a/src/arch/__init__.py
+++ b/src/arch/__init__.py
@@ -1,9 +1,9 @@
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNUv3 General License
-# ----------------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
import importlib
from types import ModuleType
@@ -13,6 +13,7 @@
__all__ = (
"zx48k",
"zxnext",
+ "zx81sd",
)
AVAILABLE_ARCHITECTURES = __all__
diff --git a/src/arch/interface/__init__.py b/src/arch/interface/__init__.py
index e69de29bb..906667a8d 100644
--- a/src/arch/interface/__init__.py
+++ b/src/arch/interface/__init__.py
@@ -0,0 +1,6 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
diff --git a/src/arch/interface/backend.py b/src/arch/interface/backend.py
index b93f444f5..b42d6e64f 100644
--- a/src/arch/interface/backend.py
+++ b/src/arch/interface/backend.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from abc import ABC, abstractmethod
__all__ = ("BackendInterface",)
diff --git a/src/arch/interface/icinstruction.py b/src/arch/interface/icinstruction.py
new file mode 100644
index 000000000..7a75eb375
--- /dev/null
+++ b/src/arch/interface/icinstruction.py
@@ -0,0 +1,5 @@
+from enum import StrEnum
+
+
+class ICInstruction(StrEnum):
+ pass
diff --git a/src/arch/interface/optimizer.py b/src/arch/interface/optimizer.py
index bd1870e52..7d2d1383f 100644
--- a/src/arch/interface/optimizer.py
+++ b/src/arch/interface/optimizer.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from abc import ABC, abstractmethod
diff --git a/src/arch/interface/quad.py b/src/arch/interface/quad.py
index a9e4140d4..d649edaf3 100644
--- a/src/arch/interface/quad.py
+++ b/src/arch/interface/quad.py
@@ -1,8 +1,16 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from dataclasses import dataclass
-from src.arch.z80.backend.icinstruction import ICInstruction
from src.symbols.symbol_ import Symbol
+from .icinstruction import ICInstruction
+
__all__ = ("Quad",)
diff --git a/src/arch/z80/__init__.py b/src/arch/z80/__init__.py
index 8e3a66681..dcbfbadac 100644
--- a/src/arch/z80/__init__.py
+++ b/src/arch/z80/__init__.py
@@ -1,4 +1,9 @@
-# vim:ts=4:et:sw=4:
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
import src.api.global_
from src.api.constants import TYPE
diff --git a/src/arch/z80/backend/_16bit.py b/src/arch/z80/backend/_16bit.py
index a9a5928c9..7df6dc1d9 100644
--- a/src/arch/z80/backend/_16bit.py
+++ b/src/arch/z80/backend/_16bit.py
@@ -1,11 +1,9 @@
-# --------------------------------------------------------------
-# Copyleft (k) 2008, by Jose M. Rodriguez-Rosa
-# (a.k.a. Boriel, http://www.boriel.com)
-#
-# This module contains 16 bit boolean, arithmetic and
-# comparison intermediate-code translations
-# --------------------------------------------------------------
-
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from src.api.tmp_labels import tmp_label
from src.arch.z80.backend._8bit import Bits8
@@ -754,11 +752,6 @@ def bxor16(cls, ins: Quad) -> list[str]:
output.append("push hl")
return output
- if op2 == 0xFFFF: # X ^ 0xFFFF = bNOT X
- output.append(runtime_call(RuntimeLabel.NEGHL))
- output.append("push hl")
- return output
-
output = cls.get_oper(op1, op2)
output.append(runtime_call(RuntimeLabel.BXOR16))
output.append("push hl")
diff --git a/src/arch/z80/backend/_32bit.py b/src/arch/z80/backend/_32bit.py
index 0ac274e2a..c436cbb69 100644
--- a/src/arch/z80/backend/_32bit.py
+++ b/src/arch/z80/backend/_32bit.py
@@ -1,10 +1,9 @@
-# --------------------------------------------------------------
-# Copyleft (k) 2008, by Jose M. Rodriguez-Rosa
-# (a.k.a. Boriel, http://www.boriel.com)
-#
-# This module contains 32 bit boolean, arithmetic and
-# comparison intermediate-code translation
-# --------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from src.api.tmp_labels import tmp_label
from src.arch.z80.backend._8bit import Bits8
diff --git a/src/arch/z80/backend/_8bit.py b/src/arch/z80/backend/_8bit.py
index f0b1a7d41..0848a1ba9 100644
--- a/src/arch/z80/backend/_8bit.py
+++ b/src/arch/z80/backend/_8bit.py
@@ -1,12 +1,9 @@
-# vim: ts=4:sw=4:et
-
-# --------------------------------------------------------------
-# Copyleft (k) 2008, by Jose M. Rodriguez-Rosa
-# (a.k.a. Boriel, http://www.boriel.com)
-#
-# This module contains 8 bit boolean, arithmetic and
-# comparison intermediate-code translations
-# --------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from src.api.tmp_labels import tmp_label
from src.arch.interface.quad import Quad
diff --git a/src/arch/z80/backend/__init__.py b/src/arch/z80/backend/__init__.py
index 0de04d872..ddaefbe9f 100644
--- a/src/arch/z80/backend/__init__.py
+++ b/src/arch/z80/backend/__init__.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.arch.z80.optimizer.helpers import HI16, LO16
from src.arch.z80.peephole import engine
diff --git a/src/arch/z80/backend/_array.py b/src/arch/z80/backend/_array.py
index c25c6429f..22f88d461 100644
--- a/src/arch/z80/backend/_array.py
+++ b/src/arch/z80/backend/_array.py
@@ -1,12 +1,9 @@
-# vim:ts=4:et:sw=4:
-
-# --------------------------------------------------------------
-# Copyleft (k) 2008, by Jose M. Rodriguez-Rosa
-# (a.k.a. Boriel, http://www.boriel.com)
-#
-# This module contains array load/store
-# intermediate-code translations
-# --------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from ._32bit import Bits32
from ._f16 import Fixed16
@@ -316,8 +313,8 @@ def _astoref(ins: Quad) -> list[str]:
def _astorestr(ins: Quad) -> list[str]:
"""Stores a string value into a memory address.
- It copies content of 2nd operand (string), into 1st, reallocating
- dynamic memory for the 1st str. These instruction DOES ALLOW
+ It copies the content of the 2nd operand (string), into 1st, reallocating
+ dynamic memory for the 1st str. These instructions DO ALLOW
immediate strings for the 2nd parameter, starting with '#'.
"""
output = _addr(ins[1])
diff --git a/src/arch/z80/backend/_f16.py b/src/arch/z80/backend/_f16.py
index 771214cb6..106655e93 100644
--- a/src/arch/z80/backend/_f16.py
+++ b/src/arch/z80/backend/_f16.py
@@ -1,10 +1,10 @@
-# --------------------------------------------------------------
-# Copyleft (k) 2008, by Jose M. Rodriguez-Rosa
-# (a.k.a. Boriel, http://www.boriel.com)
-#
-# This module contains cls.f16 (fixed) bit boolean, arithmetic and
-# comparison intermediate-code translations
-# --------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.arch.interface.quad import Quad
from ._32bit import Bits32
diff --git a/src/arch/z80/backend/_float.py b/src/arch/z80/backend/_float.py
index f1fc0324d..3263d3266 100644
--- a/src/arch/z80/backend/_float.py
+++ b/src/arch/z80/backend/_float.py
@@ -1,14 +1,10 @@
-# --------------------------------------------------------------
-# Copyleft (k) 2008, by Jose M. Rodriguez-Rosa
-# (a.k.a. Boriel, http://www.boriel.com)
-#
-# This module contains float (FP) boolean, arithmetic and
-# comparison intermediate-code translations
-# --------------------------------------------------------------
-
-# -----------------------------------------------------
-# Floating Point operators
-# -----------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.api import fp
from src.arch.interface.quad import Quad
from src.arch.z80.backend.common import _f_ops, is_float, is_int, runtime_call
diff --git a/src/arch/z80/backend/_parray.py b/src/arch/z80/backend/_parray.py
index 4dd28b105..dd324a9e9 100644
--- a/src/arch/z80/backend/_parray.py
+++ b/src/arch/z80/backend/_parray.py
@@ -1,12 +1,9 @@
-# vim:ts=4:et:sw=4:
-
-# --------------------------------------------------------------
-# Copyleft (k) 2008, by Jose M. Rodriguez-Rosa
-# (a.k.a. Boriel, http://www.boriel.com)
-#
-# This module contains local array (both parameters and
-# comparison intermediate-code translations)
-# --------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from src.api import fp
@@ -287,22 +284,25 @@ def _pastoref(ins: Quad) -> list[str]:
def _pastorestr(ins: Quad) -> list[str]:
"""Stores a string value into a memory address.
- It copies content of 2nd operand (string), into 1st, reallocating
- dynamic memory for the 1st str. These instruction DOES ALLOW
+ It copies the content of the 2nd operand (string), into 1st, reallocating
+ dynamic memory for the 1st str. These instructions DO ALLOW
immediate strings for the 2nd parameter, starting with '#'.
"""
output = _paddr(ins[1])
- temporal = False
value = ins[2]
indirect = value[0] == "*"
if indirect:
value = value[1:]
- immediate = value[0]
+ immediate = value[0] == "#"
if immediate:
value = value[1:]
+ temporal = not immediate and value[0] != "$"
+ if temporal:
+ value = value[1:]
+
if value[0] == "_":
if indirect:
if immediate:
@@ -316,8 +316,10 @@ def _pastorestr(ins: Quad) -> list[str]:
else:
output.append("ld de, (%s)" % value)
else:
- output.append("pop de")
- temporal = True
+ if immediate:
+ output.append("ld de, %s" % value)
+ else:
+ output.append("pop de")
if indirect:
output.append(runtime_call(RuntimeLabel.LOAD_DE_DE))
diff --git a/src/arch/z80/backend/_pload.py b/src/arch/z80/backend/_pload.py
index 99cc9062b..8aa10624d 100644
--- a/src/arch/z80/backend/_pload.py
+++ b/src/arch/z80/backend/_pload.py
@@ -1,12 +1,10 @@
-# vim: et:ts=4:sw=4
-
-# --------------------------------------------------------------
-# Copyleft (k) 2008, by Jose M. Rodriguez-Rosa
-# (a.k.a. Boriel, http://www.boriel.com)
-#
-# This module contains parameter load
-# intermediate-code translations
-# --------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from . import common
from ._8bit import Bits8
from ._16bit import Bits16
diff --git a/src/arch/z80/backend/_str.py b/src/arch/z80/backend/_str.py
index 4492b636c..3f57ba47e 100644
--- a/src/arch/z80/backend/_str.py
+++ b/src/arch/z80/backend/_str.py
@@ -1,12 +1,9 @@
-# vim:ts=4:et:sw=4:
-
-# --------------------------------------------------------------
-# Copyleft (k) 2008, by Jose M. Rodriguez-Rosa
-# (a.k.a. Boriel, http://www.boriel.com)
-#
-# This module contains string arithmetic and
-# comparison intermediate-code translation
-# --------------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from .common import runtime_call
from .exception import InvalidICError as InvalidIC
@@ -128,7 +125,7 @@ def addstr(cls, ins: Quad) -> list[str]:
"""Adds 2 string values. The result is pushed onto the stack.
Note: This instruction does admit direct strings (as labels).
"""
- (tmp1, tmp2, output) = cls.get_oper(ins[2], ins[3], no_exaf=True)
+ tmp1, tmp2, output = cls.get_oper(ins[2], ins[3], no_exaf=True)
if tmp1:
output.append("push hl")
@@ -146,7 +143,7 @@ def ltstr(cls, ins: Quad) -> list[str]:
"""Compares & pops top 2 strings out of the stack.
Temporal values are freed from memory. (a$ < b$)
"""
- (tmp1, tmp2, output) = cls.get_oper(ins[2], ins[3])
+ _, _, output = cls.get_oper(ins[2], ins[3])
output.append(runtime_call(RuntimeLabels.STRLT))
output.append("push af")
return output
@@ -156,7 +153,7 @@ def gtstr(cls, ins: Quad) -> list[str]:
"""Compares & pops top 2 strings out of the stack.
Temporal values are freed from memory. (a$ > b$)
"""
- (tmp1, tmp2, output) = cls.get_oper(ins[2], ins[3])
+ _, _, output = cls.get_oper(ins[2], ins[3])
output.append(runtime_call(RuntimeLabels.STRGT))
output.append("push af")
return output
@@ -166,7 +163,7 @@ def lestr(cls, ins: Quad) -> list[str]:
"""Compares & pops top 2 strings out of the stack.
Temporal values are freed from memory. (a$ <= b$)
"""
- (tmp1, tmp2, output) = cls.get_oper(ins[2], ins[3])
+ _, _, output = cls.get_oper(ins[2], ins[3])
output.append(runtime_call(RuntimeLabels.STRLE))
output.append("push af")
return output
@@ -176,7 +173,7 @@ def gestr(cls, ins: Quad) -> list[str]:
"""Compares & pops top 2 strings out of the stack.
Temporal values are freed from memory. (a$ >= b$)
"""
- (tmp1, tmp2, output) = cls.get_oper(ins[2], ins[3])
+ _, _, output = cls.get_oper(ins[2], ins[3])
output.append(runtime_call(RuntimeLabels.STRGE))
output.append("push af")
return output
@@ -186,7 +183,7 @@ def eqstr(cls, ins: Quad) -> list[str]:
"""Compares & pops top 2 strings out of the stack.
Temporal values are freed from memory. (a$ == b$)
"""
- (tmp1, tmp2, output) = cls.get_oper(ins[2], ins[3])
+ _, _, output = cls.get_oper(ins[2], ins[3])
output.append(runtime_call(RuntimeLabels.STREQ))
output.append("push af")
return output
@@ -196,7 +193,7 @@ def nestr(cls, ins: Quad) -> list[str]:
"""Compares & pops top 2 strings out of the stack.
Temporal values are freed from memory. (a$ != b$)
"""
- (tmp1, tmp2, output) = cls.get_oper(ins[2], ins[3])
+ _, _, output = cls.get_oper(ins[2], ins[3])
output.append(runtime_call(RuntimeLabels.STRNE))
output.append("push af")
return output
@@ -204,7 +201,7 @@ def nestr(cls, ins: Quad) -> list[str]:
@classmethod
def lenstr(cls, ins: Quad) -> list[str]:
"""Returns string length"""
- (tmp1, output) = cls.get_oper(ins[2], no_exaf=True)
+ tmp1, output = cls.get_oper(ins[2], no_exaf=True)
if tmp1:
output.append("push hl")
@@ -246,7 +243,7 @@ def storestr(cls, ins: Quad) -> list[str]:
if not indirect:
op1 = "#" + op1
- tmp1, tmp2, output = cls.get_oper(op1, ins[2], no_exaf=True)
+ _, tmp2, output = cls.get_oper(op1, ins[2], no_exaf=True)
if not tmp2:
output.append(runtime_call(RuntimeLabel.STORE_STR))
@@ -329,7 +326,7 @@ def paramstr(cls, ins: Quad) -> list[str]:
to a string. For indirect values, it will push
the pointer to the pointer :-)
"""
- (tmp, output) = cls.get_oper(ins[1])
+ tmp, output = cls.get_oper(ins[1])
output.pop() # Remove a register flag (useless here)
tmp = ins[1][0] in ("#", "_") # Determine if the string must be duplicated
@@ -346,6 +343,6 @@ def fparamstr(cls, ins: Quad) -> list[str]:
value, or by loading it from memory (indirect)
or directly (immediate) --prefixed with '#'--
"""
- (tmp1, output) = cls.get_oper(ins[1])
+ _, output = cls.get_oper(ins[1])
return output
diff --git a/src/arch/z80/backend/common.py b/src/arch/z80/backend/common.py
index b49f0e3f3..c6eb7a969 100644
--- a/src/arch/z80/backend/common.py
+++ b/src/arch/z80/backend/common.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import math
import re
from enum import StrEnum
@@ -378,19 +385,35 @@ def to_long(stype: DataType) -> list[str]:
)
return output
- if stype in {I8_t, U8_t, F16_t}: # Byte to word
+ if stype in (I8_t, U8_t): # Byte to word
output = to_word(stype)
+ output.append("ld e, h")
+ output.append("ld d, h")
- if stype != F16_t: # If it's a byte, just copy H to D,E
- output.append("ld e, h")
- output.append("ld d, h")
+ elif stype == I16_t: # Signed byte or fixed to word
+ output.extend(
+ [
+ "ld a, h",
+ "add a, a",
+ "sbc a, a",
+ "ld e, a",
+ "ld d, a",
+ ]
+ )
- elif stype in (I16_t, F16_t): # Signed byte or fixed to word
- output.append("ld a, h")
- output.append("add a, a")
- output.append("sbc a, a")
- output.append("ld e, a")
- output.append("ld d, a")
+ elif stype == F16_t:
+ output.extend(
+ [
+ "ex de, hl",
+ "ld de, 0",
+ # Copies the highest bit (sign) to DE
+ "ld a, h",
+ "add a, a",
+ "sbc a, a",
+ "ld e, a",
+ "ld d, a",
+ ]
+ )
elif stype in (U32_t, I32_t):
return []
diff --git a/src/arch/z80/backend/exception.py b/src/arch/z80/backend/exception.py
index 47845e5a9..2a4524411 100644
--- a/src/arch/z80/backend/exception.py
+++ b/src/arch/z80/backend/exception.py
@@ -1,4 +1,9 @@
-# vim:ts=4:et:sw=4:
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from src.api.exception import Error
diff --git a/src/arch/z80/backend/generic.py b/src/arch/z80/backend/generic.py
index 0f02a1efe..08bb9286e 100644
--- a/src/arch/z80/backend/generic.py
+++ b/src/arch/z80/backend/generic.py
@@ -1,6 +1,9 @@
-# ----------------------------------------------------------
-# Generic instructions
-# ----------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
import re
@@ -342,7 +345,7 @@ def _cast(ins: Quad):
xsB = sB = YY_TYPES[tB] # Type sizes
if tA in ("u8", "i8") and tB == "bool":
- return [] # bytes are booleans already (0 = False, not 0 = True)
+ return [*Bits8.get_oper(ins[4]), "push af"] # bytes are booleans already (0 = False, not 0 = True)
output = []
if tA in ("u8", "i8", "bool"):
diff --git a/src/arch/z80/backend/icinfo.py b/src/arch/z80/backend/icinfo.py
index a903dd378..84b462e62 100644
--- a/src/arch/z80/backend/icinfo.py
+++ b/src/arch/z80/backend/icinfo.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from collections.abc import Callable
from dataclasses import dataclass
diff --git a/src/arch/z80/backend/icinstruction.py b/src/arch/z80/backend/icinstruction.py
index 2ae2c19be..0b9ed3d5b 100644
--- a/src/arch/z80/backend/icinstruction.py
+++ b/src/arch/z80/backend/icinstruction.py
@@ -1,14 +1,21 @@
-from enum import StrEnum
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from functools import lru_cache
from typing import Any
+from src.arch.interface.icinstruction import ICInstruction as ICInstructionInterface
from src.arch.z80.backend.exception import InvalidICError
__all__ = ("ICInstruction",)
# HINT: Do not use Enums here. They cannot be subclassed
-class ICInstruction(StrEnum):
+class ICInstruction(ICInstructionInterface):
ADDU8 = "addu8"
ADDI8 = "addi8"
ADDI16 = "addi16"
diff --git a/src/arch/z80/backend/main.py b/src/arch/z80/backend/main.py
index 2a484713d..f662186c9 100644
--- a/src/arch/z80/backend/main.py
+++ b/src/arch/z80/backend/main.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from __future__ import annotations
import re
diff --git a/src/arch/z80/backend/quad.py b/src/arch/z80/backend/quad.py
index 2c231fb7d..c774e309a 100644
--- a/src/arch/z80/backend/quad.py
+++ b/src/arch/z80/backend/quad.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.arch.interface.quad import Quad as BaseQuad
from .icinstruction import ICInstruction
diff --git a/src/arch/z80/backend/runtime/__init__.py b/src/arch/z80/backend/runtime/__init__.py
index 988edfd4a..6cef2a69d 100644
--- a/src/arch/z80/backend/runtime/__init__.py
+++ b/src/arch/z80/backend/runtime/__init__.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from .labels import LABEL_REQUIRED_MODULES # noqa
from .labels import RUNTIME_LABELS # noqa
from .labels import Labels # noqa
diff --git a/src/arch/z80/backend/runtime/core.py b/src/arch/z80/backend/runtime/core.py
index 3dbd2780d..554e1ce93 100644
--- a/src/arch/z80/backend/runtime/core.py
+++ b/src/arch/z80/backend/runtime/core.py
@@ -1,4 +1,9 @@
-# Runtime labels
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from .namespace import NAMESPACE
diff --git a/src/arch/z80/backend/runtime/datarestore.py b/src/arch/z80/backend/runtime/datarestore.py
index 7b7817bac..9e37bc0a4 100644
--- a/src/arch/z80/backend/runtime/datarestore.py
+++ b/src/arch/z80/backend/runtime/datarestore.py
@@ -1,4 +1,9 @@
-# Runtime labels
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from .namespace import NAMESPACE
diff --git a/src/arch/z80/backend/runtime/io.py b/src/arch/z80/backend/runtime/io.py
index 1a8b36789..90caa1619 100644
--- a/src/arch/z80/backend/runtime/io.py
+++ b/src/arch/z80/backend/runtime/io.py
@@ -1,5 +1,9 @@
-# I/O labels
-
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from .namespace import NAMESPACE
diff --git a/src/arch/z80/backend/runtime/labels.py b/src/arch/z80/backend/runtime/labels.py
index b9359048d..5e7a4c5bb 100644
--- a/src/arch/z80/backend/runtime/labels.py
+++ b/src/arch/z80/backend/runtime/labels.py
@@ -1,4 +1,10 @@
-# Runtime Labels
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from typing import Final
from . import core, datarestore, io, math, misc, random
diff --git a/src/arch/z80/backend/runtime/math.py b/src/arch/z80/backend/runtime/math.py
index e91bcb435..5bedf47e7 100644
--- a/src/arch/z80/backend/runtime/math.py
+++ b/src/arch/z80/backend/runtime/math.py
@@ -1,4 +1,9 @@
-# Runtime labels
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from .namespace import NAMESPACE
diff --git a/src/arch/z80/backend/runtime/misc.py b/src/arch/z80/backend/runtime/misc.py
index ed46d9ade..f6b36d061 100644
--- a/src/arch/z80/backend/runtime/misc.py
+++ b/src/arch/z80/backend/runtime/misc.py
@@ -1,4 +1,9 @@
-# Miscelaneous functions
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from .namespace import NAMESPACE
diff --git a/src/arch/z80/backend/runtime/namespace.py b/src/arch/z80/backend/runtime/namespace.py
index 142bf53b9..2c53420ec 100644
--- a/src/arch/z80/backend/runtime/namespace.py
+++ b/src/arch/z80/backend/runtime/namespace.py
@@ -1,4 +1,9 @@
-# Define just the main Private namespace
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from src.api import global_
diff --git a/src/arch/z80/backend/runtime/random.py b/src/arch/z80/backend/runtime/random.py
index e83bb2f79..fbe496368 100644
--- a/src/arch/z80/backend/runtime/random.py
+++ b/src/arch/z80/backend/runtime/random.py
@@ -1,4 +1,9 @@
-# Runtime labels
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from .namespace import NAMESPACE
diff --git a/src/arch/z80/beep.py b/src/arch/z80/beep.py
index cf4b646be..b2077d142 100755
--- a/src/arch/z80/beep.py
+++ b/src/arch/z80/beep.py
@@ -1,5 +1,11 @@
#!/usr/bin/env python3
-# vim:ts=4:et:sw=4:
+
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
__doc___ = """This stdlib converts duration,pitch for a beep
from floating point to HL,DE Integers.
diff --git a/src/arch/z80/optimizer/__init__.py b/src/arch/z80/optimizer/__init__.py
index 91259bc01..912fe0e82 100644
--- a/src/arch/z80/optimizer/__init__.py
+++ b/src/arch/z80/optimizer/__init__.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from .main import Optimizer
__all__ = ("Optimizer",)
diff --git a/src/arch/z80/optimizer/asm.py b/src/arch/z80/optimizer/asm.py
index 9743de045..640d6e882 100644
--- a/src/arch/z80/optimizer/asm.py
+++ b/src/arch/z80/optimizer/asm.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import re
from functools import lru_cache
diff --git a/src/arch/z80/optimizer/basicblock.py b/src/arch/z80/optimizer/basicblock.py
index 5c9320aa5..5c7480c14 100644
--- a/src/arch/z80/optimizer/basicblock.py
+++ b/src/arch/z80/optimizer/basicblock.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from __future__ import annotations
from collections.abc import Iterable, Iterator, Sequence
@@ -439,7 +446,7 @@ def optimize(self, patterns_list):
old_unary = dict(evaluator.UNARY)
# monkey-patches some functions in this optimizer level (> 2)
- evaluator.UNARY[FN.GVAL] = lambda x: self.cpu.get(x)
+ evaluator.UNARY[FN.GVAL] = self.cpu.get
evaluator.UNARY[FN.FLAGVAL] = lambda x: {
"c": str(self.cpu.C) if self.cpu.C is not None else new_tmp_val(),
"z": str(self.cpu.Z) if self.cpu.Z is not None else new_tmp_val(),
diff --git a/src/arch/z80/optimizer/cpustate.py b/src/arch/z80/optimizer/cpustate.py
index 8e845a533..a96e46207 100644
--- a/src/arch/z80/optimizer/cpustate.py
+++ b/src/arch/z80/optimizer/cpustate.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import re
from collections import UserDict
from collections.abc import Mapping
diff --git a/src/arch/z80/optimizer/errors.py b/src/arch/z80/optimizer/errors.py
index 8dc95e67d..a146f482f 100644
--- a/src/arch/z80/optimizer/errors.py
+++ b/src/arch/z80/optimizer/errors.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.api.exception import Error
diff --git a/src/arch/z80/optimizer/flow_graph.py b/src/arch/z80/optimizer/flow_graph.py
index f76d2451d..011e665c3 100644
--- a/src/arch/z80/optimizer/flow_graph.py
+++ b/src/arch/z80/optimizer/flow_graph.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.api.debug import __DEBUG__
from .basicblock import BasicBlock, DummyBasicBlock
diff --git a/src/arch/z80/optimizer/helpers.py b/src/arch/z80/optimizer/helpers.py
index b2a427bca..ececec619 100644
--- a/src/arch/z80/optimizer/helpers.py
+++ b/src/arch/z80/optimizer/helpers.py
@@ -1,5 +1,12 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from collections.abc import Iterable, Mapping
-from typing import Any, Final, TypeVar, cast
+from typing import Any, Final, cast
from . import patterns
@@ -41,10 +48,6 @@
)
-T = TypeVar("T")
-K = TypeVar("K")
-
-
# All 'single' registers (even f FLAG one). SP is not decomposable so it's 'single' already
ALL_REGS: Final[frozenset[str]] = frozenset(
[
@@ -486,7 +489,7 @@ def HI16_val(x: int | str | None) -> str:
return f"0{HL_SEP}{x}".split(HL_SEP)[-2]
-def dict_intersection(dict_a: Mapping[K, T], dict_b: Mapping[K, T]) -> dict[K, T]:
+def dict_intersection[K, T](dict_a: Mapping[K, T], dict_b: Mapping[K, T]) -> dict[K, T]:
"""Given 2 dictionaries a, b, returns a new one which contains the common key/pair values.
e.g. for {'a': 1, 'b': 'x'}, {'a': 'q', 'b': 'x', 'c': 2} returns {'b': 'x'}
diff --git a/src/arch/z80/optimizer/labelinfo.py b/src/arch/z80/optimizer/labelinfo.py
index 96b3e0dc1..85173d3e2 100644
--- a/src/arch/z80/optimizer/labelinfo.py
+++ b/src/arch/z80/optimizer/labelinfo.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from __future__ import annotations
from dataclasses import dataclass, field
diff --git a/src/arch/z80/optimizer/labels_dict.py b/src/arch/z80/optimizer/labels_dict.py
index 4849966c4..6d3ad7a26 100644
--- a/src/arch/z80/optimizer/labels_dict.py
+++ b/src/arch/z80/optimizer/labels_dict.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from collections import UserDict
from .errors import DuplicatedLabelError
diff --git a/src/arch/z80/optimizer/main.py b/src/arch/z80/optimizer/main.py
index 3283ac0f7..cb4d69d98 100644
--- a/src/arch/z80/optimizer/main.py
+++ b/src/arch/z80/optimizer/main.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from collections import defaultdict
from src.api.config import OPTIONS
diff --git a/src/arch/z80/optimizer/memcell.py b/src/arch/z80/optimizer/memcell.py
index 8ec29cf90..0a5611f60 100644
--- a/src/arch/z80/optimizer/memcell.py
+++ b/src/arch/z80/optimizer/memcell.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import re
from functools import cached_property
diff --git a/src/arch/z80/optimizer/patterns.py b/src/arch/z80/optimizer/patterns.py
index 0efb09544..a3d9f1259 100644
--- a/src/arch/z80/optimizer/patterns.py
+++ b/src/arch/z80/optimizer/patterns.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import re
__doc__ = """ Patterns to match against generated ASM code.
diff --git a/src/arch/z80/peephole/__init__.py b/src/arch/z80/peephole/__init__.py
index e69de29bb..906667a8d 100644
--- a/src/arch/z80/peephole/__init__.py
+++ b/src/arch/z80/peephole/__init__.py
@@ -0,0 +1,6 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
diff --git a/src/arch/z80/peephole/engine.py b/src/arch/z80/peephole/engine.py
index abb56d743..484c6a2b5 100644
--- a/src/arch/z80/peephole/engine.py
+++ b/src/arch/z80/peephole/engine.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import os
from collections.abc import Iterable
from typing import NamedTuple
@@ -70,7 +77,7 @@ def read_opt(opt_path: str) -> OptPattern | None:
errmsg.warning(define_.lineno, "this template will be ignored", fpath)
return None
- except (ValueError, KeyError, TypeError):
+ except ValueError, KeyError, TypeError:
errmsg.warning(1, "There is an error in this template and it will be ignored", fpath)
else:
MAXLEN = max(len(pattern_.patt), MAXLEN or 0)
@@ -88,7 +95,7 @@ def read_opts(folder_path: str, result: list[OptPattern] | None = None) -> list[
try:
files_to_read = [f for f in os.listdir(folder_path) if f.endswith(".opt")]
- except (FileNotFoundError, NotADirectoryError, PermissionError):
+ except FileNotFoundError, NotADirectoryError, PermissionError:
return result
for fname in files_to_read:
diff --git a/src/arch/z80/peephole/evaluator.py b/src/arch/z80/peephole/evaluator.py
index 7234841bb..c01ebf02f 100644
--- a/src/arch/z80/peephole/evaluator.py
+++ b/src/arch/z80/peephole/evaluator.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from __future__ import annotations
import re
@@ -231,7 +238,7 @@ def eval(self, vars_: dict[str, Any] | None = None) -> str | Evaluator | list[An
try:
oper = FN(self.expression[0])
assert oper in UNARY
- except (AssertionError, ValueError):
+ except AssertionError, ValueError:
raise ValueError(f"Invalid unary operator '{self.expression[0]}'")
operand = self.expression[1].eval(vars_)
@@ -241,7 +248,7 @@ def eval(self, vars_: dict[str, Any] | None = None) -> str | Evaluator | list[An
try:
oper = FN(self.expression[1])
assert oper in BINARY
- except (AssertionError, ValueError):
+ except AssertionError, ValueError:
raise ValueError(f"Invalid binary operator '{self.expression[1]}'")
# Do lazy evaluation
diff --git a/src/arch/z80/peephole/opts/000_o1_push_pop.opt b/src/arch/z80/peephole/opts/000_o1_push_pop.opt
index ea9da4ffe..c1d370da5 100644
--- a/src/arch/z80/peephole/opts/000_o1_push_pop.opt
+++ b/src/arch/z80/peephole/opts/000_o1_push_pop.opt
@@ -1,14 +1,14 @@
-;; Remove sequence:
-;; push xx
-;; pop xx
-
-OLEVEL: 1
-OFLAG: 1
-
-REPLACE {{
- push $1
- pop $1
-}}
-
-WITH {{
-}}
+;; Remove sequence:
+;; push xx
+;; pop xx
+
+OLEVEL: 1
+OFLAG: 1
+
+REPLACE {{
+ push $1
+ pop $1
+}}
+
+WITH {{
+}}
diff --git a/src/arch/z80/peephole/opts/000_o3_push_pop.opt b/src/arch/z80/peephole/opts/000_o3_push_pop.opt
index 23435c3ad..ed00d9c15 100644
--- a/src/arch/z80/peephole/opts/000_o3_push_pop.opt
+++ b/src/arch/z80/peephole/opts/000_o3_push_pop.opt
@@ -1,14 +1,14 @@
-;; Remove sequence:
-;; push xx
-;; pop xx
-
-OLEVEL: 3
-OFLAG: 1
-
-REPLACE {{
- push $1
- pop $1
-}}
-
-WITH {{
-}}
+;; Remove sequence:
+;; push xx
+;; pop xx
+
+OLEVEL: 3
+OFLAG: 1
+
+REPLACE {{
+ push $1
+ pop $1
+}}
+
+WITH {{
+}}
diff --git a/src/arch/z80/peephole/opts/001_o1_ld_XXYY_ldYYXX.opt b/src/arch/z80/peephole/opts/001_o1_ld_XXYY_ldYYXX.opt
index 52e8bd562..895da86c7 100644
--- a/src/arch/z80/peephole/opts/001_o1_ld_XXYY_ldYYXX.opt
+++ b/src/arch/z80/peephole/opts/001_o1_ld_XXYY_ldYYXX.opt
@@ -1,18 +1,18 @@
-;; Replaces sequence:
-;; ld XX, YY
-;; ld YY, XX ;; This is useless
-
-;; With:
-;; ld XX, YY
-
-OLEVEL: 1
-OFLAG: 2
-
-REPLACE {{
- ld $1, $2
- ld $2, $1
-}}
-
-WITH {{
- ld $1, $2
-}}
+;; Replaces sequence:
+;; ld XX, YY
+;; ld YY, XX ;; This is useless
+
+;; With:
+;; ld XX, YY
+
+OLEVEL: 1
+OFLAG: 2
+
+REPLACE {{
+ ld $1, $2
+ ld $2, $1
+}}
+
+WITH {{
+ ld $1, $2
+}}
diff --git a/src/arch/z80/peephole/opts/002_o1_push_hlde_to_ex.opt b/src/arch/z80/peephole/opts/002_o1_push_hlde_to_ex.opt
index 0c491ad92..3964d8415 100644
--- a/src/arch/z80/peephole/opts/002_o1_push_hlde_to_ex.opt
+++ b/src/arch/z80/peephole/opts/002_o1_push_hlde_to_ex.opt
@@ -1,26 +1,26 @@
-;; Replaces sequence:
-;; push de
-;; push hl
-;; pop de
-;; pop hl
-
-;; with
-;; ex de, hl
-
-OLEVEL: 1
-OFLAG: 3
-
-REPLACE {{
- push $1
- push $2
- pop $1
- pop $2
-}}
-
-IF {{
- (($1 == hl) && ($2 == de)) || (($1 == de) && ($2 == hl))
-}}
-
-WITH {{
- ex de, hl
+;; Replaces sequence:
+;; push de
+;; push hl
+;; pop de
+;; pop hl
+
+;; with
+;; ex de, hl
+
+OLEVEL: 1
+OFLAG: 3
+
+REPLACE {{
+ push $1
+ push $2
+ pop $1
+ pop $2
+}}
+
+IF {{
+ (($1 == hl) && ($2 == de)) || (($1 == de) && ($2 == hl))
+}}
+
+WITH {{
+ ex de, hl
}}
\ No newline at end of file
diff --git a/src/arch/z80/peephole/opts/003_o1_useless_jp.opt b/src/arch/z80/peephole/opts/003_o1_useless_jp.opt
index ab9eadddb..2e3131fb7 100644
--- a/src/arch/z80/peephole/opts/003_o1_useless_jp.opt
+++ b/src/arch/z80/peephole/opts/003_o1_useless_jp.opt
@@ -1,18 +1,18 @@
-;; Replaces sequence:
-;; jp XXX
-;; XXX:
-;; with
-;; XXX:
-;; (jp is useless here)
-
-OLEVEL: 1
-OFLAG: 4
-
-REPLACE {{
- jp$1 $2
- $2:
-}}
-
-WITH {{
- $2:
+;; Replaces sequence:
+;; jp XXX
+;; XXX:
+;; with
+;; XXX:
+;; (jp is useless here)
+
+OLEVEL: 1
+OFLAG: 4
+
+REPLACE {{
+ jp$1 $2
+ $2:
+}}
+
+WITH {{
+ $2:
}}
\ No newline at end of file
diff --git a/src/arch/z80/peephole/opts/004_o1_push_pop_ldld.opt b/src/arch/z80/peephole/opts/004_o1_push_pop_ldld.opt
index a1f78caed..85b149097 100644
--- a/src/arch/z80/peephole/opts/004_o1_push_pop_ldld.opt
+++ b/src/arch/z80/peephole/opts/004_o1_push_pop_ldld.opt
@@ -1,27 +1,27 @@
-;; Replaces sequence:
-;; push hl
-;; pop de
-;; pop hl | ld hl, XX
-;; with
-;; ex de, hl
-;; pop hl | ld hl, XX
-;; (also reverse order allowed)
-
-OLEVEL: 1
-OFLAG: 5
-
-REPLACE {{
- ld d, h
- ld e, l
- $1 hl$2
-}}
-
-IF {{
- ($1 == pop) || ($1 == ld)
-}}
-
-WITH {{
- ex de, hl
- $1 hl$2
-}}
-
+;; Replaces sequence:
+;; push hl
+;; pop de
+;; pop hl | ld hl, XX
+;; with
+;; ex de, hl
+;; pop hl | ld hl, XX
+;; (also reverse order allowed)
+
+OLEVEL: 1
+OFLAG: 5
+
+REPLACE {{
+ ld d, h
+ ld e, l
+ $1 hl$2
+}}
+
+IF {{
+ ($1 == pop) || ($1 == ld)
+}}
+
+WITH {{
+ ex de, hl
+ $1 hl$2
+}}
+
diff --git a/src/arch/z80/peephole/opts/005_o1_push_af_ld_aX.opt b/src/arch/z80/peephole/opts/005_o1_push_af_ld_aX.opt
index 484cf5945..cfc19bd71 100644
--- a/src/arch/z80/peephole/opts/005_o1_push_af_ld_aX.opt
+++ b/src/arch/z80/peephole/opts/005_o1_push_af_ld_aX.opt
@@ -1,23 +1,23 @@
-;; Replaces sequence:
-;; ld a, (ix|iy +|- NNN)
-;; inc|dec|cp a
-;; ld (ix|iy +|- NNN), a
-;; with
-;; inc|dec|cp (ix|iy +|- NNN)
-
-OLEVEL: 1
-OFLAG: 6
-
-REPLACE {{
- ld a, $1
- $2 a
- ld $1, a
-}}
-
-IF {{
- (IS_INDIR($1) || ($1 == "(hl)")) && (($2 == cp) || ($2 == inc) || ($2 == dec))
-}}
-
-WITH {{
- $2 $1
-}}
+;; Replaces sequence:
+;; ld a, (ix|iy +|- NNN)
+;; inc|dec|cp a
+;; ld (ix|iy +|- NNN), a
+;; with
+;; inc|dec|cp (ix|iy +|- NNN)
+
+OLEVEL: 1
+OFLAG: 6
+
+REPLACE {{
+ ld a, $1
+ $2 a
+ ld $1, a
+}}
+
+IF {{
+ (IS_INDIR($1) || ($1 == "(hl)")) && (($2 == cp) || ($2 == inc) || ($2 == dec))
+}}
+
+WITH {{
+ $2 $1
+}}
diff --git a/src/arch/z80/peephole/opts/006_o1_push_af_pop_XX.opt b/src/arch/z80/peephole/opts/006_o1_push_af_pop_XX.opt
index a28692209..f85f844fd 100644
--- a/src/arch/z80/peephole/opts/006_o1_push_af_pop_XX.opt
+++ b/src/arch/z80/peephole/opts/006_o1_push_af_pop_XX.opt
@@ -1,27 +1,27 @@
-;; Replaces sequence:
-;; push af
-;; pop XY
-;; with
-;; ld X, a
-;; (also reverse order allowed)
-
-OLEVEL: 1
-OFLAG: 7
-
-REPLACE {{
- push $1
- pop $2
-}}
-
-IF {{
- ($1 <> $2) && (($1 == af) || ($2 == af))
-}}
-
-DEFINE {{
- $3 = HIREG($1)
- $4 = HIREG($2)
-}}
-
-WITH {{
- ld $4, $3
-}}
+;; Replaces sequence:
+;; push af
+;; pop XY
+;; with
+;; ld X, a
+;; (also reverse order allowed)
+
+OLEVEL: 1
+OFLAG: 7
+
+REPLACE {{
+ push $1
+ pop $2
+}}
+
+IF {{
+ ($1 <> $2) && (($1 == af) || ($2 == af))
+}}
+
+DEFINE {{
+ $3 = HIREG($1)
+ $4 = HIREG($2)
+}}
+
+WITH {{
+ ld $4, $3
+}}
diff --git a/src/arch/z80/peephole/opts/006_o3_push_af_pop_XX.opt b/src/arch/z80/peephole/opts/006_o3_push_af_pop_XX.opt
index 6b2cde034..7ead1848b 100644
--- a/src/arch/z80/peephole/opts/006_o3_push_af_pop_XX.opt
+++ b/src/arch/z80/peephole/opts/006_o3_push_af_pop_XX.opt
@@ -1,27 +1,27 @@
-;; Replaces sequence:
-;; push af
-;; pop XY
-;; with
-;; ld X, a
-;; (also reverse order allowed)
-
-OLEVEL: 3
-OFLAG: 7
-
-REPLACE {{
- push $1
- pop $2
-}}
-
-IF {{
- ($1 <> $2) && (($1 == af) || ($2 == af))
-}}
-
-DEFINE {{
- $3 = HIREG($1)
- $4 = HIREG($2)
-}}
-
-WITH {{
- ld $4, $3
-}}
+;; Replaces sequence:
+;; push af
+;; pop XY
+;; with
+;; ld X, a
+;; (also reverse order allowed)
+
+OLEVEL: 3
+OFLAG: 7
+
+REPLACE {{
+ push $1
+ pop $2
+}}
+
+IF {{
+ ($1 <> $2) && (($1 == af) || ($2 == af))
+}}
+
+DEFINE {{
+ $3 = HIREG($1)
+ $4 = HIREG($2)
+}}
+
+WITH {{
+ ld $4, $3
+}}
diff --git a/src/arch/z80/peephole/opts/007_o1_ex_de_hl.opt b/src/arch/z80/peephole/opts/007_o1_ex_de_hl.opt
index 64bfd7aec..d0494bc02 100644
--- a/src/arch/z80/peephole/opts/007_o1_ex_de_hl.opt
+++ b/src/arch/z80/peephole/opts/007_o1_ex_de_hl.opt
@@ -1,16 +1,16 @@
-;; Remove sequence:
-;; ex de, hl
-;; ex de, hl
-;;
-;; Also works with ex af, af' and exx
-
-OLEVEL: 1
-OFLAG: 8
-
-REPLACE {{
- ex$1
- ex$1
-}}
-
-WITH {{
-}}
+;; Remove sequence:
+;; ex de, hl
+;; ex de, hl
+;;
+;; Also works with ex af, af' and exx
+
+OLEVEL: 1
+OFLAG: 8
+
+REPLACE {{
+ ex$1
+ ex$1
+}}
+
+WITH {{
+}}
diff --git a/src/arch/z80/peephole/opts/008_o1_sbc_jp.opt b/src/arch/z80/peephole/opts/008_o1_sbc_jp.opt
index c3ed14eaf..2af5c2b95 100644
--- a/src/arch/z80/peephole/opts/008_o1_sbc_jp.opt
+++ b/src/arch/z80/peephole/opts/008_o1_sbc_jp.opt
@@ -1,28 +1,28 @@
-;; Replace sequence:
-;; sbc a, a
-;; or a
-;; jp z, __X
-;;
-;; With:
-;; jp nc, __X
-
-OLEVEL: 1
-OFLAG: 9
-
-REPLACE {{
- sbc a, a
- or a
- $1 $2, $3
-}}
-
-DEFINE {{
- $4 = (($2 == "z") && "nc") || ("c")
-}}
-
-IF {{
- (($1 == "jp") || ($1 == "jr"))
-}}
-
-WITH {{
- $1 $4, $3
-}}
+;; Replace sequence:
+;; sbc a, a
+;; or a
+;; jp z, __X
+;;
+;; With:
+;; jp nc, __X
+
+OLEVEL: 1
+OFLAG: 9
+
+REPLACE {{
+ sbc a, a
+ or a
+ $1 $2, $3
+}}
+
+DEFINE {{
+ $4 = (($2 == "z") && "nc") || ("c")
+}}
+
+IF {{
+ (($1 == "jp") || ($1 == "jr"))
+}}
+
+WITH {{
+ $1 $4, $3
+}}
diff --git a/src/arch/z80/peephole/opts/009_o1_inc_mem.opt b/src/arch/z80/peephole/opts/009_o1_inc_mem.opt
index a49d185e6..fbeee997a 100644
--- a/src/arch/z80/peephole/opts/009_o1_inc_mem.opt
+++ b/src/arch/z80/peephole/opts/009_o1_inc_mem.opt
@@ -1,30 +1,30 @@
-;; Replace sequence:
-;; ld a, (xxx)
-;; inc a | dec a
-;; ld (xxx), a
-;;
-;; With:
-;; ld hl, xxx
-;; inc (hl) | dec (hl)
-
-OLEVEL: 1
-OFLAG: 10
-
-REPLACE {{
- ld a, ($1)
- $2 a
- ld ($1), a
-}}
-
-DEFINE {{
- $3 = "(" + $1 + ")"
-}}
-
-IF {{
- (($2 == "inc") || ($2 == "dec")) && (($1 <> "hl") && (!IS_INDIR($3)))
-}}
-
-WITH {{
- ld hl, $1
- $2 (hl)
-}}
+;; Replace sequence:
+;; ld a, (xxx)
+;; inc a | dec a
+;; ld (xxx), a
+;;
+;; With:
+;; ld hl, xxx
+;; inc (hl) | dec (hl)
+
+OLEVEL: 1
+OFLAG: 10
+
+REPLACE {{
+ ld a, ($1)
+ $2 a
+ ld ($1), a
+}}
+
+DEFINE {{
+ $3 = "(" + $1 + ")"
+}}
+
+IF {{
+ (($2 == "inc") || ($2 == "dec")) && (($1 <> "hl") && (!IS_INDIR($3)))
+}}
+
+WITH {{
+ ld hl, $1
+ $2 (hl)
+}}
diff --git a/src/arch/z80/peephole/opts/010_o1_ld_de_hl_ex_de_hl.opt b/src/arch/z80/peephole/opts/010_o1_ld_de_hl_ex_de_hl.opt
index 4f29f3eac..efc9def61 100644
--- a/src/arch/z80/peephole/opts/010_o1_ld_de_hl_ex_de_hl.opt
+++ b/src/arch/z80/peephole/opts/010_o1_ld_de_hl_ex_de_hl.opt
@@ -1,33 +1,33 @@
-;; Replaces sequence:
-;; push AB
-;; pop CD
-;; With:
-;; ld C, A
-;; ld B, D
-
-OLEVEL: 1
-OFLAG: 11
-
-REPLACE {{
- push $1
- pop $2
-}}
-
-DEFINE {{
- $3 = HIREG($1)
- $4 = LOREG($1)
- $5 = HIREG($2)
- $6 = LOREG($2)
-}}
-
-IF {{
- ($1 <> $2) &&
- (($1 == hl) || ($1 == de) || ($1 == bc)) &&
- (($2 == hl) || ($2 == de) || ($2 == bc))
-}}
-
-WITH {{
- ld $5, $3
- ld $6, $4
-}}
-
+;; Replaces sequence:
+;; push AB
+;; pop CD
+;; With:
+;; ld C, A
+;; ld B, D
+
+OLEVEL: 1
+OFLAG: 11
+
+REPLACE {{
+ push $1
+ pop $2
+}}
+
+DEFINE {{
+ $3 = HIREG($1)
+ $4 = LOREG($1)
+ $5 = HIREG($2)
+ $6 = LOREG($2)
+}}
+
+IF {{
+ ($1 <> $2) &&
+ (($1 == hl) || ($1 == de) || ($1 == bc)) &&
+ (($2 == hl) || ($2 == de) || ($2 == bc))
+}}
+
+WITH {{
+ ld $5, $3
+ ld $6, $4
+}}
+
diff --git a/src/arch/z80/peephole/opts/011_o1_ld_h_a_pop_af_or_h.opt b/src/arch/z80/peephole/opts/011_o1_ld_h_a_pop_af_or_h.opt
index 85c8e810d..ea8f660c6 100644
--- a/src/arch/z80/peephole/opts/011_o1_ld_h_a_pop_af_or_h.opt
+++ b/src/arch/z80/peephole/opts/011_o1_ld_h_a_pop_af_or_h.opt
@@ -1,27 +1,27 @@
-;; Replaces sequence:
-;; ld h, a
-;; pop af
-;; or h | and h | xor h
-;; With:
-;; pop de
-;; or d | and d | xor d
-;;
-;; At O1 Level, this happens only with 8 bit operations between h and a
-
-OLEVEL: 1
-OFLAG: 12
-
-REPLACE {{
- ld h, a
- pop af
- $1 h
-}}
-
-IF {{
- ($1 IN (or, and, xor))
-}}
-
-WITH {{
- pop de
- $1 d
-}}
+;; Replaces sequence:
+;; ld h, a
+;; pop af
+;; or h | and h | xor h
+;; With:
+;; pop de
+;; or d | and d | xor d
+;;
+;; At O1 Level, this happens only with 8 bit operations between h and a
+
+OLEVEL: 1
+OFLAG: 12
+
+REPLACE {{
+ ld h, a
+ pop af
+ $1 h
+}}
+
+IF {{
+ ($1 IN (or, and, xor))
+}}
+
+WITH {{
+ pop de
+ $1 d
+}}
diff --git a/src/arch/z80/peephole/opts/012_o1_ld_hl_push_pop_de.opt b/src/arch/z80/peephole/opts/012_o1_ld_hl_push_pop_de.opt
index 5b5722ff0..5b438789a 100644
--- a/src/arch/z80/peephole/opts/012_o1_ld_hl_push_pop_de.opt
+++ b/src/arch/z80/peephole/opts/012_o1_ld_hl_push_pop_de.opt
@@ -1,27 +1,27 @@
-;; Replaces sequence:
-;; ld hl, XX
-;; push hl
-;; ld hl, YY
-;; pop de
-;; with
-;; ld de, XX
-;; ld hl, YY
-
-OLEVEL: 1
-OFLAG: 13
-
-REPLACE {{
- ld hl, $1
- push hl
- ld hl, $2
- pop de
-}}
-
-IF {{
-}}
-
-WITH {{
- ld de, $1
- ld hl, $2
-}}
-
+;; Replaces sequence:
+;; ld hl, XX
+;; push hl
+;; ld hl, YY
+;; pop de
+;; with
+;; ld de, XX
+;; ld hl, YY
+
+OLEVEL: 1
+OFLAG: 13
+
+REPLACE {{
+ ld hl, $1
+ push hl
+ ld hl, $2
+ pop de
+}}
+
+IF {{
+}}
+
+WITH {{
+ ld de, $1
+ ld hl, $2
+}}
+
diff --git a/src/arch/z80/peephole/opts/013_o1_neg_neg.opt b/src/arch/z80/peephole/opts/013_o1_neg_neg.opt
index 6c625562a..058ed0cf0 100644
--- a/src/arch/z80/peephole/opts/013_o1_neg_neg.opt
+++ b/src/arch/z80/peephole/opts/013_o1_neg_neg.opt
@@ -1,16 +1,16 @@
-;; Replaces sequence:
-;; neg
-;; neg
-;; with
-;; ; nop ;; double neg can be removed
-
-OLEVEL: 1
-OFLAG: 13
-
-REPLACE {{
- neg
- neg
-}}
-
-WITH {{
-}}
+;; Replaces sequence:
+;; neg
+;; neg
+;; with
+;; ; nop ;; double neg can be removed
+
+OLEVEL: 1
+OFLAG: 13
+
+REPLACE {{
+ neg
+ neg
+}}
+
+WITH {{
+}}
diff --git a/src/arch/z80/peephole/opts/014_o1_or_sbc_a_a.opt b/src/arch/z80/peephole/opts/014_o1_or_sbc_a_a.opt
index 26954a515..97ee62f05 100644
--- a/src/arch/z80/peephole/opts/014_o1_or_sbc_a_a.opt
+++ b/src/arch/z80/peephole/opts/014_o1_or_sbc_a_a.opt
@@ -1,22 +1,22 @@
-;; Replaces sequence:
-;; or|xor|and a
-;; sbc a, a
-;; with
-;; xor a
-
-OLEVEL: 1
-OFLAG: 14
-
-REPLACE {{
- $1 a
- sbc a, a
-}}
-
-IF {{
- ($1 IN (or, and, xor))
-}}
-
-WITH {{
- xor a
-}}
-
+;; Replaces sequence:
+;; or|xor|and a
+;; sbc a, a
+;; with
+;; xor a
+
+OLEVEL: 1
+OFLAG: 14
+
+REPLACE {{
+ $1 a
+ sbc a, a
+}}
+
+IF {{
+ ($1 IN (or, and, xor))
+}}
+
+WITH {{
+ xor a
+}}
+
diff --git a/src/arch/z80/peephole/opts/015_o3_ld_r_n_not_required.opt b/src/arch/z80/peephole/opts/015_o3_ld_r_n_not_required.opt
index aa814c363..839a9aa5c 100644
--- a/src/arch/z80/peephole/opts/015_o3_ld_r_n_not_required.opt
+++ b/src/arch/z80/peephole/opts/015_o3_ld_r_n_not_required.opt
@@ -1,18 +1,18 @@
-;; Removes sequence
-;; ld a, XXXX
-;; if a is not used later
-
-OLEVEL: 3
-OFLAG: 15
-
-REPLACE {{
- ld a, $1
-}}
-
-IF {{
- !IS_REQUIRED(a)
-}}
-
-WITH {{
-}}
-
+;; Removes sequence
+;; ld a, XXXX
+;; if a is not used later
+
+OLEVEL: 3
+OFLAG: 15
+
+REPLACE {{
+ ld a, $1
+}}
+
+IF {{
+ !IS_REQUIRED(a)
+}}
+
+WITH {{
+}}
+
diff --git a/src/arch/z80/peephole/opts/016_o1_xor_a_jp_z.opt b/src/arch/z80/peephole/opts/016_o1_xor_a_jp_z.opt
index 66315a3da..6b6dd4593 100644
--- a/src/arch/z80/peephole/opts/016_o1_xor_a_jp_z.opt
+++ b/src/arch/z80/peephole/opts/016_o1_xor_a_jp_z.opt
@@ -1,22 +1,22 @@
-;; Replaces sequence:
-;; or|xor|and a
-;; sbc a, a
-;; with
-;; xor a
-
-OLEVEL: 1
-OFLAG: 17
-
-REPLACE {{
- xor a
- $1 z, $2
-}}
-
-IF {{
- ($1 IN (jp, jr))
-}}
-
-WITH {{
- $1 $2
-}}
-
+;; Replaces sequence:
+;; or|xor|and a
+;; sbc a, a
+;; with
+;; xor a
+
+OLEVEL: 1
+OFLAG: 17
+
+REPLACE {{
+ xor a
+ $1 z, $2
+}}
+
+IF {{
+ ($1 IN (jp, jr))
+}}
+
+WITH {{
+ $1 $2
+}}
+
diff --git a/src/arch/z80/peephole/opts/017_jp_cond_jp_labell.opt b/src/arch/z80/peephole/opts/017_jp_cond_jp_labell.opt
index ad275466b..9a2bbeb17 100644
--- a/src/arch/z80/peephole/opts/017_jp_cond_jp_labell.opt
+++ b/src/arch/z80/peephole/opts/017_jp_cond_jp_labell.opt
@@ -1,30 +1,30 @@
-;; Replace sequence:
-;; jp , __LABEL
-;; jp OTHER
-;; __LABEL:
-;;
-;; With:
-;; jp !, OTHER
-
-OLEVEL: 1
-OFLAG: 17
-
-REPLACE {{
- $1 $2, $3
- $4 $5
-$3:
-}}
-
-;; Defines $4 as the negated condition
-DEFINE {{
- $6 = (($2 == nc) && c) || (($2 == c) && nc) || (($2 == nz) && z) || (($2 == z) && nz)
-}}
-
-IF {{
- $6 && (LEN($5) == 1) && (($1 == jp) || ($1 == jr)) && (($4 == jp) || ($4 == jr))
-}}
-
-WITH {{
- $4 $6, $5
-$3:
-}}
+;; Replace sequence:
+;; jp , __LABEL
+;; jp OTHER
+;; __LABEL:
+;;
+;; With:
+;; jp !, OTHER
+
+OLEVEL: 1
+OFLAG: 17
+
+REPLACE {{
+ $1 $2, $3
+ $4 $5
+$3:
+}}
+
+;; Defines $4 as the negated condition
+DEFINE {{
+ $6 = (($2 == nc) && c) || (($2 == c) && nc) || (($2 == nz) && z) || (($2 == z) && nz)
+}}
+
+IF {{
+ $6 && (LEN($5) == 1) && (($1 == jp) || ($1 == jr)) && (($4 == jp) || ($4 == jr))
+}}
+
+WITH {{
+ $4 $6, $5
+$3:
+}}
diff --git a/src/arch/z80/peephole/opts/018_EQ16_by_sbchl.opt b/src/arch/z80/peephole/opts/018_EQ16_by_sbchl.opt
index 00afeaca2..e2008e5ff 100644
--- a/src/arch/z80/peephole/opts/018_EQ16_by_sbchl.opt
+++ b/src/arch/z80/peephole/opts/018_EQ16_by_sbchl.opt
@@ -1,33 +1,33 @@
-;; Tries to optimize a == b for U/Integers
-;; Replace sequence:
-;; call __EQ16
-;; or a | and a
-;; jp nz, ...
-;; With:
-;; or a
-;; sbc hl, de
-;; jp z, ...
-
-OLEVEL: 1
-OFLAG: 18
-
-REPLACE {{
- call __EQ16
- $1 a
- jp $2, $3
-}}
-
-;; Defines $4 as the negated condition
-DEFINE {{
- $4 = (($2 == nz) && z) || nz
-}}
-
-IF {{
- ($1 == or) || ($1 == and)
-}}
-
-WITH {{
- or a
- sbc hl, de
- jp $4, $3
-}}
+;; Tries to optimize a == b for U/Integers
+;; Replace sequence:
+;; call __EQ16
+;; or a | and a
+;; jp nz, ...
+;; With:
+;; or a
+;; sbc hl, de
+;; jp z, ...
+
+OLEVEL: 1
+OFLAG: 18
+
+REPLACE {{
+ call __EQ16
+ $1 a
+ jp $2, $3
+}}
+
+;; Defines $4 as the negated condition
+DEFINE {{
+ $4 = (($2 == nz) && z) || nz
+}}
+
+IF {{
+ ($1 == or) || ($1 == and)
+}}
+
+WITH {{
+ or a
+ sbc hl, de
+ jp $4, $3
+}}
diff --git a/src/arch/z80/peephole/opts/019_sub1_jpnc_ora_jpz.opt b/src/arch/z80/peephole/opts/019_sub1_jpnc_ora_jpz.opt
index cae3a6e4a..b7348a713 100644
--- a/src/arch/z80/peephole/opts/019_sub1_jpnc_ora_jpz.opt
+++ b/src/arch/z80/peephole/opts/019_sub1_jpnc_ora_jpz.opt
@@ -1,28 +1,28 @@
-;; Tries to optimize a == b for U/Bytes
-;; Replace sequence:
-;; sub 1
-;; jp nc, __LABEL | jp c, __LABEL
-;; With:
-;; or a
-;; jp nz, __LABEL | jp z, __LABEL
-
-OLEVEL: 1
-OFLAG: 19
-
-REPLACE {{
- sub 1
- $1 $2, $3
-}}
-
-DEFINE {{
- $4 = (($2 == nc) && nz) || z
-}}
-
-IF {{
- (($1 == jp) || ($1 == jr)) && (($2 == nc) || ($2 == c))
-}}
-
-WITH {{
- or a
- $1 $4, $3
-}}
+;; Tries to optimize a == b for U/Bytes
+;; Replace sequence:
+;; sub 1
+;; jp nc, __LABEL | jp c, __LABEL
+;; With:
+;; or a
+;; jp nz, __LABEL | jp z, __LABEL
+
+OLEVEL: 1
+OFLAG: 19
+
+REPLACE {{
+ sub 1
+ $1 $2, $3
+}}
+
+DEFINE {{
+ $4 = (($2 == nc) && nz) || z
+}}
+
+IF {{
+ (($1 == jp) || ($1 == jr)) && (($2 == nc) || ($2 == c))
+}}
+
+WITH {{
+ or a
+ $1 $4, $3
+}}
diff --git a/src/arch/z80/peephole/opts/020_o1_bool_norm_empty.opt b/src/arch/z80/peephole/opts/020_o1_bool_norm_empty.opt
index 07acaf1ae..e10815c07 100644
--- a/src/arch/z80/peephole/opts/020_o1_bool_norm_empty.opt
+++ b/src/arch/z80/peephole/opts/020_o1_bool_norm_empty.opt
@@ -1,32 +1,32 @@
-;; Remove the boolean normalization if it's done after calling
-;; certain routines that return the bool result already normalized.
-
-;; The sequence
-;; sub 1
-;; sbc a, a
-;; inc a
-;; can be removed
-
-OLEVEL: 1
-OFLAG: 20
-
-REPLACE {{
- $1
- sub 1
- sbc a, a
- inc a
-}}
-
-WITH {{
- $1
-}}
-
-IF {{
- $1 IN ("xor a", "ld a, 0",
- "call .core.__GEF", "call .core.__LEI16", "call .core.__LEI8", "call .core.__LTI8",
- "call .core.__ANDF", "call .core.__EQF", "call .core.__GTF", "call .core.__LTI16",
- "call .core.__LEF", "call .core.__LEI32", "call .core.__LTF", "call .core.__LTI32",
- "call .core.__NEF", "call .core.__NOTF", "call .core.__ORF", "call .core.__XORF",
- "call .core.__STREQ", "call .core.__STRNE", "call .core.__STRLT", "call .core.__STRLE",
- "call .core.__STRGT", "call .core.__STRGE")
-}}
+;; Remove the boolean normalization if it's done after calling
+;; certain routines that return the bool result already normalized.
+
+;; The sequence
+;; sub 1
+;; sbc a, a
+;; inc a
+;; can be removed
+
+OLEVEL: 1
+OFLAG: 20
+
+REPLACE {{
+ $1
+ sub 1
+ sbc a, a
+ inc a
+}}
+
+WITH {{
+ $1
+}}
+
+IF {{
+ $1 IN ("xor a", "ld a, 0",
+ "call .core.__GEF", "call .core.__LEI16", "call .core.__LEI8", "call .core.__LTI8",
+ "call .core.__ANDF", "call .core.__EQF", "call .core.__GTF", "call .core.__LTI16",
+ "call .core.__LEF", "call .core.__LEI32", "call .core.__LTF", "call .core.__LTI32",
+ "call .core.__NEF", "call .core.__NOTF", "call .core.__ORF", "call .core.__XORF",
+ "call .core.__STREQ", "call .core.__STRNE", "call .core.__STRLT", "call .core.__STRLE",
+ "call .core.__STRGT", "call .core.__STRGE")
+}}
diff --git a/src/arch/z80/peephole/opts/021_o1_bool_norm_neg.opt b/src/arch/z80/peephole/opts/021_o1_bool_norm_neg.opt
index dc7f71d37..278bf3539 100644
--- a/src/arch/z80/peephole/opts/021_o1_bool_norm_neg.opt
+++ b/src/arch/z80/peephole/opts/021_o1_bool_norm_neg.opt
@@ -1,26 +1,26 @@
-;; The sequence:
-;; sbc a, a ; A is either 0 or -1
-;; sub 1
-;; sbc a, a
-;; inc a
-;; can be replaced by
-;; neg ; A is either 0 or 1
-
-OLEVEL: 1
-OFLAG: 21
-
-REPLACE {{
- $1
- sub 1
- sbc a, a
- inc a
-}}
-
-WITH {{
- $1
- neg
-}}
-
-IF {{
- $1 IN ("sbc a, a", "call .core.__NOT32", "call .core.__XOR8", "call .core.__XOR16", "call .core.__XOR32")
-}}
+;; The sequence:
+;; sbc a, a ; A is either 0 or -1
+;; sub 1
+;; sbc a, a
+;; inc a
+;; can be replaced by
+;; neg ; A is either 0 or 1
+
+OLEVEL: 1
+OFLAG: 21
+
+REPLACE {{
+ $1
+ sub 1
+ sbc a, a
+ inc a
+}}
+
+WITH {{
+ $1
+ neg
+}}
+
+IF {{
+ $1 IN ("sbc a, a", "call .core.__NOT32", "call .core.__XOR8", "call .core.__XOR16", "call .core.__XOR32")
+}}
diff --git a/src/arch/z80/peephole/opts/022_insr_a_or_a.opt b/src/arch/z80/peephole/opts/022_insr_a_or_a.opt
index 406296d01..fb3c21ac6 100644
--- a/src/arch/z80/peephole/opts/022_insr_a_or_a.opt
+++ b/src/arch/z80/peephole/opts/022_insr_a_or_a.opt
@@ -1,24 +1,24 @@
-;; Replace sequence:
-;; and X|or X|xor X|sub X|cp X|dec a|inc a
-;; or a
-;;
-;; With:
-;; and X|or X|xor X|sub X|cp X|dec a|inc a
-
-OLEVEL: 1
-OFLAG: 22
-
-REPLACE {{
- $1 $2
- $3 a
-}}
-
-IF {{
- (($3 == or) || ($3 == and)) &&
- (($1 == or) || ($1 == sub) || ($1 == and) || ($1 == xor) || ($1 == cp) ||
- (($2 == a) && (($1 == dec) || ($1 == inc))))
-}}
-
-WITH {{
- $1 $2
-}}
+;; Replace sequence:
+;; and X|or X|xor X|sub X|cp X|dec a|inc a
+;; or a
+;;
+;; With:
+;; and X|or X|xor X|sub X|cp X|dec a|inc a
+
+OLEVEL: 1
+OFLAG: 22
+
+REPLACE {{
+ $1 $2
+ $3 a
+}}
+
+IF {{
+ (($3 == or) || ($3 == and)) &&
+ (($1 == or) || ($1 == sub) || ($1 == and) || ($1 == xor) || ($1 == cp) ||
+ (($2 == a) && (($1 == dec) || ($1 == inc))))
+}}
+
+WITH {{
+ $1 $2
+}}
diff --git a/src/arch/z80/peephole/opts/023_ld_hl_bc_outl.opt b/src/arch/z80/peephole/opts/023_ld_hl_bc_outl.opt
index 617a2ae4b..8a863b248 100644
--- a/src/arch/z80/peephole/opts/023_ld_hl_bc_outl.opt
+++ b/src/arch/z80/peephole/opts/023_ld_hl_bc_outl.opt
@@ -1,27 +1,27 @@
-;; Replace sequence:
-;; ld hl, (NN) | ld hl, NN | pop hl
-;; ld b, h
-;; ld c, l
-;; out (c), a | in a, (c)
-;; With:
-;; ld bc, (NN) | ld bc, NN | pop bc
-;; out (c), a | in a, (c)
-
-OLEVEL: 1
-OFLAG: 23
-
-REPLACE {{
- $1 hl$2
- ld b, h
- ld c, l
- $3
-}}
-
-IF {{
- (($1 == ld) || ($1 == pop)) && (($3 == "out (c), a") || ($3 == "in a, (c)"))
-}}
-
-WITH {{
- $1 bc$2
- $3
-}}
+;; Replace sequence:
+;; ld hl, (NN) | ld hl, NN | pop hl
+;; ld b, h
+;; ld c, l
+;; out (c), a | in a, (c)
+;; With:
+;; ld bc, (NN) | ld bc, NN | pop bc
+;; out (c), a | in a, (c)
+
+OLEVEL: 1
+OFLAG: 23
+
+REPLACE {{
+ $1 hl$2
+ ld b, h
+ ld c, l
+ $3
+}}
+
+IF {{
+ (($1 == ld) || ($1 == pop)) && (($3 == "out (c), a") || ($3 == "in a, (c)"))
+}}
+
+WITH {{
+ $1 bc$2
+ $3
+}}
diff --git a/src/arch/z80/peephole/opts/024_o1_bool_norm_duble_neg.opt b/src/arch/z80/peephole/opts/024_o1_bool_norm_duble_neg.opt
index 315ec4ea2..c3fb6a345 100644
--- a/src/arch/z80/peephole/opts/024_o1_bool_norm_duble_neg.opt
+++ b/src/arch/z80/peephole/opts/024_o1_bool_norm_duble_neg.opt
@@ -1,23 +1,23 @@
-;; The sequence:
-;; sbc a, a ; A is either 0 or -1
-;; sub 1
-;; sbc a, a
-;; neg
-;; can be replaced by
-;; sbc a, a ; A is either 0 or -1
-;; inc a
-
-OLEVEL: 1
-OFLAG: 24
-
-REPLACE {{
- sbc a, a
- sub 1
- sbc a, a
- neg
-}}
-
-WITH {{
- sbc a, a
- inc a
-}}
+;; The sequence:
+;; sbc a, a ; A is either 0 or -1
+;; sub 1
+;; sbc a, a
+;; neg
+;; can be replaced by
+;; sbc a, a ; A is either 0 or -1
+;; inc a
+
+OLEVEL: 1
+OFLAG: 24
+
+REPLACE {{
+ sbc a, a
+ sub 1
+ sbc a, a
+ neg
+}}
+
+WITH {{
+ sbc a, a
+ inc a
+}}
diff --git a/src/arch/z80/peephole/opts/025_ld_h_a_oper_A.opt b/src/arch/z80/peephole/opts/025_ld_h_a_oper_A.opt
index 60c7aa662..c76aef181 100644
--- a/src/arch/z80/peephole/opts/025_ld_h_a_oper_A.opt
+++ b/src/arch/z80/peephole/opts/025_ld_h_a_oper_A.opt
@@ -1,27 +1,27 @@
-;; Replace sequence:
-;; ld h, r (r != a)
-;; ld a, X
-;; or/and/cp/add a,/sbc a,/sub h
-;;
-;; With:
-;; ld a, X
-;; or|xor|and|cp|add|sub r
-
-
-OLEVEL: 1
-OFLAG: 25
-
-REPLACE {{
- ld h, $1
- ld a, $2
- $3 h
-}}
-
-IF {{
- ($1 <> a) && (($3 == or) || ($3 == xor) || ($3 == and) || ($3 == cp) || ($3 == sub) || ($3 == "add a,") || ($3 == "sbc a,"))
-}}
-
-WITH {{
- ld a, $2
- $3 $1
-}}
+;; Replace sequence:
+;; ld h, r (r != a)
+;; ld a, X
+;; or/and/cp/add a,/sbc a,/sub h
+;;
+;; With:
+;; ld a, X
+;; or|xor|and|cp|add|sub r
+
+
+OLEVEL: 1
+OFLAG: 25
+
+REPLACE {{
+ ld h, $1
+ ld a, $2
+ $3 h
+}}
+
+IF {{
+ ($1 <> a) && (($3 == or) || ($3 == xor) || ($3 == and) || ($3 == cp) || ($3 == sub) || ($3 == "add a,") || ($3 == "sbc a,"))
+}}
+
+WITH {{
+ ld a, $2
+ $3 $1
+}}
diff --git a/src/arch/z80/peephole/opts/027_ld_h_a_oper_A.opt b/src/arch/z80/peephole/opts/027_ld_h_a_oper_A.opt
index d3af28ac2..3afb79b9f 100644
--- a/src/arch/z80/peephole/opts/027_ld_h_a_oper_A.opt
+++ b/src/arch/z80/peephole/opts/027_ld_h_a_oper_A.opt
@@ -1,22 +1,22 @@
-;; Replace sequence:
-;; ld h, X
-;; or h | and h
-;;
-;; With:
-;; or X | and X
-
-OLEVEL: 1
-OFLAG: 27
-
-REPLACE {{
- ld h, $1
- $2 h
-}}
-
-IF {{
- ($2 IN (or, xor, and, cp, sub, "add a,", "sbc a,"))
-}}
-
-WITH {{
- $2 $1
-}}
+;; Replace sequence:
+;; ld h, X
+;; or h | and h
+;;
+;; With:
+;; or X | and X
+
+OLEVEL: 1
+OFLAG: 27
+
+REPLACE {{
+ ld h, $1
+ $2 h
+}}
+
+IF {{
+ ($2 IN (or, xor, and, cp, sub, "add a,", "sbc a,"))
+}}
+
+WITH {{
+ $2 $1
+}}
diff --git a/src/arch/z80/peephole/opts/028_o2_pop_up.opt b/src/arch/z80/peephole/opts/028_o2_pop_up.opt
index c57a27a46..855643104 100644
--- a/src/arch/z80/peephole/opts/028_o2_pop_up.opt
+++ b/src/arch/z80/peephole/opts/028_o2_pop_up.opt
@@ -1,28 +1,28 @@
-;; Replace sequence:
-;;
-;; pop rr
-;;
-;; With:
-;; pop rr
-;;
-;;
-;; This frees the stack ASAP and hopefully clash against a PUSH
-
-
-OLEVEL: 2
-OFLAG: 28
-
-REPLACE {{
- $2
- pop $1
-}}
-
-IF {{
- !(INSTR($2) IN (jp, jr, ret, call, djnz, rst)) && !NEEDS($2, (sp, $1)) && !IS_LABEL($2) &&
- OP1($2) <> "sp" && OP2($2) <> "sp"
-}}
-
-WITH {{
- pop $1
- $2
-}}
+;; Replace sequence:
+;;
+;; pop rr
+;;
+;; With:
+;; pop rr
+;;
+;;
+;; This frees the stack ASAP and hopefully clash against a PUSH
+
+
+OLEVEL: 2
+OFLAG: 28
+
+REPLACE {{
+ $2
+ pop $1
+}}
+
+IF {{
+ !(INSTR($2) IN (jp, jr, ret, call, djnz, rst)) && !NEEDS($2, (sp, $1)) && !IS_LABEL($2) &&
+ OP1($2) <> "sp" && OP2($2) <> "sp"
+}}
+
+WITH {{
+ pop $1
+ $2
+}}
diff --git a/src/arch/z80/peephole/opts/028_o3_pop_up.opt b/src/arch/z80/peephole/opts/028_o3_pop_up.opt
index f2998a4ac..00f9ff0e4 100644
--- a/src/arch/z80/peephole/opts/028_o3_pop_up.opt
+++ b/src/arch/z80/peephole/opts/028_o3_pop_up.opt
@@ -1,27 +1,27 @@
-;; Replace sequence:
-;;
-;; pop rr
-;;
-;; With:
-;; pop rr
-;;
-;;
-;; This frees the stack ASAP and hopefully clash against a PUSH
-
-OLEVEL: 3
-OFLAG: 28
-
-REPLACE {{
- $2
- pop $1
-}}
-
-IF {{
- !(INSTR($2) IN (jp, jr, ret, call, djnz, rst)) && !NEEDS($2, (sp, $1)) && !IS_LABEL($2)
- && OP1($2) <> "sp"
-}}
-
-WITH {{
- pop $1
- $2
-}}
+;; Replace sequence:
+;;
+;; pop rr
+;;
+;; With:
+;; pop rr
+;;
+;;
+;; This frees the stack ASAP and hopefully clash against a PUSH
+
+OLEVEL: 3
+OFLAG: 28
+
+REPLACE {{
+ $2
+ pop $1
+}}
+
+IF {{
+ !(INSTR($2) IN (jp, jr, ret, call, djnz, rst)) && !NEEDS($2, (sp, $1)) && !IS_LABEL($2)
+ && OP1($2) <> "sp"
+}}
+
+WITH {{
+ pop $1
+ $2
+}}
diff --git a/src/arch/z80/peephole/opts/029_cp_0_or_a.opt b/src/arch/z80/peephole/opts/029_cp_0_or_a.opt
index be249f8db..2a0642d68 100644
--- a/src/arch/z80/peephole/opts/029_cp_0_or_a.opt
+++ b/src/arch/z80/peephole/opts/029_cp_0_or_a.opt
@@ -1,20 +1,20 @@
-;; Replace sequence:
-;; sub 0|cp 0
-;;
-;; With:
-;; or a
-
-OLEVEL: 1
-OFLAG: 29
-
-REPLACE {{
- $1 0
-}}
-
-IF {{
- ($1 == cp) || ($1 == sub)
-}}
-
-WITH {{
- or a
-}}
+;; Replace sequence:
+;; sub 0|cp 0
+;;
+;; With:
+;; or a
+
+OLEVEL: 1
+OFLAG: 29
+
+REPLACE {{
+ $1 0
+}}
+
+IF {{
+ ($1 == cp) || ($1 == sub)
+}}
+
+WITH {{
+ or a
+}}
diff --git a/src/arch/z80/peephole/opts/030_ora_jp_nc_jp.opt b/src/arch/z80/peephole/opts/030_ora_jp_nc_jp.opt
index 9d439e145..34674817a 100644
--- a/src/arch/z80/peephole/opts/030_ora_jp_nc_jp.opt
+++ b/src/arch/z80/peephole/opts/030_ora_jp_nc_jp.opt
@@ -1,22 +1,22 @@
-;; Replace sequence:
-;; or a | and a
-;; jp nc, X | jr nc, X
-;;
-;; With:
-;; jp X | jr X
-
-OLEVEL: 1
-OFLAG: 30
-
-REPLACE {{
- $1 a
- $2 nc, $3
-}}
-
-IF {{
- ($1 IN (or, xor, and, cp, sub)) && (($2 == jp) || ($2 == jr))
-}}
-
-WITH {{
- $2 $3
-}}
+;; Replace sequence:
+;; or a | and a
+;; jp nc, X | jr nc, X
+;;
+;; With:
+;; jp X | jr X
+
+OLEVEL: 1
+OFLAG: 30
+
+REPLACE {{
+ $1 a
+ $2 nc, $3
+}}
+
+IF {{
+ ($1 IN (or, xor, and, cp, sub)) && (($2 == jp) || ($2 == jr))
+}}
+
+WITH {{
+ $2 $3
+}}
diff --git a/src/arch/z80/peephole/opts/031_jpX_Y_jpX.opt b/src/arch/z80/peephole/opts/031_jpX_Y_jpX.opt
index 3a404e2e1..e142b6d73 100644
--- a/src/arch/z80/peephole/opts/031_jpX_Y_jpX.opt
+++ b/src/arch/z80/peephole/opts/031_jpX_Y_jpX.opt
@@ -1,25 +1,25 @@
-;; Removes useless (jumped over) instructions
-;; Replace sequence:
-;; jp X | jr X
-;; YYYY
-;;
-;; With:
-;; jp X | jr X
-;;
-;; YYYY must not be a label
-
-OLEVEL: 1
-OFLAG: 31
-
-REPLACE {{
- $1 $2
- $3
-}}
-
-IF {{
- (($1 == jp) || ($1 == jr)) && (!IS_LABEL($3)) && (LEN($2) == 1) && (!IS_ASM($3))
-}}
-
-WITH {{
- $1 $2
-}}
+;; Removes useless (jumped over) instructions
+;; Replace sequence:
+;; jp X | jr X
+;; YYYY
+;;
+;; With:
+;; jp X | jr X
+;;
+;; YYYY must not be a label
+
+OLEVEL: 1
+OFLAG: 31
+
+REPLACE {{
+ $1 $2
+ $3
+}}
+
+IF {{
+ (($1 == jp) || ($1 == jr)) && (!IS_LABEL($3)) && (LEN($2) == 1) && (!IS_ASM($3))
+}}
+
+WITH {{
+ $1 $2
+}}
diff --git a/src/arch/z80/peephole/opts/032_call_LOADSTR_ld_a1.opt b/src/arch/z80/peephole/opts/032_call_LOADSTR_ld_a1.opt
index 9b9902746..7156931c5 100644
--- a/src/arch/z80/peephole/opts/032_call_LOADSTR_ld_a1.opt
+++ b/src/arch/z80/peephole/opts/032_call_LOADSTR_ld_a1.opt
@@ -1,22 +1,22 @@
-;; Replace sequence:
-;; call __LOADSTR
-;; ld a, 1
-;; call __PRINTSTR
-;;
-;; With:
-;; xor a
-;; call __PRINTSTR
-
-OLEVEL: 1
-OFLAG: 32
-
-REPLACE {{
- call __LOADSTR
- ld a, 1
- call __PRINTSTR
-}}
-
-WITH {{
- xor a
- call __PRINTSTR
-}}
+;; Replace sequence:
+;; call __LOADSTR
+;; ld a, 1
+;; call __PRINTSTR
+;;
+;; With:
+;; xor a
+;; call __PRINTSTR
+
+OLEVEL: 1
+OFLAG: 32
+
+REPLACE {{
+ call __LOADSTR
+ ld a, 1
+ call __PRINTSTR
+}}
+
+WITH {{
+ xor a
+ call __PRINTSTR
+}}
diff --git a/src/arch/z80/peephole/opts/050_o1_ld_a_ld_h_pop.opt b/src/arch/z80/peephole/opts/050_o1_ld_a_ld_h_pop.opt
index 8d1fc4825..e8623acdd 100644
--- a/src/arch/z80/peephole/opts/050_o1_ld_a_ld_h_pop.opt
+++ b/src/arch/z80/peephole/opts/050_o1_ld_a_ld_h_pop.opt
@@ -1,29 +1,29 @@
-;; Replace sequence:
-;; ld a, X
-;; ld h, a
-;; ld a, Y
-;;
-;; With:
-;; ld h, X
-;; ld a, Y
-;;
-;; Whenever X is (hl), (ix + n) or a register
-
-
-OLEVEL: 1
-OFLAG: 50
-
-REPLACE {{
- ld a, $4
- ld h, a
- ld a, $3
-}}
-
-IF {{
- (($4 == "(hl)") || (IS_INDIR($4)) || (IS_REG8($4)))
-}}
-
-WITH {{
- ld h, $4
- ld a, $3
-}}
+;; Replace sequence:
+;; ld a, X
+;; ld h, a
+;; ld a, Y
+;;
+;; With:
+;; ld h, X
+;; ld a, Y
+;;
+;; Whenever X is (hl), (ix + n) or a register
+
+
+OLEVEL: 1
+OFLAG: 50
+
+REPLACE {{
+ ld a, $4
+ ld h, a
+ ld a, $3
+}}
+
+IF {{
+ (($4 == "(hl)") || (IS_INDIR($4)) || (IS_REG8($4)))
+}}
+
+WITH {{
+ ld h, $4
+ ld a, $3
+}}
diff --git a/src/arch/z80/peephole/opts/051_o1_ld_a_ld_h_a_pop.opt b/src/arch/z80/peephole/opts/051_o1_ld_a_ld_h_a_pop.opt
index d6cb6a736..505bd6279 100644
--- a/src/arch/z80/peephole/opts/051_o1_ld_a_ld_h_a_pop.opt
+++ b/src/arch/z80/peephole/opts/051_o1_ld_a_ld_h_a_pop.opt
@@ -1,29 +1,29 @@
-;; Replace sequence:
-;; ld a, X
-;; ld h, a
-;; pop af
-;;
-;; With:
-;; ld h, X
-;; pop af
-;;
-;; Whenever X is (hl), (ix + n) a number or a register
-
-
-OLEVEL: 1
-OFLAG: 50
-
-REPLACE {{
- ld a, $1
- ld h, a
- pop af
-}}
-
-IF {{
- (($1 == "(hl)") || (IS_INDIR($1)) || (IS_REG8($1)))
-}}
-
-WITH {{
- pop af
- ld h, $1
-}}
+;; Replace sequence:
+;; ld a, X
+;; ld h, a
+;; pop af
+;;
+;; With:
+;; ld h, X
+;; pop af
+;;
+;; Whenever X is (hl), (ix + n) a number or a register
+
+
+OLEVEL: 1
+OFLAG: 50
+
+REPLACE {{
+ ld a, $1
+ ld h, a
+ pop af
+}}
+
+IF {{
+ (($1 == "(hl)") || (IS_INDIR($1)) || (IS_REG8($1)))
+}}
+
+WITH {{
+ pop af
+ ld h, $1
+}}
diff --git a/src/arch/z80/peephole/opts/052_o3_ldhl_ld_a_l.opt b/src/arch/z80/peephole/opts/052_o3_ldhl_ld_a_l.opt
index 4606ba779..98029b41a 100644
--- a/src/arch/z80/peephole/opts/052_o3_ldhl_ld_a_l.opt
+++ b/src/arch/z80/peephole/opts/052_o3_ldhl_ld_a_l.opt
@@ -1,22 +1,22 @@
-;; Replaces sequence:
-;; ld hl, (_XXXX)
-;; ld a, l
-;;
-;; With:
-;; ld a, (_XXXX)
-
-OLEVEL: 3
-OFLAG: 52
-
-REPLACE {{
- ld hl, ($1)
- ld a, l
-}}
-
-IF {{
- !IS_REQUIRED(hl)
-}}
-
-WITH {{
- ld a, ($1)
-}}
+;; Replaces sequence:
+;; ld hl, (_XXXX)
+;; ld a, l
+;;
+;; With:
+;; ld a, (_XXXX)
+
+OLEVEL: 3
+OFLAG: 52
+
+REPLACE {{
+ ld hl, ($1)
+ ld a, l
+}}
+
+IF {{
+ !IS_REQUIRED(hl)
+}}
+
+WITH {{
+ ld a, ($1)
+}}
diff --git a/src/arch/z80/peephole/opts/053_o1_ldh_nnn.opt b/src/arch/z80/peephole/opts/053_o1_ldh_nnn.opt
index 59b0d39f4..58d686a32 100644
--- a/src/arch/z80/peephole/opts/053_o1_ldh_nnn.opt
+++ b/src/arch/z80/peephole/opts/053_o1_ldh_nnn.opt
@@ -1,21 +1,21 @@
-;; Replaces sequence:
-;; ld a, (_XXXX)
-;; ld h, a
-;;
-;; With:
-;; ld hl, (_XXXX - 1)
-
-OLEVEL: 1
-OFLAG: 53
-
-REPLACE {{
- ld a, ($1)
- ld h, a
- pop af
-}}
-
-
-WITH {{
- pop af
- ld hl, ($1 - 1)
-}}
+;; Replaces sequence:
+;; ld a, (_XXXX)
+;; ld h, a
+;;
+;; With:
+;; ld hl, (_XXXX - 1)
+
+OLEVEL: 1
+OFLAG: 53
+
+REPLACE {{
+ ld a, ($1)
+ ld h, a
+ pop af
+}}
+
+
+WITH {{
+ pop af
+ ld hl, ($1 - 1)
+}}
diff --git a/src/arch/z80/peephole/opts/054_o3_ld_a_N_ld_hl_a.opt b/src/arch/z80/peephole/opts/054_o3_ld_a_N_ld_hl_a.opt
index dcff2a0d8..af02599cb 100644
--- a/src/arch/z80/peephole/opts/054_o3_ld_a_N_ld_hl_a.opt
+++ b/src/arch/z80/peephole/opts/054_o3_ld_a_N_ld_hl_a.opt
@@ -1,24 +1,24 @@
-;; Replaces sequence:
-;; ld hl, (_XXXX)
-;; ld a, l
-;;
-;; With:
-;; ld a, (_XXXX)
-
-OLEVEL: 3
-OFLAG: 54
-
-REPLACE {{
- ld a, $1
- ld hl, ($2)
- ld (hl), a
-}}
-
-IF {{
- !IS_REQUIRED(a) && IS_IMMED($1)
-}}
-
-WITH {{
- ld hl, ($2)
- ld (hl), $1
-}}
+;; Replaces sequence:
+;; ld hl, (_XXXX)
+;; ld a, l
+;;
+;; With:
+;; ld a, (_XXXX)
+
+OLEVEL: 3
+OFLAG: 54
+
+REPLACE {{
+ ld a, $1
+ ld hl, ($2)
+ ld (hl), a
+}}
+
+IF {{
+ !IS_REQUIRED(a) && IS_IMMED($1)
+}}
+
+WITH {{
+ ld hl, ($2)
+ ld (hl), $1
+}}
diff --git a/src/arch/z80/peephole/opts/100_o3_ld_rr_N_ld_ss_N.opt b/src/arch/z80/peephole/opts/100_o3_ld_rr_N_ld_ss_N.opt
index 201dfb972..99cc5cdd2 100644
--- a/src/arch/z80/peephole/opts/100_o3_ld_rr_N_ld_ss_N.opt
+++ b/src/arch/z80/peephole/opts/100_o3_ld_rr_N_ld_ss_N.opt
@@ -1,20 +1,20 @@
-;; Removes useless LD's
-;; Tries to guess the value of the 1st and 2nd operands.
-;; If they're the same, this LD is useless
-
-OLEVEL: 3
-OFLAG: 100
-
-REPLACE {{
- ld $1, $2
-}}
-
-IF {{
- (GVAL($1) == GVAL($2)) ||
- ((IS_REG8($1) || IS_REG8($2)) &&
- (LOVAL(GVAL($1)) == LOVAL(GVAL($2)))
- )
-}}
-
-WITH {{
-}}
+;; Removes useless LD's
+;; Tries to guess the value of the 1st and 2nd operands.
+;; If they're the same, this LD is useless
+
+OLEVEL: 3
+OFLAG: 100
+
+REPLACE {{
+ ld $1, $2
+}}
+
+IF {{
+ (GVAL($1) == GVAL($2)) ||
+ ((IS_REG8($1) || IS_REG8($2)) &&
+ (LOVAL(GVAL($1)) == LOVAL(GVAL($2)))
+ )
+}}
+
+WITH {{
+}}
diff --git a/src/arch/z80/peephole/opts/101_o3_ld_rr_N_ld_h_r_ld_l_r.opt b/src/arch/z80/peephole/opts/101_o3_ld_rr_N_ld_h_r_ld_l_r.opt
index 30b229046..028800e8d 100644
--- a/src/arch/z80/peephole/opts/101_o3_ld_rr_N_ld_h_r_ld_l_r.opt
+++ b/src/arch/z80/peephole/opts/101_o3_ld_rr_N_ld_h_r_ld_l_r.opt
@@ -1,36 +1,36 @@
-;; Replace sequence:
-;; ld hl, N
-;; ld b, h
-;; ld c, l
-;;
-;; With:
-;; ld bc, N
-;;
-;; If hl is not required later
-;; also works with any other 16bit register
-
-OLEVEL: 3
-OFLAG: 101
-
-REPLACE {{
- ld $1, $2
- ld $3, $4
- ld $5, $6
-}}
-
-IF {{
- (
- IS_REG16($1) && IS_REG16($7) && !IS_REQUIRED($1)
- && IS_REG8($5) && IS_REG8($3) && IS_REG8($4) && IS_REG8($6)
- && ((HIREG($1) == $4 && HIREG($7) == $3 && LOREG($1) == $6 && LOREG($7) == $5) ||
- (HIREG($1) == $6 && HIREG($7) == $5 && LOREG($1) == $4 && LOREG($7) == $3))
- )
-}}
-
-DEFINE {{
- $7 = IS_REG16($3 + $5) && ($3 + $5) || ($5 + $3)
-}}
-
-WITH {{
- ld $7, $2
-}}
+;; Replace sequence:
+;; ld hl, N
+;; ld b, h
+;; ld c, l
+;;
+;; With:
+;; ld bc, N
+;;
+;; If hl is not required later
+;; also works with any other 16bit register
+
+OLEVEL: 3
+OFLAG: 101
+
+REPLACE {{
+ ld $1, $2
+ ld $3, $4
+ ld $5, $6
+}}
+
+IF {{
+ (
+ IS_REG16($1) && IS_REG16($7) && !IS_REQUIRED($1)
+ && IS_REG8($5) && IS_REG8($3) && IS_REG8($4) && IS_REG8($6)
+ && ((HIREG($1) == $4 && HIREG($7) == $3 && LOREG($1) == $6 && LOREG($7) == $5) ||
+ (HIREG($1) == $6 && HIREG($7) == $5 && LOREG($1) == $4 && LOREG($7) == $3))
+ )
+}}
+
+DEFINE {{
+ $7 = IS_REG16($3 + $5) && ($3 + $5) || ($5 + $3)
+}}
+
+WITH {{
+ ld $7, $2
+}}
diff --git a/src/arch/z80/peephole/opts/102_o3_ld_r_n_ld_r_n2.opt b/src/arch/z80/peephole/opts/102_o3_ld_r_n_ld_r_n2.opt
index b8a491e11..b9947b2fc 100644
--- a/src/arch/z80/peephole/opts/102_o3_ld_r_n_ld_r_n2.opt
+++ b/src/arch/z80/peephole/opts/102_o3_ld_r_n_ld_r_n2.opt
@@ -1,18 +1,18 @@
-;; Removes useless LD's
-;; Replaces;
-;; LD r, XXX
-;; LD r, YYY
-;; With:
-;; LD r, YYY
-
-OLEVEL: 3
-OFLAG: 102
-
-REPLACE {{
- ld $1, $2
- ld $1, $3
-}}
-
-WITH {{
- ld $1, $3
-}}
+;; Removes useless LD's
+;; Replaces;
+;; LD r, XXX
+;; LD r, YYY
+;; With:
+;; LD r, YYY
+
+OLEVEL: 3
+OFLAG: 102
+
+REPLACE {{
+ ld $1, $2
+ ld $1, $3
+}}
+
+WITH {{
+ ld $1, $3
+}}
diff --git a/src/arch/z80/peephole/opts/103_o3_or_and_a.opt b/src/arch/z80/peephole/opts/103_o3_or_and_a.opt
index fb00a768e..51590a888 100644
--- a/src/arch/z80/peephole/opts/103_o3_or_and_a.opt
+++ b/src/arch/z80/peephole/opts/103_o3_or_and_a.opt
@@ -1,17 +1,17 @@
-;; Removes useless AND, OR a
-
-OLEVEL: 3
-OFLAG: 103
-
-REPLACE {{
- $1 $2
-}}
-
-IF {{
- ($1 IN (and, or)) &&
- (($2 == a) || (!IS_REQUIRED(a))) &&
- !IS_REQUIRED(f)
-}}
-
-WITH {{
-}}
+;; Removes useless AND, OR a
+
+OLEVEL: 3
+OFLAG: 103
+
+REPLACE {{
+ $1 $2
+}}
+
+IF {{
+ ($1 IN (and, or)) &&
+ (($2 == a) || (!IS_REQUIRED(a))) &&
+ !IS_REQUIRED(f)
+}}
+
+WITH {{
+}}
diff --git a/src/arch/z80/peephole/opts/103_o3_xor_a.opt b/src/arch/z80/peephole/opts/103_o3_xor_a.opt
index 4467f3323..8db94ac6b 100644
--- a/src/arch/z80/peephole/opts/103_o3_xor_a.opt
+++ b/src/arch/z80/peephole/opts/103_o3_xor_a.opt
@@ -1,15 +1,15 @@
-;; Removes useless XOR a
-
-OLEVEL: 3
-OFLAG: 103
-
-REPLACE {{
- xor a
-}}
-
-IF {{
- (GVAL(a) == 0) && ((FLAGVAL(c) == 0 && FLAGVAL(z) == 1) || !IS_REQUIRED(f))
-}}
-
-WITH {{
-}}
+;; Removes useless XOR a
+
+OLEVEL: 3
+OFLAG: 103
+
+REPLACE {{
+ xor a
+}}
+
+IF {{
+ (GVAL(a) == 0) && ((FLAGVAL(c) == 0 && FLAGVAL(z) == 1) || !IS_REQUIRED(f))
+}}
+
+WITH {{
+}}
diff --git a/src/arch/z80/peephole/opts/104_o3_opt27.opt b/src/arch/z80/peephole/opts/104_o3_opt27.opt
index 09409c591..3b1ff6a51 100644
--- a/src/arch/z80/peephole/opts/104_o3_opt27.opt
+++ b/src/arch/z80/peephole/opts/104_o3_opt27.opt
@@ -1,29 +1,29 @@
-;; Replaces
-;;
-;; ld r, XX
-;; cp r | sub r | and r | or r | xor r
-;;
-;; With
-;; cp XX | sub XX | and XX | or XX | xor XX
-;;
-;; IF r != a, and r is not required later
-;;
-;; Formerly OPT27
-
-OLEVEL: 3
-OFLAG: 104
-
-REPLACE {{
- ld $1, $2
- $3 $1
-}}
-
-IF {{
- ($1 <> a) &&
- !IS_REQUIRED($1) && !IS_REQUIRED(f) &&
- ($3 IN (cp, or, and, xor, sub))
-}}
-
-WITH {{
- $3 $2
-}}
+;; Replaces
+;;
+;; ld r, XX
+;; cp r | sub r | and r | or r | xor r
+;;
+;; With
+;; cp XX | sub XX | and XX | or XX | xor XX
+;;
+;; IF r != a, and r is not required later
+;;
+;; Formerly OPT27
+
+OLEVEL: 3
+OFLAG: 104
+
+REPLACE {{
+ ld $1, $2
+ $3 $1
+}}
+
+IF {{
+ ($1 <> a) &&
+ !IS_REQUIRED($1) && !IS_REQUIRED(f) &&
+ ($3 IN (cp, or, and, xor, sub))
+}}
+
+WITH {{
+ $3 $2
+}}
diff --git a/src/arch/z80/peephole/opts/105_o3_opt27.opt b/src/arch/z80/peephole/opts/105_o3_opt27.opt
index f43b81c87..67abb36c1 100644
--- a/src/arch/z80/peephole/opts/105_o3_opt27.opt
+++ b/src/arch/z80/peephole/opts/105_o3_opt27.opt
@@ -1,29 +1,29 @@
-;; Replaces
-;;
-;; ld r, XX
-;; add a, r | adc a, r | sbc a, r
-;;
-;; With
-;; add a, XX | adc a, XX | sbc a, XX
-;;
-;; IF r != a, and r is not required later
-;;
-;; Formerly OPT27
-
-OLEVEL: 3
-OFLAG: 105
-
-REPLACE {{
- ld $1, $2
- $3 a, $1
-}}
-
-IF {{
- ($1 <> a) &&
- !IS_REQUIRED($1) && !IS_REQUIRED(f) &&
- ($3 IN (add, adc, sbc))
-}}
-
-WITH {{
- $3 a, $2
-}}
+;; Replaces
+;;
+;; ld r, XX
+;; add a, r | adc a, r | sbc a, r
+;;
+;; With
+;; add a, XX | adc a, XX | sbc a, XX
+;;
+;; IF r != a, and r is not required later
+;;
+;; Formerly OPT27
+
+OLEVEL: 3
+OFLAG: 105
+
+REPLACE {{
+ ld $1, $2
+ $3 a, $1
+}}
+
+IF {{
+ ($1 <> a) &&
+ !IS_REQUIRED($1) && !IS_REQUIRED(f) &&
+ ($3 IN (add, adc, sbc))
+}}
+
+WITH {{
+ $3 a, $2
+}}
diff --git a/src/arch/z80/peephole/opts/106_o3_exdehl.opt b/src/arch/z80/peephole/opts/106_o3_exdehl.opt
index c250b69aa..db95cfcd3 100644
--- a/src/arch/z80/peephole/opts/106_o3_exdehl.opt
+++ b/src/arch/z80/peephole/opts/106_o3_exdehl.opt
@@ -1,23 +1,23 @@
-;; Replaces
-;;
-;; ld de, nn
-;;
-;; With
-;; ex de, hl
-;;
-;; if hl is no longer required, and guessed val of hl is nn
-
-OLEVEL: 3
-OFLAG: 106
-
-REPLACE {{
- ld de, $1
-}}
-
-IF {{
- GVAL(hl) == GVAL($1) && !IS_REQUIRED(hl)
-}}
-
-WITH {{
- ex de, hl
-}}
+;; Replaces
+;;
+;; ld de, nn
+;;
+;; With
+;; ex de, hl
+;;
+;; if hl is no longer required, and guessed val of hl is nn
+
+OLEVEL: 3
+OFLAG: 106
+
+REPLACE {{
+ ld de, $1
+}}
+
+IF {{
+ GVAL(hl) == GVAL($1) && !IS_REQUIRED(hl)
+}}
+
+WITH {{
+ ex de, hl
+}}
diff --git a/src/arch/z80/peephole/opts/107_o3_exdehl_ldde.opt b/src/arch/z80/peephole/opts/107_o3_exdehl_ldde.opt
index 78386b331..78147aa54 100644
--- a/src/arch/z80/peephole/opts/107_o3_exdehl_ldde.opt
+++ b/src/arch/z80/peephole/opts/107_o3_exdehl_ldde.opt
@@ -1,25 +1,25 @@
-;; Replaces
-;;
-;; ld hl, nn
-;; ex de, hl
-;;
-;; With
-;; ld de, nn
-;;
-;; if hl is no longer required
-
-OLEVEL: 3
-OFLAG: 107
-
-REPLACE {{
- ld hl, $1
- ex de, hl
-}}
-
-IF {{
- !IS_REQUIRED(hl)
-}}
-
-WITH {{
- ld de, $1
-}}
+;; Replaces
+;;
+;; ld hl, nn
+;; ex de, hl
+;;
+;; With
+;; ld de, nn
+;;
+;; if hl is no longer required
+
+OLEVEL: 3
+OFLAG: 107
+
+REPLACE {{
+ ld hl, $1
+ ex de, hl
+}}
+
+IF {{
+ !IS_REQUIRED(hl)
+}}
+
+WITH {{
+ ld de, $1
+}}
diff --git a/src/arch/z80/peephole/opts/108_o3_inc_dec.opt b/src/arch/z80/peephole/opts/108_o3_inc_dec.opt
index e7849f0e0..2d7f5a24b 100644
--- a/src/arch/z80/peephole/opts/108_o3_inc_dec.opt
+++ b/src/arch/z80/peephole/opts/108_o3_inc_dec.opt
@@ -1,28 +1,28 @@
-;; Replaces
-;;
-;; ld rr, nn
-;;
-;; With
-;; inc / dec rr
-;;
-;; If the previous known value of rr is nn - / + 1
-
-
-OLEVEL: 3
-OFLAG: 108
-
-REPLACE {{
- ld $1, $2
-}}
-
-DEFINE {{
- $3 = ($2 == GVAL($1) .+ 1) && "inc" || ($2 == GVAL($1) .- 1) && "dec"
-}}
-
-IF {{
- IS_REG16($1) && $3 <> ""
-}}
-
-WITH {{
- $3 $1
-}}
+;; Replaces
+;;
+;; ld rr, nn
+;;
+;; With
+;; inc / dec rr
+;;
+;; If the previous known value of rr is nn - / + 1
+
+
+OLEVEL: 3
+OFLAG: 108
+
+REPLACE {{
+ ld $1, $2
+}}
+
+DEFINE {{
+ $3 = ($2 == GVAL($1) .+ 1) && "inc" || ($2 == GVAL($1) .- 1) && "dec"
+}}
+
+IF {{
+ IS_REG16($1) && $3 <> ""
+}}
+
+WITH {{
+ $3 $1
+}}
diff --git a/src/arch/z80/peephole/opts/109_o4_ld_mem_op_ld_mem.opt b/src/arch/z80/peephole/opts/109_o4_ld_mem_op_ld_mem.opt
index 713718dd2..95e730ed7 100644
--- a/src/arch/z80/peephole/opts/109_o4_ld_mem_op_ld_mem.opt
+++ b/src/arch/z80/peephole/opts/109_o4_ld_mem_op_ld_mem.opt
@@ -1,17 +1,17 @@
-;; Removes useless operation
-;; Tries to guess if the result of the operation LD, OR, AND, XOR, ADC, ADD, SBC is later used
-
-OLEVEL: 4
-OFLAG: 109
-
-REPLACE {{
- $1 $2, $3
-}}
-
-IF {{
-($2 <> sp) && ($2 <> ix) && !IS_REQUIRED($2) &&
- ($1 == ld || (!IS_REQUIRED(f) && ($1 == add || $1 == adc || $1 == sbc)))
-}}
-
-WITH {{
-}}
+;; Removes useless operation
+;; Tries to guess if the result of the operation LD, OR, AND, XOR, ADC, ADD, SBC is later used
+
+OLEVEL: 4
+OFLAG: 109
+
+REPLACE {{
+ $1 $2, $3
+}}
+
+IF {{
+($2 <> sp) && ($2 <> ix) && !IS_REQUIRED($2) &&
+ ($1 == ld || (!IS_REQUIRED(f) && ($1 == add || $1 == adc || $1 == sbc)))
+}}
+
+WITH {{
+}}
diff --git a/src/arch/z80/peephole/opts/110_o4_ld_mem_op_ld_mem.opt b/src/arch/z80/peephole/opts/110_o4_ld_mem_op_ld_mem.opt
index aa76419ef..3780f39d5 100644
--- a/src/arch/z80/peephole/opts/110_o4_ld_mem_op_ld_mem.opt
+++ b/src/arch/z80/peephole/opts/110_o4_ld_mem_op_ld_mem.opt
@@ -1,19 +1,19 @@
-;; Removes useless LD's
-;; Tries to guess the value of the 1st and 2nd operands.
-;; If they're the same, this LD is useless
-
-OLEVEL: 4
-OFLAG: 110
-
-REPLACE {{
- $1 $2
-}}
-
-IF {{
- ((($1 == inc || $1 == dec) && !IS_REQUIRED($2))
- || ($1 == sub && !IS_REQUIRED(a))
- ) && ($2 <> sp) && !IS_REQUIRED(f)
-}}
-
-WITH {{
-}}
+;; Removes useless LD's
+;; Tries to guess the value of the 1st and 2nd operands.
+;; If they're the same, this LD is useless
+
+OLEVEL: 4
+OFLAG: 110
+
+REPLACE {{
+ $1 $2
+}}
+
+IF {{
+ ((($1 == inc || $1 == dec) && !IS_REQUIRED($2))
+ || ($1 == sub && !IS_REQUIRED(a))
+ ) && ($2 <> sp) && !IS_REQUIRED(f)
+}}
+
+WITH {{
+}}
diff --git a/src/arch/z80/peephole/parser.py b/src/arch/z80/peephole/parser.py
index 99d2958e8..8e312773c 100644
--- a/src/arch/z80/peephole/parser.py
+++ b/src/arch/z80/peephole/parser.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import re
from collections import defaultdict
from types import MappingProxyType
diff --git a/src/arch/z80/peephole/pattern.py b/src/arch/z80/peephole/pattern.py
index c6bf76a49..aee82d5cd 100644
--- a/src/arch/z80/peephole/pattern.py
+++ b/src/arch/z80/peephole/pattern.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import itertools
import re
diff --git a/src/arch/z80/peephole/template.py b/src/arch/z80/peephole/template.py
index 740548467..8b55cf284 100644
--- a/src/arch/z80/peephole/template.py
+++ b/src/arch/z80/peephole/template.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from .pattern import BasicLinePattern
diff --git a/src/arch/z80/visitor/__init__.py b/src/arch/z80/visitor/__init__.py
index e69de29bb..906667a8d 100644
--- a/src/arch/z80/visitor/__init__.py
+++ b/src/arch/z80/visitor/__init__.py
@@ -0,0 +1,6 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
diff --git a/src/arch/z80/visitor/builtin_translator.py b/src/arch/z80/visitor/builtin_translator.py
index 3a040e8e8..85e47f0d9 100644
--- a/src/arch/z80/visitor/builtin_translator.py
+++ b/src/arch/z80/visitor/builtin_translator.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.api import global_ as gl
from src.api.constants import SCOPE, TYPE
from src.api.global_ import optemps
@@ -12,6 +19,13 @@ class BuiltinTranslator(TranslatorVisitor):
REQUIRES = backend.REQUIRES
+ def __init__(self, backend: backend.Backend, parent_visitor: TranslatorVisitor):
+ super().__init__(backend)
+ self.parent_visitor = parent_visitor
+
+ def visit(self, node):
+ return self.parent_visitor.visit(node)
+
# region STRING Functions
def visit_INKEY(self, node):
self.runtime_call(RuntimeLabel.INKEY, Type.string.size)
@@ -118,7 +132,7 @@ def visit_SQR(self, node):
# endregion
def visit_LBOUND(self, node):
- yield node.operands[1]
+ yield self.visit(node.operands[1])
self.ic_param(gl.BOUND_TYPE, node.operands[1].t)
entry = node.operands[0]
if entry.scope == SCOPE.global_:
@@ -134,7 +148,7 @@ def visit_LBOUND(self, node):
self.runtime_call(RuntimeLabel.LBOUND, self.TYPE(gl.BOUND_TYPE).size)
def visit_UBOUND(self, node):
- yield node.operands[1]
+ yield self.visit(node.operands[1])
self.ic_param(gl.BOUND_TYPE, node.operands[1].t)
entry = node.operands[0]
if entry.scope == SCOPE.global_:
diff --git a/src/arch/z80/visitor/function_translator.py b/src/arch/z80/visitor/function_translator.py
index b2b94a0b0..26257203e 100644
--- a/src/arch/z80/visitor/function_translator.py
+++ b/src/arch/z80/visitor/function_translator.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import src.api
from src.api import global_ as gl
from src.api.config import OPTIONS
@@ -17,9 +24,9 @@ class FunctionTranslator(Translator):
REQUIRES = backend.REQUIRES
def __init__(self, backend: Backend, function_list: list[symbols.ID]):
+ super().__init__(backend)
if function_list is None:
function_list = []
- super().__init__(backend)
assert isinstance(function_list, list)
assert all(x.token == "FUNCTION" for x in function_list)
@@ -61,20 +68,20 @@ def visit_FUNCTION(self, node):
if local_var.class_ == CLASS.array and local_var.scope == SCOPE.local:
lbound_label = local_var.mangled + ".__LBOUND__"
ubound_label = local_var.mangled + ".__UBOUND__"
- lbound_needed = not local_var.is_zero_based and local_var.is_dynamically_accessed
-
- bound_ptrs = [lbound_label if lbound_needed else "0", "0"]
- if local_var.ubound_used:
- bound_ptrs[1] = ubound_label
+ lbound_needed = not local_var.is_zero_based and (
+ local_var.is_dynamically_accessed or local_var.lbound_used or OPTIONS.array_check
+ )
+ ubound_needed = local_var.ubound_used or OPTIONS.array_check
+ bound_ptrs = [lbound_label if lbound_needed else "0", ubound_label if ubound_needed else "0"]
- if bound_ptrs != ["0", "0"]:
+ if lbound_needed or ubound_needed:
OPTIONS["__DEFINES"].value["__ZXB_USE_LOCAL_ARRAY_WITH_BOUNDS__"] = ""
if lbound_needed:
l = ["%04X" % bound.lower for bound in local_var.bounds]
bound_tables.append(LabelledData(lbound_label, l))
- if local_var.ubound_used:
+ if ubound_needed:
l = ["%04X" % bound.upper for bound in local_var.bounds]
bound_tables.append(LabelledData(ubound_label, l))
@@ -106,7 +113,7 @@ def visit_FUNCTION(self, node):
self.ic_lvard(local_var.offset, q)
for i in node.ref.body:
- yield i
+ yield self.visit(i)
self.ic_label("%s__leave" % node.mangled)
diff --git a/src/arch/z80/visitor/translator.py b/src/arch/z80/visitor/translator.py
index 8b47a634b..66725c0d0 100644
--- a/src/arch/z80/visitor/translator.py
+++ b/src/arch/z80/visitor/translator.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from collections.abc import Callable
from typing import Any, NamedTuple
@@ -48,27 +55,27 @@ def visit_CLS(self, node):
def visit_NUMBER(self, node):
__DEBUG__("NUMBER " + str(node))
- yield node.value
+ yield node
def visit_STRING(self, node):
__DEBUG__("STRING " + str(node))
node.t = "#" + self.add_string_label(node.value)
- yield node.t
+ yield node
def visit_END(self, node):
- yield node.children[0]
+ yield self.visit(node.children[0])
__DEBUG__("END")
self.ic_end(node.children[0].t)
def visit_ERROR(self, node):
# Raises an error
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(TYPE.ubyte, node.children[0].t)
self.runtime_call(RuntimeLabel.ERROR, 0)
def visit_STOP(self, node):
"""Returns to BASIC with an error code"""
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(TYPE.ubyte, node.children[0].t)
self.runtime_call(RuntimeLabel.STOP, 0)
self.ic_end(0)
@@ -76,15 +83,16 @@ def visit_STOP(self, node):
def visit_LET(self, node):
assert node.children[0].token == "VAR"
if self.O_LEVEL < 2 or node.children[0].accessed or node.children[1].token == "CONSTEXPR":
- yield node.children[1]
+ yield self.visit(node.children[1])
+
__DEBUG__("LET")
self.emit_let_left_part(node)
def visit_POKE(self, node):
ch0 = node.children[0]
ch1 = node.children[1]
- yield ch0
- yield ch1
+ yield self.visit(ch0)
+ yield self.visit(ch1)
if ch0.token == "VAR" and ch0.class_ != CLASS.const and ch0.scope == SCOPE.global_:
self.ic_store(ch1.type_, "*" + str(ch0.t), ch1.t)
@@ -92,7 +100,7 @@ def visit_POKE(self, node):
self.ic_store(ch1.type_, str(ch0.t), ch1.t)
def visit_RANDOMIZE(self, node):
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(node.children[0].type_, node.children[0].t)
self.runtime_call(RuntimeLabel.RANDOMIZE, 0)
@@ -100,7 +108,7 @@ def visit_LABEL(self, node):
self.ic_label(node.mangled)
def visit_CONST(self, node):
- yield node.symbol
+ yield self.visit(node.symbol)
def visit_VAR(self, node):
__DEBUG__(
@@ -110,7 +118,7 @@ def visit_VAR(self, node):
scope = node.scope
if node.t == node.mangled and scope == SCOPE.global_:
- return
+ return node
p = "*" if node.byref else "" # Indirection prefix
@@ -120,6 +128,8 @@ def visit_VAR(self, node):
offset = node.offset
self.ic_pload(node.type_, node.t, p + str(-offset))
+ return node
+
def visit_CONSTEXPR(self, node):
yield node.t
@@ -131,7 +141,7 @@ def visit_PARAMDECL(self, node):
self.visit_VAR(node)
def visit_UNARY(self, node):
- uvisitor = UnaryOpTranslator(self.backend)
+ uvisitor = UnaryOpTranslator(self.backend, self)
att = f"visit_{node.operator}"
if hasattr(uvisitor, att):
yield getattr(uvisitor, att)(node)
@@ -140,18 +150,20 @@ def visit_UNARY(self, node):
raise InvalidOperatorError(node.operator)
def visit_BUILTIN(self, node):
- yield node.operand
- bvisitor = BuiltinTranslator(self.backend)
+ yield self.visit(node.operand)
+ bvisitor = BuiltinTranslator(self.backend, self)
att = f"visit_{node.fname}"
if hasattr(bvisitor, att):
yield getattr(bvisitor, att)(node)
+ if node.discard_result:
+ self.ic_fparam(node.type_, optemps.new_t())
return
raise InvalidBuiltinFunctionError(node.fname)
def visit_BINARY(self, node):
- yield node.left
- yield node.right
+ yield self.visit(node.left)
+ yield self.visit(node.right)
ins = {"PLUS": "add", "MINUS": "sub"}.get(node.operator, node.operator.lower())
ins_t: dict[str, Callable[[TYPE | symbols.BASICTYPE, Any, Any, Any], None]] = {
@@ -178,18 +190,12 @@ def visit_BINARY(self, node):
}
ins_t[ins](node.left.type_, node.t, str(node.left.t), str(node.right.t))
- def visit_TYPECAST(self, node):
- yield node.operand
- assert node.operand.type_.is_basic
- assert node.type_.is_basic
- self.ic_cast(node.t, node.operand.type_, node.type_, node.operand.t)
-
def visit_FUNCDECL(self, node):
# Delay emission of functions until the end of the main code
gl.FUNCTIONS.append(node.entry)
def visit_CALL(self, node: symbols.CALL):
- yield node.args # arglist
+ yield self.visit(node.args) # arglist
if node.entry.convention == CONVENTION.fastcall:
if len(node.args) > 0: # At least 1 parameter
self.ic_fparam(node.args[0].type_, optemps.new_t())
@@ -200,7 +206,7 @@ def visit_CALL(self, node: symbols.CALL):
def visit_ARGLIST(self, node):
for i in range(len(node) - 1, -1, -1): # visit in reverse order
- yield node[i]
+ yield self.visit(node[i])
def visit_ARGUMENT(self, node):
if not node.byref:
@@ -212,7 +218,7 @@ def visit_ARGUMENT(self, node):
else: # PARAMETER
self.ic_pload(node.type_, node.t, str(node.value.offset))
else:
- yield node.value
+ yield self.visit(node.value)
self.ic_param(node.type_, node.t)
return
@@ -239,7 +245,9 @@ def visit_ARGUMENT(self, node):
return
# Must compute Address of @array(...)
- if node.value.scope == SCOPE.global_ and self.O_LEVEL > 1: # Calculate offset if global variable
+ if (
+ self.O_LEVEL > 1 and node.value.scope == SCOPE.global_ and node.value.offset is not None
+ ): # Calculate offset if global variable
node.value = SymbolBINARY.make_node(
"PLUS",
symbols.UNARY("ADDRESS", node.value.entry, node.value.lineno, type_=self.TYPE(gl.PTR_TYPE)),
@@ -251,13 +259,13 @@ def visit_ARGUMENT(self, node):
node.value = SymbolARRAYACCESS.copy_from(node.value)
node.value = symbols.UNARY("ADDRESS", node.value, node.lineno, type_=self.TYPE(gl.PTR_TYPE))
- yield node.value
+ yield self.visit(node.value)
def visit_ARRAYLOAD(self, node):
scope = node.entry.scope
if node.offset is None:
- yield node.args
+ yield self.visit(node.args)
if scope == SCOPE.global_:
self.ic_aload(node.type_, node.entry.t, node.entry.mangled)
@@ -325,8 +333,8 @@ def visit_LETARRAY(self, node):
scope = arr.scope
if arr.offset is None:
- yield node.children[1] # Right expression
- yield arr
+ yield self.visit(node.children[1]) # Right expression
+ yield self.visit(arr)
if scope == SCOPE.global_:
self.ic_astore(arr.type_, arr.entry.mangled, node.children[1].t)
@@ -338,14 +346,14 @@ def visit_LETARRAY(self, node):
else:
name = arr.entry.data_label
if scope == SCOPE.global_:
- yield node.children[1] # Right expression
+ yield self.visit(node.children[1]) # Right expression
self.ic_store(arr.type_, "%s + %i" % (name, arr.offset), node.children[1].t)
elif scope == SCOPE.local:
t1 = optemps.new_t()
t2 = optemps.new_t()
self.ic_pload(gl.PTR_TYPE, t1, -(arr.entry.offset - self.TYPE(gl.PTR_TYPE).size))
self.ic_add(gl.PTR_TYPE, t2, t1, arr.offset)
- yield node.children[1] # Right expression
+ yield self.visit(node.children[1]) # Right expression
if arr.type_ == Type.string:
self.ic_store(arr.type_, f"*{t2}", node.children[1].t)
@@ -357,7 +365,7 @@ def visit_LETARRAY(self, node):
def visit_LETSUBSTR(self, node):
"""LET X$(a TO b) = Y$"""
# load Y$
- yield node.children[3]
+ self.visit(node.children[3])
if check.is_temporary_value(node.children[3]):
self.ic_param(TYPE.string, node.children[3].t)
@@ -367,10 +375,10 @@ def visit_LETSUBSTR(self, node):
self.ic_param(TYPE.ubyte, 0)
# Load a
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_param(gl.PTR_TYPE, node.children[1].t)
# Load b
- yield node.children[2]
+ yield self.visit(node.children[2])
self.ic_param(gl.PTR_TYPE, node.children[2].t)
# Load x$
str_var = node.children[0]
@@ -396,8 +404,8 @@ def visit_LETARRAYSUBSTR(self, node):
if self.O_LEVEL > 1 and not node.children[0].entry.accessed:
return
- expr = node.children[3] # right expression
- yield expr
+ expr = node.children[3]
+ yield self.visit(node.children[3]) # right expression
if check.is_temporary_value(expr):
self.ic_param(TYPE.string, expr.t)
@@ -406,9 +414,9 @@ def visit_LETARRAYSUBSTR(self, node):
self.ic_param(gl.PTR_TYPE, expr.t)
self.ic_param(TYPE.ubyte, 0)
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_param(gl.PTR_TYPE, node.children[1].t)
- yield node.children[2]
+ yield self.visit(node.children[2])
self.ic_param(gl.PTR_TYPE, node.children[2].t)
node_ = node.children[0]
@@ -417,7 +425,7 @@ def visit_LETARRAYSUBSTR(self, node):
# Address of an array element.
if node_.offset is None:
- yield node_
+ yield self.visit(node_)
if scope == SCOPE.global_:
self.ic_aload(gl.PTR_TYPE, node_.t, entry.mangled)
elif scope == SCOPE.parameter: # TODO: These 2 are never used!??
@@ -437,18 +445,18 @@ def visit_LETARRAYSUBSTR(self, node):
self.runtime_call(RuntimeLabel.LETSUBSTR, 0)
def visit_ARRAYACCESS(self, node):
- yield node.arglist
+ yield self.visit(node.args)
def visit_STRSLICE(self, node):
- yield node.string
+ yield self.visit(node.string)
if node.string.token == "STRING" or node.string.token == "VAR" and node.string.scope == SCOPE.global_:
self.ic_param(gl.PTR_TYPE, node.string.t)
# Now emit the slicing indexes
- yield node.lower
+ yield self.visit(node.lower)
self.ic_param(node.lower.type_, node.lower.t)
- yield node.upper
+ yield self.visit(node.upper)
self.ic_param(node.upper.type_, node.upper.t)
if node.string.token == "VAR" and node.string.mangled[0] == "_" or node.string.token == "STRING":
@@ -459,7 +467,7 @@ def visit_STRSLICE(self, node):
self.runtime_call(RuntimeLabel.STRSLICE, self.TYPE(gl.PTR_TYPE).size)
def visit_FUNCCALL(self, node):
- yield node.args
+ yield self.visit(node.args)
if node.entry.convention == CONVENTION.fastcall:
if len(node.args) > 0: # At least 1
@@ -500,7 +508,7 @@ def visit_READ(self, node):
scope = arr.scope
if arr.offset is None:
- yield arr
+ yield self.visit(arr)
if scope == SCOPE.global_:
self.ic_astore(arr.type_, arr.entry.mangled, t)
@@ -530,7 +538,7 @@ def visit_DO_LOOP(self, node):
self.ic_label(loop_label)
if node.children:
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_jump(loop_label)
self.ic_label(end_loop)
@@ -552,10 +560,10 @@ def visit_DO_WHILE(self, node):
self.LOOPS.append(("DO", end_loop, continue_loop)) # Saves which labels to jump upon EXIT or CONTINUE
if len(node.children) > 1:
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_label(continue_loop)
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_jnzero(node.children[0].type_, node.children[0].t, loop_label)
self.ic_label(end_loop)
self.LOOPS.pop()
@@ -589,20 +597,20 @@ def visit_FOR(self, node):
self.LOOPS.append(("FOR", end_loop, loop_continue)) # Saves which label to jump upon EXIT FOR and CONTINUE FOR
- yield node.children[1] # Gets starting value (lower limit)
+ yield self.visit(node.children[1]) # Gets starting value (lower limit)
self.emit_let_left_part(node) # Stores it in the iterator variable
self.ic_jump(loop_label_start)
# FOR body statements
self.ic_label(loop_body)
- yield node.children[4]
+ yield self.visit(node.children[4])
# Jump here to continue next iteration
self.ic_label(loop_continue)
# VAR = VAR + STEP
- yield node.children[0] # Iterator Var
- yield node.children[3] # Step
+ yield self.visit(node.children[0]) # Iterator Var
+ yield self.visit(node.children[3]) # Step
t = optemps.new_t()
self.ic_add(type_, t, node.children[0].t, node.children[3].t)
self.emit_let_left_part(node, t)
@@ -615,13 +623,13 @@ def visit_FOR(self, node):
direct = True
else:
direct = False
- yield node.children[3] # Step
+ yield self.visit(node.children[3]) # Step
self.ic_jgezero(type_, node.children[3].t, loop_label_gt)
if not direct or node.children[3].value < 0: # Here for negative steps
# Compares if var < limit2
- yield node.children[0] # Value of var
- yield node.children[2] # Value of limit2
+ yield self.visit(node.children[0]) # Value of var
+ yield self.visit(node.children[2]) # Value of limit2
self.ic_lt(type_, node.t, node.children[0].t, node.children[2].t)
self.ic_jzero(TYPE.ubyte, node.t, loop_body)
@@ -631,8 +639,8 @@ def visit_FOR(self, node):
if not direct or node.children[3].value >= 0: # Here for positive steps
# Compares if var > limit2
- yield node.children[0] # Value of var
- yield node.children[2] # Value of limit2
+ yield self.visit(node.children[0]) # Value of var
+ yield self.visit(node.children[2]) # Value of limit2
self.ic_gt(type_, node.t, node.children[0].t, node.children[2].t)
self.ic_jzero(TYPE.ubyte, node.t, loop_body)
@@ -648,7 +656,7 @@ def visit_GOSUB(self, node):
def visit_ON_GOTO(self, node):
table_label = src.api.tmp_labels.tmp_label()
self.ic_param(gl.PTR_TYPE, "#" + table_label)
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(node.children[0].type_, node.children[0].t)
self.runtime_call(RuntimeLabel.ON_GOTO, 0)
self.JUMP_TABLES.append(JumpTable(table_label, node.children[1:]))
@@ -656,7 +664,7 @@ def visit_ON_GOTO(self, node):
def visit_ON_GOSUB(self, node):
table_label = src.api.tmp_labels.tmp_label()
self.ic_param(gl.PTR_TYPE, "#" + table_label)
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(node.children[0].type_, node.children[0].t)
self.runtime_call(RuntimeLabel.ON_GOSUB, 0)
self.JUMP_TABLES.append(JumpTable(table_label, node.children[1:]))
@@ -669,7 +677,7 @@ def visit_CHKBREAK(self, node):
def visit_IF(self, node):
assert 1 < len(node.children) < 4, "IF nodes: %i" % len(node.children)
- yield node.children[0]
+ yield self.visit(node.children[0])
if_label_else = src.api.tmp_labels.tmp_label()
if_label_endif = src.api.tmp_labels.tmp_label()
@@ -678,18 +686,18 @@ def visit_IF(self, node):
else:
self.ic_jzero(node.children[0].type_, node.children[0].t, if_label_endif)
- yield node.children[1] # THEN...
+ yield self.visit(node.children[1]) # THEN...
if len(node.children) == 3: # Has else?
self.ic_jump(if_label_endif)
self.ic_label(if_label_else)
- yield node.children[2]
+ yield self.visit(node.children[2])
self.ic_label(if_label_endif)
def visit_RETURN(self, node):
if len(node.children) == 2: # Something to return?
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_ret(node.children[1].type_, node.children[1].t, "%s__leave" % node.children[0].mangled)
elif len(node.children) == 1:
self.ic_return("%s__leave" % node.children[0].mangled)
@@ -708,10 +716,10 @@ def visit_UNTIL_DO(self, node):
self.LOOPS.append(("DO", end_loop, continue_loop)) # Saves which labels to jump upon EXIT or CONTINUE
if len(node.children) > 1:
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_label(continue_loop)
- yield node.children[0] # Condition
+ yield self.visit(node.children[0]) # Condition
self.ic_jzero(node.children[0].type_, node.children[0].t, loop_label)
self.ic_label(end_loop)
self.LOOPS.pop()
@@ -723,11 +731,11 @@ def visit_WHILE(self, node):
self.LOOPS.append(("WHILE", end_loop, loop_label)) # Saves which labels to jump upon EXIT or CONTINUE
self.ic_label(loop_label)
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_jzero(node.children[0].type_, node.children[0].t, end_loop)
if len(node.children) > 1:
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_jump(loop_label)
self.ic_label(end_loop)
@@ -745,10 +753,10 @@ def visit_WHILE_DO(self, node):
def visit_PLOT(self, node):
self.norm_attr()
TMP_HAS_ATTR = self.check_attr(node, 2)
- yield TMP_HAS_ATTR
- yield node.children[0]
+ yield self.visit(TMP_HAS_ATTR)
+ yield self.visit(node.children[0])
self.ic_param(node.children[0].type_, node.children[0].t)
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_fparam(node.children[1].type_, node.children[1].t)
self.runtime_call(RuntimeLabel.PLOT, 0)
self.HAS_ATTR = TMP_HAS_ATTR is not None
@@ -756,10 +764,10 @@ def visit_PLOT(self, node):
def visit_DRAW(self, node):
self.norm_attr()
TMP_HAS_ATTR = self.check_attr(node, 2)
- yield TMP_HAS_ATTR
- yield node.children[0]
+ yield self.visit(TMP_HAS_ATTR)
+ yield self.visit(node.children[0])
self.ic_param(node.children[0].type_, node.children[0].t)
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_fparam(node.children[1].type_, node.children[1].t)
self.runtime_call(RuntimeLabel.DRAW, 0)
self.HAS_ATTR = TMP_HAS_ATTR is not None
@@ -767,12 +775,12 @@ def visit_DRAW(self, node):
def visit_DRAW3(self, node):
self.norm_attr()
TMP_HAS_ATTR = self.check_attr(node, 3)
- yield TMP_HAS_ATTR
- yield node.children[0]
+ yield self.visit(TMP_HAS_ATTR)
+ yield self.visit(node.children[0])
self.ic_param(node.children[0].type_, node.children[0].t)
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_param(node.children[1].type_, node.children[1].t)
- yield node.children[2]
+ yield self.visit(node.children[2])
self.ic_fparam(node.children[2].type_, node.children[2].t)
self.runtime_call(RuntimeLabel.DRAW3, 0)
self.HAS_ATTR = TMP_HAS_ATTR is not None
@@ -780,12 +788,12 @@ def visit_DRAW3(self, node):
def visit_CIRCLE(self, node):
self.norm_attr()
TMP_HAS_ATTR = self.check_attr(node, 3)
- yield TMP_HAS_ATTR
- yield node.children[0]
+ yield self.visit(TMP_HAS_ATTR)
+ yield self.visit(node.children[0])
self.ic_param(node.children[0].type_, node.children[0].t)
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_param(node.children[1].type_, node.children[1].t)
- yield node.children[2]
+ yield self.visit(node.children[2])
self.ic_fparam(node.children[2].type_, node.children[2].t)
self.runtime_call(RuntimeLabel.CIRCLE, 0)
self.HAS_ATTR = TMP_HAS_ATTR is not None
@@ -797,25 +805,18 @@ def visit_CIRCLE(self, node):
# PRINT, LOAD, SAVE and I/O statements
# -----------------------------------------------------------------------------------------------------
def visit_OUT(self, node):
- yield node.children[0]
- yield node.children[1]
+ yield self.visit(node.children[0])
+ yield self.visit(node.children[1])
self.ic_out(node.children[0].t, node.children[1].t)
def visit_PRINT(self, node):
self.norm_attr()
+ skip_tokens = set(self.ATTR_TMP) | {"PRINT_TAB", "PRINT_AT", "PRINT_COMMA"}
for i in node.children:
- yield i
+ yield self.visit(i)
# Print subcommands (AT, OVER, INK, etc... must be skipped here)
- if (
- i.token
- in (
- "PRINT_TAB",
- "PRINT_AT",
- "PRINT_COMMA",
- )
- + self.ATTR_TMP
- ):
+ if i.token in skip_tokens:
continue
self.ic_fparam(i.type_, i.t)
@@ -837,14 +838,14 @@ def visit_PRINT(self, node):
self.runtime_call(RuntimeLabel.PRINT_EOL, 0)
def visit_PRINT_AT(self, node):
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_param(TYPE.ubyte, node.children[0].t)
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_fparam(TYPE.ubyte, node.children[1].t)
self.runtime_call(RuntimeLabel.PRINT_AT, 0) # Procedure call. Discard return
def visit_PRINT_TAB(self, node):
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(TYPE.ubyte, node.children[0].t)
self.runtime_call(RuntimeLabel.PRINT_TAB, 0)
@@ -852,22 +853,22 @@ def visit_PRINT_COMMA(self, node):
self.runtime_call(RuntimeLabel.PRINT_COMMA, 0)
def visit_LOAD(self, node):
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_param(TYPE.string, node.children[0].t)
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_param(gl.PTR_TYPE, node.children[1].t)
- yield node.children[2]
+ yield self.visit(node.children[2])
self.ic_param(gl.PTR_TYPE, node.children[2].t)
self.ic_param(TYPE.ubyte, int(node.token == "LOAD"))
self.runtime_call(RuntimeLabel.LOAD_CODE, 0)
def visit_SAVE(self, node):
- yield (node.children[0])
+ yield self.visit(node.children[0])
self.ic_param(TYPE.string, node.children[0].t)
- yield (node.children[1])
+ yield self.visit(node.children[1])
self.ic_param(gl.PTR_TYPE, node.children[1].t)
- yield node.children[2]
+ yield self.visit(node.children[2])
self.ic_param(gl.PTR_TYPE, node.children[2].t)
self.runtime_call(RuntimeLabel.SAVE_CODE, 0)
@@ -875,7 +876,7 @@ def visit_VERIFY(self, node):
return self.visit_LOAD(node)
def visit_BORDER(self, node):
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(TYPE.ubyte, node.children[0].t)
self.runtime_call(RuntimeLabel.BORDER, 0)
@@ -886,14 +887,14 @@ def visit_BEEP(self, node):
self.ic_fparam(TYPE.uinteger, DE)
self.runtime_call(RuntimeLabel.BEEPER, 0) # Procedure call. Discard return
else:
- yield node.children[1]
+ yield self.visit(node.children[1])
self.ic_param(TYPE.float, node.children[1].t)
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(TYPE.float, node.children[0].t)
self.runtime_call(RuntimeLabel.BEEP, 0)
def visit_PAUSE(self, node):
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(node.children[0].type_, node.children[0].t)
self.runtime_call(RuntimeLabel.PAUSE, 0)
@@ -904,7 +905,7 @@ def visit_PAUSE(self, node):
# ATTR sentences: INK, PAPER, BRIGHT, FLASH, INVERSE, OVER, ITALIC, BOLD
# -----------------------------------------------------------------------
def visit_ATTR_sentence(self, node):
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(TYPE.ubyte, node.children[0].t)
label = {
@@ -1018,12 +1019,16 @@ def loop_cont_label(self, loop_type):
raise InvalidLoopError(loop_type)
@classmethod
- def default_value(cls, type_: symbols.TYPE, expr) -> list[str]: # TODO: This function must be moved to api.xx
+ def default_value(cls, type_: symbols.TYPING, expr: symbols.EXPR) -> list[str]:
+ # TODO: This function must be moved to api.xx
"""Returns a list of bytes (as hexadecimal 2 char string)"""
- assert isinstance(type_, symbols.TYPE)
+ assert isinstance(type_, symbols.TYPING)
assert type_.is_basic
assert check.is_static(expr)
+ if isinstance(type_, symbols.TYPEREF):
+ type_ = type_.final
+
if expr.token in ("CONSTEXPR", "CONST"): # a constant expression like @label + 1
if type_ in (cls.TYPE(TYPE.float), cls.TYPE(TYPE.string)):
error(expr.lineno, f"Can't convert non-numeric value to {type_.name} at compile time")
diff --git a/src/arch/z80/visitor/translator_inst_visitor.py b/src/arch/z80/visitor/translator_inst_visitor.py
index b31d991f2..ea89f2541 100644
--- a/src/arch/z80/visitor/translator_inst_visitor.py
+++ b/src/arch/z80/visitor/translator_inst_visitor.py
@@ -1,14 +1,22 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.api.constants import TYPE
from src.api.debug import __DEBUG__
from src.arch.interface.quad import Quad
from src.arch.z80.backend import Backend
from src.arch.z80.backend.common import BOOL_t, F16_t, F_t, I8_t, I16_t, I32_t, STR_t, U8_t, U16_t, U32_t
-from src.ast import NodeVisitor
+from src.ast_ import NodeVisitor
from src.symbols import sym
class TranslatorInstVisitor(NodeVisitor):
def __init__(self, backend: Backend):
+ super().__init__()
self.backend = backend
def emit(self, *args: str) -> None:
@@ -18,8 +26,8 @@ def emit(self, *args: str) -> None:
self.backend.MEMORY.append(quad)
@staticmethod
- def TSUFFIX(type_: TYPE | sym.TYPEREF | sym.BASICTYPE) -> str:
- assert isinstance(type_, sym.TYPE) or TYPE.is_valid(type_)
+ def TSUFFIX(type_: TYPE | sym.TYPING) -> str:
+ assert isinstance(type_, sym.TYPING) or TYPE.is_valid(type_)
_TSUFFIX = {
TYPE.byte: I8_t,
@@ -34,17 +42,16 @@ def TSUFFIX(type_: TYPE | sym.TYPEREF | sym.BASICTYPE) -> str:
TYPE.boolean: BOOL_t,
}
- if isinstance(type_, sym.TYPEREF):
+ if isinstance(type_, sym.TYPEREF | sym.BASICTYPE):
type_ = type_.final
assert isinstance(type_, sym.BASICTYPE)
+ type_ = TYPE.to_type(type_.name)
- if isinstance(type_, sym.BASICTYPE):
- return _TSUFFIX[type_.type_]
-
+ assert isinstance(type_, TYPE)
return _TSUFFIX[type_]
@classmethod
- def _no_bool(cls, type_: TYPE | sym.TYPEREF | sym.BASICTYPE) -> str:
+ def _no_bool(cls, type_: TYPE | sym.TYPING | sym.BASICTYPE) -> str:
"""Returns the corresponding type suffix except for bool which maps to U8_t"""
return cls.TSUFFIX(type_) if cls.TSUFFIX(type_) != BOOL_t else U8_t
diff --git a/src/arch/z80/visitor/translator_visitor.py b/src/arch/z80/visitor/translator_visitor.py
index b4d072ab8..e0c7cd5a2 100644
--- a/src/arch/z80/visitor/translator_visitor.py
+++ b/src/arch/z80/visitor/translator_visitor.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from typing import NamedTuple
import src.api.global_ as gl
@@ -10,7 +17,7 @@
from src.arch.z80 import backend
from src.arch.z80.backend.runtime import LABEL_REQUIRED_MODULES, RUNTIME_LABELS
from src.arch.z80.backend.runtime import Labels as RuntimeLabel
-from src.ast.tree import ChildrenList
+from src.ast_.tree import ChildrenList
from src.symbols import sym as symbols
from src.symbols.symbol_ import Symbol
from src.symbols.type_ import Type
@@ -90,11 +97,17 @@ def dumpMemory(MEMORY):
def visit_BLOCK(self, node):
__DEBUG__("BLOCK", 2)
for child in node.children:
- yield child
+ yield self.visit(child)
+
+ def visit_TYPECAST(self, node):
+ yield self.visit(node.operand)
+ assert node.operand.type_.is_basic
+ assert node.type_.is_basic
+ self.ic_cast(node.t, node.operand.type_, node.type_, node.operand.t)
# Visits any temporal attribute
def visit_ATTR_TMP(self, node):
- yield node.children[0]
+ yield self.visit(node.children[0])
self.ic_fparam(node.children[0].type_, node.children[0].t)
label = {
@@ -115,8 +128,11 @@ def runtime_call(self, label: str, num: int):
if label in LABEL_REQUIRED_MODULES:
backend.REQUIRES.add(LABEL_REQUIRED_MODULES[label])
- # This function must be called before emit_strings
def emit_data_blocks(self):
+ """Emits the DATA instruction blocks used by READ.
+ This function must be called before emit_strings() because it will emit access to string variables,
+ marking them as required.
+ """
if not gl.DATA_IS_USED or not gl.DATAS:
return # nothing to do
@@ -241,10 +257,12 @@ def traverse_const(node):
raise InvalidCONSTexpr(node)
@staticmethod
- def check_attr(node, n):
+ def check_attr(node: Symbol, n: int) -> Symbol | None:
"""Check if ATTR has to be normalized
after this instruction has been translated
to intermediate code.
"""
if len(node.children) > n:
return node.children[n]
+
+ return None
diff --git a/src/arch/z80/visitor/unary_op_translator.py b/src/arch/z80/visitor/unary_op_translator.py
index 3984eb289..f81499f76 100644
--- a/src/arch/z80/visitor/unary_op_translator.py
+++ b/src/arch/z80/visitor/unary_op_translator.py
@@ -1,26 +1,41 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.api.constants import SCOPE
+from src.arch.z80.backend import Backend
from src.arch.z80.visitor.translator_visitor import TranslatorVisitor
class UnaryOpTranslator(TranslatorVisitor):
"""UNARY sub-visitor. E.g. -a or bNot pi"""
+ def __init__(self, backend: Backend, parent_visitor: TranslatorVisitor):
+ super().__init__(backend)
+ self.parent_visitor = parent_visitor # Will use this visitor visit() function
+
+ def visit(self, node):
+ return self.parent_visitor.visit(node)
+
def visit_MINUS(self, node):
- yield node.operand
+ yield self.visit(node.operand)
self.ic_neg(node.type_, node.t, node.operand.t)
def visit_NOT(self, node):
- yield node.operand
+ yield self.visit(node.operand)
self.ic_not(node.operand.type_, node.t, node.operand.t)
def visit_BNOT(self, node):
- yield node.operand
+ yield self.visit(node.operand)
self.ic_bnot(node.operand.type_, node.t, node.operand.t)
def visit_ADDRESS(self, node):
scope = node.operand.scope
if node.operand.token == "ARRAYACCESS":
- yield node.operand
+ yield self.visit(node.operand)
# Address of an array element.
if scope == SCOPE.global_:
self.ic_aaddr(node.t, node.operand.entry.mangled)
diff --git a/src/arch/z80/visitor/var_translator.py b/src/arch/z80/visitor/var_translator.py
index 276ecb190..b743819d5 100644
--- a/src/arch/z80/visitor/var_translator.py
+++ b/src/arch/z80/visitor/var_translator.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import src.api
from src.api import global_ as gl
from src.api.config import OPTIONS
@@ -48,7 +55,7 @@ def visit_ARRAYDECL(self, node):
ubound_label = entry.mangled + ".__UBOUND__"
bound_ptrs = ["0", "0"] # NULL by default
- if not entry.is_zero_based and entry.is_dynamically_accessed:
+ if not entry.is_zero_based and (entry.is_dynamically_accessed or entry.lbound_used):
bound_ptrs[0] = lbound_label
if entry.ubound_used or OPTIONS.array_check:
diff --git a/src/arch/zx48k/__init__.py b/src/arch/zx48k/__init__.py
index 58de38638..facc1de1e 100644
--- a/src/arch/zx48k/__init__.py
+++ b/src/arch/zx48k/__init__.py
@@ -1,4 +1,9 @@
-# vim:ts=4:et:sw=4:
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
import src.api.global_
from src.api.constants import TYPE
diff --git a/src/arch/zx48k/backend/__init__.py b/src/arch/zx48k/backend/__init__.py
index c631e4174..7c0a23e98 100644
--- a/src/arch/zx48k/backend/__init__.py
+++ b/src/arch/zx48k/backend/__init__.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.api.config import OPTIONS
from src.arch.z80.backend import (
HI16,
diff --git a/src/arch/zx48k/beep.py b/src/arch/zx48k/beep.py
index 9bc5b557b..63ecd38c2 100755
--- a/src/arch/zx48k/beep.py
+++ b/src/arch/zx48k/beep.py
@@ -1,5 +1,11 @@
#!/usr/bin/env python3
-# vim:ts=4:et:sw=4:
+
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
__doc___ = """This library converts duration,pitch for a beep
from floating point to HL,DE Integers.
diff --git a/src/arch/zx81sd/README.md b/src/arch/zx81sd/README.md
new file mode 100644
index 000000000..1d2858b4c
--- /dev/null
+++ b/src/arch/zx81sd/README.md
@@ -0,0 +1,114 @@
+# `zx81sd` architecture: ZX81 + SD81 Booster
+
+`zx81sd` is an architecture for this compiler targeting a **real ZX81
+with the [SD81 Booster](https://www.sd81.eu/) card** (an interface that
+adds a Spectrum-like screen, AY/beeper sound, a paged memory mapper and
+SD card access).
+
+All code specific to this architecture lives exclusively under:
+
+```
+src/arch/zx81sd/ (compiler backend, this documentation,
+ and the packaging tools under tools/)
+src/lib/arch/zx81sd/ (ASM runtime + BASIC stdlib)
+```
+
+## The port's golden rule
+
+This compiler is shared by every architecture (zx48k, zx128k,
+zxnext...). **The zx81sd port never modifies the shared frontend or
+stdlib/runtime.** The `#include`/`#require` resolution mechanism looks
+first in `src/lib/arch/zx81sd/`, and if it doesn't find the file it
+automatically falls back to `src/lib/arch/zx48k/` (the shared one).
+That's why many zx81sd overrides are full copies of the zx48k version
+with just a few lines changed: the whole file has to be copied, not
+patched — a partial override simply doesn't exist as a concept here.
+
+Before touching anything outside `zx81sd/`, stop: there's probably a
+way to solve it with an override.
+
+## Port status
+
+Functionally complete since 2026-07-02: FP (our own RST $28), graphics
+(PLOT/DRAW/arcs/CIRCLE), sound (BEEP and PLAY over the AY ZonX and the
+beeper), keyboard (INKEY$/INPUT over the ZX81's physical keyboard),
+joystick, the complete MCU library (files, RTC/BAT, voice, memory
+mapper...) and LOAD/SAVE/VERIFY...CODE against SD.
+
+Pending / not audited, remaining screen utilities in the shared stdlib:
+
+- `winscroll.bas`: **confirmed working on real hardware** — no ROM
+ dependency, no override needed (like `scroll.bas` before its fix, or
+ `4inarow.bas`). No source changes needed either
+ (`examples/winscroll.bas` as-is).
+- `putchars.bas`: **confirmed working on real hardware** — no ROM
+ dependency, no override needed
+ (`examples/sd81/putcharstile.bas`).
+- `puttile.bas`: **confirmed working on real hardware** — needed a
+ zx81sd override for hardcoded screen/attribute base constants, same
+ bug class as `print42.bas`/`print64.bas` (fixed the same way,
+ self-modifying code patched at runtime).
+- `screen.bas`: **does depend on the ROM** (`$2538`/`$5C65`/`$19E8`,
+ fixed Spectrum routines and sysvars to read back a screen character)
+ — will need a real override, not just an audit.
+- `print42.bas`/`print64.bas`: **ported and confirmed working on real
+ hardware** — fixed sysvars → zx81sd equivalents, and the screen/
+ attribute base constants are patched at runtime (self-modifying
+ code) instead of being fixed.
+
+See [doc/BASIC_CHANGES.md](doc/BASIC_CHANGES.md) for the general
+pattern behind this kind of fix. The `maskedsprites.bas`/MSFS port
+(masked sprites over the memory mapper) is also in progress, still not
+fully working.
+
+## Documentation
+
+- **[doc/PORTING_GUIDE.md](doc/PORTING_GUIDE.md)** — step-by-step
+ checklist for porting a Spectrum program to zx81sd. Start here.
+- **[doc/USAGE.md](doc/USAGE.md)** — how to compile a program, package
+ it for the ZX81 and load it from the SD card.
+- **[doc/PRECAUTIONS.md](doc/PRECAUTIONS.md)** — what to keep in mind
+ when writing or porting software for this architecture (memory map,
+ sysvars, keyboard, things that do NOT exist here even though they do
+ on a Spectrum).
+- **[doc/SYSVARS.md](doc/SYSVARS.md)** — full Spectrum ↔ zx81sd sysvar
+ and I/O port (ULA, screen address) equivalence table.
+- **[doc/BASIC_CHANGES.md](doc/BASIC_CHANGES.md)** — what BASIC source
+ changes were needed to port each official example in `examples/`
+ (with the why for each one). Mandatory starting point before porting
+ a new example: it's almost always one of the patterns already
+ cataloged there.
+- **[doc/MAP.md](doc/MAP.md)** — detailed technical log of every bug
+ found and fixed during the port (ASM runtime, not BASIC sources),
+ with the investigation trace for each one. The document to check
+ when something fails at runtime in a way that resembles an
+ already-solved bug.
+
+## Examples
+
+The example programs already adapted/tested for this architecture are
+in [`examples/sd81/`](../../../examples/sd81/) (alongside the rest of
+the compiler's `examples/`). The detail of what had to be changed in
+each one, and why, is in
+[doc/BASIC_CHANGES.md](doc/BASIC_CHANGES.md).
+
+## Packaging tools
+
+[`tools/split_sd81.py`](tools/split_sd81.py) splits the flat binary
+produced by the compiler into 8KB pages and generates the `.p` loader
+for the ZX81; [`tools/zx81_p_loader.py`](tools/zx81_p_loader.py) is the
+BASIC tokenizer it uses to generate it; [`tools/boot1.asm`](tools/boot1.asm)/
+[`tools/boot1.bin`](tools/boot1.bin) are the second-stage loader (stage
+1), fixed for every program. See [doc/USAGE.md](doc/USAGE.md) for the
+full compile → package → load flow. The wider set of debugging/
+diagnostic sources used during the port's development (not tools, just
+one-off tests) lives in a private companion repository, not this one.
+
+## Related repositories
+
+- **[SD81 Booster](https://codeberg.org/Retrostuff/SD81-Booster)** —
+ the hardware/firmware for the interface this port targets.
+- **[EightyOne Cross-platform](https://codeberg.org/wilco2009/EightyOne-CrossPlatform)** —
+ the emulator used during development to test without real hardware.
+- **[CPM_SD81](https://codeberg.org/wilco2009/CPM_SD81)** — CP/M on the
+ SD81 Booster.
diff --git a/src/arch/zx81sd/__init__.py b/src/arch/zx81sd/__init__.py
new file mode 100644
index 000000000..f4ec43c0c
--- /dev/null
+++ b/src/arch/zx81sd/__init__.py
@@ -0,0 +1,34 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# ZX81 + SD81 Booster architecture for Boriel ZX BASIC compiler
+# Inherits from zx48k, overriding the backend for SD81 hardware.
+# --------------------------------------------------------------------
+
+import src.api.global_
+from src.api.constants import TYPE
+from src.arch.z80 import (
+ FunctionTranslator,
+ Translator,
+ VarTranslator,
+ beep,
+ optimizer, # noqa
+)
+from src.arch.zx81sd import backend # noqa
+
+__all__ = (
+ "FunctionTranslator",
+ "Translator",
+ "VarTranslator",
+ "beep",
+)
+
+# -----------------------------------------
+# Arch initialization setup (same as zx48k)
+# -----------------------------------------
+src.api.global_.PARAM_ALIGN = 2
+src.api.global_.BOUND_TYPE = TYPE.uinteger
+src.api.global_.SIZE_TYPE = TYPE.ubyte
+src.api.global_.PTR_TYPE = TYPE.uinteger
+src.api.global_.STR_INDEX_TYPE = TYPE.uinteger
+src.api.global_.MIN_STRSLICE_IDX = 0
+src.api.global_.MAX_STRSLICE_IDX = 65534
diff --git a/src/arch/zx81sd/backend/__init__.py b/src/arch/zx81sd/backend/__init__.py
new file mode 100644
index 000000000..792f01277
--- /dev/null
+++ b/src/arch/zx81sd/backend/__init__.py
@@ -0,0 +1,29 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# ZX81 + SD81 Booster backend for Boriel ZX BASIC compiler
+# --------------------------------------------------------------------
+
+from src.arch.z80.backend import (
+ HI16,
+ INITS,
+ LO16,
+ MEMINITS,
+ REQUIRES,
+ TMP_COUNTER,
+ TMP_STORAGES,
+ Float,
+)
+
+from .main import Backend
+
+__all__ = [
+ "HI16",
+ "INITS",
+ "LO16",
+ "MEMINITS",
+ "REQUIRES",
+ "TMP_COUNTER",
+ "TMP_STORAGES",
+ "Backend",
+ "Float",
+]
diff --git a/src/arch/zx81sd/backend/generic.py b/src/arch/zx81sd/backend/generic.py
new file mode 100644
index 000000000..f9492c660
--- /dev/null
+++ b/src/arch/zx81sd/backend/generic.py
@@ -0,0 +1,29 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# ZX81 + SD81 Booster — END opcode handler
+# --------------------------------------------------------------------
+
+from src.arch.interface.quad import Quad
+from src.arch.z80.backend import Bits16, common
+
+
+def _end(ins: Quad):
+ """End-of-program sequence for ZX81 + SD81 Booster.
+
+ There's no ROM or BASIC to return to: it stops the CPU safely.
+ If END appears more than once in the program (early exits), later
+ occurrences generate a JP to the first emitted END block.
+ """
+ output = Bits16.get_oper(ins[1])
+ output.append("ld b, h")
+ output.append("ld c, l")
+
+ if common.FLAG_end_emitted:
+ return output + [f"jp {common.END_LABEL}"]
+
+ common.FLAG_end_emitted = True
+
+ output.append(f"{common.END_LABEL}:")
+ output.append("di")
+ output.append("halt")
+ return output
diff --git a/src/arch/zx81sd/backend/main.py b/src/arch/zx81sd/backend/main.py
new file mode 100644
index 000000000..70c32131c
--- /dev/null
+++ b/src/arch/zx81sd/backend/main.py
@@ -0,0 +1,213 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# ZX81 + SD81 Booster backend — program prologue / epilogue
+# --------------------------------------------------------------------
+
+from src.api.config import OPTIONS
+from src.api.options import Action
+from src.arch.z80.backend import Backend as Z80Backend
+from src.arch.z80.backend import ICInfo, common
+from src.arch.z80.backend.icinstruction import ICInstruction
+from src.arch.z80.backend.runtime import NAMESPACE
+from src.arch.z80.peephole import engine
+
+from .generic import _end
+
+# ---------------------------------------------------------------------------
+# ZX81 + SD81 Booster architecture constants
+# ---------------------------------------------------------------------------
+
+# Memory map (flat, ORG $0000):
+# $0000-$00FF RST vector table, emitted directly below (emit_prologue)
+# + padding up to $0100
+# $0100-$0FFF stage 2 bootstrap (prologue) + system routines
+# $1000-$7FFF ZX BASIC runtime + user code (28 KB)
+# $8000-$80FF runtime sysvars
+# $8100-$BFFF heap + user data (~15.75 KB)
+# $C000-$D7FF Spectrum screen bitmap (block 6, dedicated page)
+# $D800-$DAFF screen attributes
+# $E000-$FFFF block 7 — data banking (maps, sprites...)
+
+_ORG = 0x0000 # the binary starts at $0000 (RST vectors pad up to $0100)
+_STAGE2_ENTRY = 0x0100 # stage 2 bootstrap's entry point
+
+_HEAP_ADDR = 0x8100 # heap in the data area ($8100-$BFFF)
+_HEAP_SIZE = 0x3EFF # ~15.75 KB
+
+# SD81 pages assigned to each block (loaded by BASIC before the jump)
+# Page 8 -> block 0 ($0000-$1FFF) <- stage 1 only maps this one
+# Page 9 -> block 1 ($2000-$3FFF) ┐
+# Page 10 -> block 2 ($4000-$5FFF) │ stage 2 (here) maps these
+# Page 11 -> block 3 ($6000-$7FFF) │
+# Page 12 -> block 4 ($8000-$9FFF) │ (data, not executable without MC45)
+# Page 13 -> block 5 ($A000-$BFFF) ┘
+
+_PAGE_MAP = [
+ (1, 9), # block 1 -> page 9
+ (2, 10), # block 2 -> page 10
+ (3, 11), # block 3 -> page 11
+ (4, 12), # block 4 -> page 12
+ (5, 13), # block 5 -> page 13
+ (7, 15), # block 7 -> page 15 (data banking: large static tables via explicit ORG)
+]
+
+_SD81_PAGE_PORT = 0xE7 # memory mapper port (full mode: OUT (C), A)
+_STACK_TOP = 0x7FFF # stack at the top of the executable area
+
+
+def _map_block(block: int, page: int) -> list[str]:
+ """Emits OUT (C), A to map a page to a block (full mode, 64 pages)."""
+ return [
+ f"ld b, {page}",
+ f"ld a, {block}",
+ f"ld c, {_SD81_PAGE_PORT:#04x}",
+ "out (c), a",
+ ]
+
+
+class Backend(Z80Backend):
+ def init(self):
+ super().init()
+
+ OPTIONS(Action.ADD_IF_NOT_DEFINED, name="org", type=int, default=_ORG)
+
+ # The generic Z80 backend (super().init(), right above) already
+ # registers "heap_size" (4768) and "heap_address" (None) with
+ # ADD_IF_NOT_DEFINED, so an ADD_IF_NOT_DEFINED here would be a
+ # no-op and the heap would end up reserved inline (DEFS) with
+ # 4768 bytes inside the executable area. Assigned directly so
+ # the heap lives in the data area $8100-$BFFF; the CLI's
+ # --heap-address/-H flags are applied afterward and can still
+ # override these values.
+ OPTIONS.heap_size = _HEAP_SIZE
+ OPTIONS.heap_address = _HEAP_ADDR
+
+ OPTIONS(Action.ADD_IF_NOT_DEFINED, name="heap_start_label", type=str, default=f"{NAMESPACE}.ZXBASIC_MEM_HEAP")
+ OPTIONS(Action.ADD_IF_NOT_DEFINED, name="heap_size_label", type=str, default=f"{NAMESPACE}.ZXBASIC_HEAP_SIZE")
+ OPTIONS(Action.ADD_IF_NOT_DEFINED, name="headerless", type=bool, default=False, ignore_none=True)
+
+ self._QUAD_TABLE.update(
+ {
+ ICInstruction.END: ICInfo(1, _end),
+ }
+ )
+
+ engine.main()
+
+ @staticmethod
+ def emit_prologue() -> list[str]:
+ """
+ Program prologue for ZX81 + SD81 Booster.
+
+ Structure of the generated binary (ORG $0000):
+ $0000-$00FF RST vector table (emitted directly below)
+ $0100 START_LABEL (stage 2 bootstrap's entry point)
+ - mapping blocks 1-5 to their final pages
+ - SP = $7FFF
+ - CALL <#init routines> (SD81_INIT_SYSVARS, etc.)
+ - JP __MAIN_LABEL__
+
+ The (external) stage 1 bootstrap, in the BASIC loader at $6000,
+ has already:
+ - Set HFILE=$C000 and activated Spectrum mode (POKE 2045, 172)
+ - Disabled memory-mapped IO (POKE 2056)
+ - Disabled interrupts (DI)
+ - Mapped block 0 -> page 8 (JP $0100 now runs on clean RAM)
+ """
+ # -- Heap definitions ------------------------------------------
+ heap_init = [f"{common.DATA_LABEL}:"]
+
+ if common.REQUIRES.intersection(common.MEMINITS) or f"{NAMESPACE}.__MEM_INIT" in common.INITS:
+ heap_init.append("; Defines HEAP SIZE\n" + OPTIONS.heap_size_label + " EQU " + str(OPTIONS.heap_size))
+ if OPTIONS.heap_address is None:
+ heap_init.append(OPTIONS.heap_start_label + ":")
+ heap_init.append(f"DEFS {OPTIONS.heap_size}")
+ else:
+ heap_init.append("; Defines HEAP ADDRESS\n" + OPTIONS.heap_start_label + f" EQU {OPTIONS.heap_address}")
+
+ heap_init.append(
+ "; Defines USER DATA Length in bytes\n"
+ + f"{NAMESPACE}.ZXBASIC_USER_DATA_LEN"
+ + f" EQU {common.DATA_END_LABEL} - {common.DATA_LABEL}"
+ )
+ heap_init.append(f"{NAMESPACE}.__LABEL__.ZXBASIC_USER_DATA_LEN" + f" EQU {NAMESPACE}.ZXBASIC_USER_DATA_LEN")
+ heap_init.append(f"{NAMESPACE}.__LABEL__.ZXBASIC_USER_DATA EQU {common.DATA_LABEL}")
+
+ # -- RST vector table ($0000-$00FF) ---------------------------------
+ # Must be the very first thing in the binary. Each RST takes 8 bytes.
+ # An absolute org is used for every entry: the assembler fills the
+ # gaps with zeros, which is correct for a non-executable area.
+ output = ["org $0000"]
+ output.append("jp $0100") # $0000: reset / RST 0 -> stage 2
+ output.append("org $0008")
+ output.append("di") # $0008: RST $08 (Spectrum error handler)
+ output.append("halt")
+ output.append("org $0010")
+ output.append("di") # $0010-$0037: unused RSTs
+ output.append("halt")
+ output.append("org $0018")
+ output.append("di")
+ output.append("halt")
+ output.append("org $0020")
+ output.append("di")
+ output.append("halt")
+ output.append("org $0028")
+ output.append("jp .core.FP_CALC_ENTRY") # $0028: RST $28 -> our own FP calculator
+ output.append("org $0030")
+ output.append("di")
+ output.append("halt")
+ output.append("org $0038")
+ # $0038: RST $38 (IM1 interrupt). Interrupts stay disabled for the
+ # whole program, so this should never really be reached; if
+ # something did reactivate them, behave like a real Z80 HALT
+ # instead of hanging forever (di+halt): wait for the next VSYNC
+ # pulse (port $AF, bits 6-1 = pulse counter, reset on read) and
+ # return, resuming right after the RST $38 that got us here.
+ # AF must be preserved: ported code that executes EI (Spectrum
+ # habit) makes stray ZX81 INTs land here mid-computation, and
+ # clobbering A/F corrupted arithmetic at random (see MAP.md,
+ # oilpanic sprite(13,2) bug).
+ output.append("push af")
+ output.append(".core.__RST38_WAIT:")
+ output.append("in a, ($AF)")
+ output.append("and $7E")
+ output.append("jr z, .core.__RST38_WAIT")
+ output.append("pop af")
+ output.append("ret")
+ output.append("org $0066")
+ output.append("retn") # $0066: NMI disabled, but the vector must exist
+
+ # -- Stage 2 bootstrap at $0100 ------------------------------------
+ output.append(f"org {_STAGE2_ENTRY}")
+ output.append(f"{common.START_LABEL}:")
+
+ if OPTIONS.headerless:
+ output.extend(heap_init)
+ return output
+
+ # Mapping blocks 1-5 to their final pages.
+ # Block 0 was already mapped to page 8 by the external stage 1.
+ for block, page in _PAGE_MAP:
+ output.extend(_map_block(block, page))
+
+ # Stack at the top of the executable area
+ output.append(f"ld sp, {_STACK_TOP:#06x}")
+
+ # Calls to initialization routines registered with #init
+ # (SD81_INIT_SYSVARS and any other one from the included runtime)
+ output.extend(f"call {label}" for label in sorted(common.INITS))
+
+ # Jump to the user's program
+ output.append(f"jp {common.MAIN_LABEL}")
+
+ output.extend(heap_init)
+ return output
+
+ @staticmethod
+ def emit_epilogue() -> list[str]:
+ output = list(common.AT_END)
+ if OPTIONS.autorun:
+ output.append(f"END {common.START_LABEL}")
+ else:
+ output.append("END")
+ return output
diff --git a/src/arch/zx81sd/doc/BASIC_CHANGES.md b/src/arch/zx81sd/doc/BASIC_CHANGES.md
new file mode 100644
index 000000000..255cb592c
--- /dev/null
+++ b/src/arch/zx81sd/doc/BASIC_CHANGES.md
@@ -0,0 +1,481 @@
+# BASIC source changes needed to port official examples to zx81sd
+
+This file records, for every official `examples/` sample tried on
+zx81sd, whether the BASIC source needed touching (never the original:
+always an adapted copy in `examples/sd81/`) or just different compile
+flags.
+
+Ground rule: the official zxbasic source **is never modified**. When a
+program depends on a Spectrum-specific sysvar or absolute address, an
+adapted copy is made next to the original.
+
+---
+
+## 1. `examples/english/comecoquitos.bas` — NO source changes
+
+Compiles as-is. Only needs command-line flags:
+
+```
+python -m src.zxbc.zxbc comecoquitos.bas --arch zx81sd --string-base 1 --array-base 1 -o comecocos.bin
+```
+
+**Why**: the source uses 1-based string indexing (classic Sinclair
+BASIC style, `l$(f)`, slicing). With zxbasic's default 0-based indexing,
+the collision comparisons end up shifted by one position — not a bug in
+the port, it happens exactly the same on zx48k without those flags.
+
+---
+
+## 2. `examples/english/snake_en.bas` → `examples/sd81/snake_sd81.bas`
+
+One single-line change:
+
+```diff
+-73 POKE UINTEGER 23675, @udg(0, 0): REM Sets UDG variable to first element
++73 POKE UINTEGER $8002, @udg(0, 0): REM zx81sd's UDG sysvar (was 23675 on Spectrum)
+```
+
+**Why**: `23675` ($5C7B) is the absolute address of the `UDG` sysvar in
+the **Spectrum**'s memory map. On zx81sd the same sysvar lives at
+`$8002` (see `SYSVAR_BASE+2` in `src/lib/arch/zx81sd/runtime/sysvars.asm`).
+Without the change, the POKE lands in free RAM and the UDGs (the
+snake's head and fruit) never activate — the game would run but draw
+blank spaces instead of the graphics.
+
+**General pattern**: any example that does `POKE`/`PEEK` on a Spectrum
+sysvar address instead of using the higher-level function (here it
+would have been enough not to use raw UDG) needs this kind of address
+translation. See also flights.bas below.
+
+---
+
+## 3. `examples/flights.bas` → `examples/sd81/flights_sd81.bas`
+
+Three content changes (plus cosmetic trailing-whitespace cleanup with
+no functional effect, not listed):
+
+### 3.1 Remove a Spectrum sysvar POKE with no equivalent
+
+```diff
+-1 POKE 23658,8: BORDER 1: PAPER 1: INK 7: CLS
++1 BORDER 1: PAPER 1: INK 7: CLS
+```
+
+**Why**: `23658` ($5C6A) is `REPDEL` (key repeat delay) on the
+Spectrum — that sysvar doesn't exist on zx81sd, and the POKE would
+write to an arbitrary RAM address. Simply removed: the effect
+(adjusting keyboard auto-repeat) has no equivalent and isn't needed by
+the game.
+
+### 3.2 Translate the COORDS sysvar to its real zx81sd address
+
+```diff
+-2295 IF gc<>0 THEN PLOT OVER 1;x1,168+16-y1: DRAW OVER 1;x2-PEEK 23677,168+16-y2-PEEK 23678: END IF
++2295 IF gc<>0 THEN PLOT OVER 1;x1,168+16-y1: DRAW OVER 1;x2-PEEK $8004,168+16-y2-PEEK $8005: END IF
+```
+(and three more identical occurrences on lines 2370, 2378 and 2445)
+
+**Why**: `23677`/`23678` ($5C7D/$5C7E) are the Spectrum's `COORDS`
+sysvar (last `PLOT` coordinate). On zx81sd it lives at `$8004`/`$8005`
+(`COORDS EQU SYSVAR_BASE+$04` in `sysvars.asm`). Without the
+translation, the `PEEK`s read the compiled program's own RAM instead
+of the actual coordinate, and the horizon-line `DRAW` comes out with a
+random offset depending on that RAM's contents — this was the first
+symptom reported ("the horizon line draws wrong").
+
+### 3.3 Key comparisons to lowercase
+
+```diff
+-3010 IF k$="S" THEN LET pow=pow-1: END IF
+-3020 IF k$="F" THEN LET pow=pow+1: END IF
+-3030 IF k$="Q" THEN LET pt=pt+1: END IF
+-3040 IF k$="A" THEN LET pt=pt-1: END IF
+-3050 IF k$="O" AND rl>-30 THEN LET rl=rl-1: END IF
+-3060 IF k$="P" AND rl<30 THEN LET rl=rl+1: END IF
++3010 IF k$="s" THEN LET pow=pow-1: END IF
++3020 IF k$="f" THEN LET pow=pow+1: END IF
++3030 IF k$="q" THEN LET pt=pt+1: END IF
++3040 IF k$="a" THEN LET pt=pt-1: END IF
++3050 IF k$="o" AND rl>-30 THEN LET rl=rl-1: END IF
++3060 IF k$="p" AND rl<30 THEN LET rl=rl+1: END IF
+```
+(and the "Y"/"N" comparisons on lines 6150/6160)
+
+**Why (this change matches the old keyboard scheme — see the "no
+longer needed" note below)**: at the time `flights.bas` was ported,
+`zx81sd/runtime/io/keyboard/keyscan.asm` (a hand-written rescan of the
+**ZX81's own physical keyboard**, since the SD81 Booster has no
+Spectrum keyboard) only ever returned lowercase, regardless of
+whether `SHIFT` was pressed — `SHIFT+letter` didn't produce the
+uppercase of that letter, but one of the ZX81's classic symbols (colon,
+quotes, `+`, `-`...). With that scheme there was no physical
+combination that reproduced Spectrum-style "CAPS SHIFT+S", and the only
+possible adaptation was comparing against the unshifted lowercase.
+Same reason, already solved a different way in `comecoquitos.bas`
+(there the source already compared in lowercase, "o","p","q","a","y","n"
+— its author just happened to type it that way, nothing we had to
+touch) and in `snake_sd81.bas` (compares "O"/"o" with `OR`, covering
+both cases without needing any change).
+
+**No longer needed (2026-07-04)**: `keyscan.asm` was redesigned so a
+direct key press gives lowercase and `SHIFT+letter` gives the
+UPPERCASE of that letter (plus `SHIFT+"2"` as a persistent CAPS LOCK,
+and the ZX81's classic symbols reachable with `"."+key` from
+`INPUT()`). With the current scheme, `INKEY$="S"` would already work
+naturally by pressing `SHIFT+S`, exactly like on a Spectrum — the
+case change in `flights_sd81.bas` documented above was necessary at the
+time but wouldn't be needed if ported today from scratch. Kept here as
+a record instead of reverting the already-tested copy. Full detail of
+the redesign in [MAP.md](MAP.md), section "New keyboard scheme".
+
+---
+
+## 4. `examples/english/4inarow.bas` — NO source changes
+
+Compiles with default flags:
+
+```
+python -m src.zxbc.zxbc 4inarow.bas --arch zx81sd -o row4.bin
+```
+
+Its arrays use indices 1..8, which fit in the default 0-based indexing
+without any slicing collision (it doesn't do string slicing), and its
+key comparisons already use lowercase ("y"/"n" in the prompts). It
+exercises the arc code (`DRAW 8,0,PI`) and `CIRCLE` without needing any
+patch.
+
+---
+
+## 5. `examples/scroll.bas` — NO source changes
+
+Compiles with default flags:
+
+```
+python -m src.zxbc.zxbc scroll.bas --arch zx81sd -o scroll.bin
+```
+
+The problem wasn't in the example but in the library: `scroll.bas` had
+no zx81sd version — the zx48k one was used as-is, and its 8 subs
+(`ScrollRight/Left/Up/Down` + their `*Aligned` variants) call `$22AC`
+(the Spectrum ROM's *PIXEL-ADD* routine, which doesn't exist on
+zx81sd). `src/lib/arch/zx81sd/stdlib/scroll.bas` was created, identical
+except those 8 calls are replaced with `call PIXEL_ADDR` (our own
+implementation, same register contract as the ROM — see
+[MAP.md](MAP.md)). The example itself needed no change, same as
+4inarow.bas.
+
+---
+
+## 5b. `examples/winscroll.bas` — NO source changes, no library override needed either
+
+Compiles with default flags:
+
+```
+python -m src.zxbc.zxbc winscroll.bas --arch zx81sd -o winscroll.bin
+```
+
+Unlike `scroll.bas`, `winscroll.bas` (character-cell window scroll:
+`WinScrollRight/Left/Up/Down(row, col, width, height)`) has **no ROM
+dependency at all** — it computes everything from `SCREEN_ADDR`/
+`SCREEN_ATTR_ADDR` (read dynamically, not hardcoded) and only pulls in
+`sysvars.asm` via `#require` (a link-time dependency tag, not a
+textual `#include` — it doesn't risk the `INCBIN`-inline bug described
+in [PRECAUTIONS.md](PRECAUTIONS.md) section 8). No zx81sd override was
+needed; the shared zx48k library is used as-is via the normal fallback
+mechanism. Verified by simulation (screen and attribute area fully
+touched, clean `HALT` at `__END_PROGRAM`, no illegal writes).
+**Confirmed working on real hardware.**
+
+---
+
+## 5c. `putchars.bas`/`puttile.bas` — one clean, one needed a zx81sd override
+
+New example `examples/sd81/putcharstile.bas` (no official example to
+adapt) exercises both libraries:
+
+- **`putchars.bas`** (`putChars`/`getChars`/`paint`/`paintData`/
+ `getPaintData`/`putCharsOverMode`): audited, **no ROM dependency at
+ all** — every address is computed from `SCREEN_ADDR`/
+ `SCREEN_ATTR_ADDR` read dynamically, and the file's own
+ `#require "sysvars.asm"` is the safe link-time tag (see section 5b
+ above), not a textual include. No zx81sd override needed; used as-is.
+- **`puttile.bas`** (`putTile`, places a 16×16 px tile): **did need an
+ override**. Unlike `putchars.bas`, it hardcodes the screen/attribute
+ base as **immediate constants** — `add a,64` (screen, the Spectrum
+ ROM's `$4000` high byte) and `add a,88` (attributes, `$5800`'s high
+ byte) — instead of reading `SCREEN_ADDR`/`SCREEN_ATTR_ADDR`. Same bug
+ class as `print42.bas`/`print64.bas`. Fixed the same way:
+ `src/lib/arch/zx81sd/stdlib/puttile.bas` patches both constants once
+ on entering `putTile()` (self-modifying code on the `ADD A,n`
+ instructions themselves), reading the real high byte of
+ `SCREEN_ADDR`/`SCREEN_ATTR_ADDR` at that point. Everything else
+ (the interrupt save/restore dance, the stack-based pixel pushing) is
+ architecture-agnostic and copied unchanged — the `ld a,i` / `jp po`
+ idiom naturally never re-enables interrupts on zx81sd, since IFF2 is
+ always reset here (the runtime never does `EI`).
+
+**Debugging note**: the first simulation run of this example looked
+broken (`paint()` seemingly painted only 1 of 3 rows, `putTile`'s area
+looked empty) — turned out to be a false alarm caused by the example's
+own final `PRINT AT` landing on the screen's last row (23), which
+combined with `input()`'s cursor handling to trigger a normal scroll
+that shifted the drawing up before the check ran. Moving the final
+prompt to row 20 fixed it; `paint()`/`putTile()` were correct all
+along. Verified with breakpoints right after each function returns
+(not just at the end) to catch this class of false negative.
+**Confirmed working on real hardware.**
+
+---
+
+## 6. `examples/maskedsprites.bas` → `examples/sd81/maskedsprites_sd81.bas`
+
+**In progress — still not fully working.**
+
+One substantial change: `WaitForNewFrame` (defined in the example
+itself, not the library) rewritten to not depend on interrupts:
+
+```diff
+- ld de,23672
+- ld c,a ; A = C = minimumNumberofFramesToWaitSinceLastWait
+- READ_IFF2
+- ex af,af'
+- ei ; interrupts MUST be enabled before HALT
+- halt
++ ld de, FRAMES
++ ld c,a ; C = minimumNumberofFramesToWaitSinceLastWait
++ call VSYNC_TICK ; guarantees at least one frame waited (replaces EI+HALT)
+ wait:
+ ld a,(de)
+ sub (hl)
+ cp c
+- jr c,wait
+- ld a,(de)
+- ld (hl),a
+- ex af,af'
+- ret pe
+- di
+- RET
++ jr nc,enough
++ call VSYNC_TICK
++ jr wait
++enough:
++ ld a,(de)
++ ld (hl),a
++ ret
+```
+
+**Why**: `23672` is the Spectrum ROM's `FRAMES` counter, automatically
+incremented by the 50Hz IM1 interrupt. The original does one initial
+`HALT` (waits for the interrupt) and then a loop that trusts the
+interrupt to keep incrementing it in the background while the loop just
+*checks* without waiting again. On zx81sd there are no interrupts
+(permanent DI; the `$0038` vector is a `DI;HALT` trap, not a real
+handler) — that `HALT` would never wake up. It's replaced with
+`VSYNC_TICK` (polling the SD81 Booster hardware's VSYNC pulse counter
+through a port, the same routine `PAUSE` already uses), called
+explicitly on every loop iteration that needs to wait.
+
+**Update — MSFS ported to the zx81sd mapper (block 7)**: the risk
+above no longer applies. `src/lib/arch/zx81sd/stdlib/cb/maskedsprites.bas`
+was created (a full override of the shared library, which stays
+untouched): `SetBankPreservingINTs`/`GetBankPreservingRegs` rewritten
+in hand-written ASM over port `$E7` (block 7, `$E000-$FFFF` — reserved
+in our memory map for "data banking, maps, sprites"), and
+`MaskedSpritesFileSystemStart` fixed at `$E000` instead of "whatever's
+left up to `$FFFF`" (which assumed flat Spectrum RAM). The rest of MSFS
+(hundreds of lines of block-bitmap arithmetic) is bank/address-agnostic
+and was copied without touching a single line. Full detail, including a
+real bug caught in the process (registers not preserved when written
+in BASIC instead of ASM), in [MAP.md](MAP.md).
+
+Despite the fixes above (confirmed by simulation and, partially, on a
+real emulator/hardware), the library in its current state **still
+doesn't fully work** — work in progress, don't consider this example
+closed yet.
+
+---
+
+## 7. `examples/sd81/pong.bas` — classic transcription, one ASM change
+
+Not an official `examples/` sample but a classic Sinclair-BASIC-style
+transcription of Pong (GOSUB/line numbers), added directly under
+`examples/sd81/`. It already uses lowercase key comparisons
+("4","q","3","a"), matching the current keyboard scheme with no change
+needed — the only change was replacing the screen sync:
+
+```diff
+ ASM
+- call VSYNC_TICK ; guarantees at least one frame waited (replaces EI+HALT)
++ call .core.VSYNC_TICK ; guarantees at least one frame waited (replaces EI+HALT)
+ ; halt ; Avoids screen flickering
+ END ASM
+```
+
+**Why**: `vsync.asm` wraps `VSYNC_TICK` in
+`push namespace core ... pop namespace`; calling it from an
+`ASM ... END ASM` block that isn't inside that namespace needs the
+full `.core.VSYNC_TICK` prefix — the same error pattern as in
+`maskedsprites_sd81.bas` (see [PRECAUTIONS.md](PRECAUTIONS.md),
+section 5). Without the prefix, the compiler gives
+`Undefined GLOBAL label '.VSYNC_TICK'`.
+
+---
+
+## 8. `examples/sd81/block7test.bas` — no official equivalent, mapper test
+
+A minimal program (not a transcription of any example) written to
+confirm, in isolation, that the SD81 Booster's memory mapper provides
+independent storage per page: it writes different patterns to pages 20
+and 63 of block 7 (`$E000-$FFFF`) via `Map()` and checks they don't
+overwrite each other. It was used to rule out the mapper as a cause
+during the MSFS bug investigation (see [MAP.md](MAP.md)). Kept as a
+reference example of direct `Map()`/`MapGet` usage over block 7.
+
+---
+
+## 9. `print42.bas`/`print64.bas` — new libraries (no example changes)
+
+`stdlib/print42.bas` and `stdlib/print64.bas` (Britlion) implement a
+42/64-column `PRINT` drawing pixel by pixel, instead of the normal
+32-column `PRINT`. The zx48k version depends on the Spectrum ROM in
+several places:
+
+- `print42.bas`: `ld hl,$5C7B` (`UDG` sysvar), `ld de,15360`
+ (fixed `CHARS-256`) and `ld a,(23693)` (`ATTR_P` sysvar).
+- `print64.bas`: only `ld a,(23693)` (`ATTR_P`) — its 4px charset is
+ its own, it doesn't use `UDG`/`CHARS`.
+- Both, additionally, place the screen and attributes using
+ **immediate** fixed-base constants (`add a,64` / `add a,88`, the
+ high bytes of `$4000`/`$5800`) instead of reading a pointer — this
+ works on the Spectrum because those addresses are ROM literals, but
+ on zx81sd `SCREEN_ADDR`/`SCREEN_ATTR_ADDR` are variables (block 6,
+ `$C000`/`$D800`).
+
+`src/lib/arch/zx81sd/stdlib/print42.bas` and `print64.bas` were
+created (full overrides): fixed sysvars → their zx81sd equivalents
+(`UDG`/`CHARS`/`ATTR_P`, see `sysvars.asm`), and the two screen/
+attribute base constants are **patched once on entering the function**
+(self-modifying code on the `ADD A,n` instruction itself), reading the
+real high byte of `SCREEN_ADDR`/`SCREEN_ATTR_ADDR` at that point — the
+same thing the original ROM did with its own routines, only with a
+variable value instead of a fixed one.
+
+**Real bug caught during the port** (not in the example, in the
+library itself): a `#include once ` naively placed at the
+top of the file (BASIC level, outside any `ASM` block) produced
+`error: illegal preprocessor character`, and placed inside the
+function's `ASM` block it compiled but hung the program at runtime
+(`HALT` at a garbage code address). Cause: if this library happens to
+be the **first** one to include `sysvars.asm` in the whole program, it
+drags in `bootstrap.asm`/`charset.asm` behind it — and the latter does
+`INCBIN "specfont.bin"` (the complete font, not assembly text) — right
+at that point in the source file. At BASIC level, the BASIC lexer tries
+to tokenize binary source bytes as if they were text (hence the
+"illegal preprocessor character"). Inside the function, the font binary
+gets emitted literally in the middle of the function's compiled body,
+and the CPU "executes" it as if it were instructions the moment control
+flow falls there. Fix: don't include `sysvars.asm` at all from these
+files — they trust that it's already included by the rest of the
+runtime (`CLS`/`PRINT`, which any program using `print42`/`print64`
+will also use), and only reference the already-namespaced symbols
+(`.core.CHARS`, etc.). Full detail in [PRECAUTIONS.md](PRECAUTIONS.md)
+and [MAP.md](MAP.md).
+
+Verified by simulation (the usual Python harness): both test strings
+(`tests_debug/print4264.bas`, companion repository) render legibly
+pixel by pixel at the expected position, with no writes outside
+screen/attributes/the file's own variables, and the program reaches its
+normal `__END_PROGRAM`/`HALT`. **Confirmed working on real hardware.**
+
+---
+
+## 10. MCU command examples (unofficial, no example to adapt)
+
+Six new example programs demonstrating the SD81 Booster MCU's command
+surface (`stdlib/mcu.bas`/`joy.bas`), written directly for zx81sd —
+none of them adapt an official `examples/` program. `examples/sd81/
+filesystem.bas` already existed from an earlier session (it's what the
+companion repository's `files.bas`/`FILES*` binaries were compiled
+from) and was expanded to also demo `McuMkdir`/`McuRmdir`/`McuCopy`/
+`McuMove`/`McuFree`, not just `McuCd`/`McuDirPrint`/`McuLoad`.
+
+| File | MCU commands exercised | Status |
+|---|---|---|
+| `examples/sd81/filesystem.bas` | `McuPwd`, `McuCd`, `McuDirPrint`, `McuMkdir`, `McuRmdir`, `McuSave`, `McuCopy`, `McuMove`, `McuDel`, `McuFree` | **Confirmed working on real hardware** |
+| `examples/sd81/wavplayer.bas` | `McuLoad` with a `.WAV` file (played directly by the MCU, not loaded into memory) | **Confirmed working on real hardware** |
+| `examples/sd81/vgmplayer.bas` | `McuPlayVgm`, `McuContVgm`, `McuPauseVgm`, `McuStopVgm` | **Confirmed working on real hardware** |
+| `examples/sd81/pegdemo.bas` | `McuLoadPeg`, `McuPlayPeg`, `McuStopPeg` | **Confirmed working on real hardware** (byte order below is correct) |
+| `examples/sd81/joystick.bas` | `Joy()` (joy.bas, MCU cmd 21) | **Confirmed working on real hardware** |
+| `examples/sd81/randomaccess.bas` | `McuFOpenZx`, `McuFSeek`, `McuFRead`, `McuFWrite`, `McuFClose` | **Confirmed working on real hardware** |
+
+Memory-mapper access (`Map()`/`MapGet()`) already has a dedicated
+example, `examples/sd81/block7test.bas` (see section 8 above), so no
+new one was added for that.
+
+**Compile-time bug found while writing these**: `stdlib/input.bas`
+defines `input()` as a **function** (`a$ = input(maxChars)`), not the
+native BASIC `INPUT` statement — using `input a$` (statement syntax)
+after `#include ` produces
+`error: Cannot convert string to a value. Use VAL() function`, since
+the identifier `input` is now bound to that function. All six examples
+above end with `a$ = input(20)` to wait for a keypress, not
+`input a$`.
+
+**PEG byte encoding, resolved**: the SD81 Booster manual's Appendix B
+documents the PEG instruction set as 16-bit words in big-endian order
+(e.g. `LD R,XX` = `0R XX`), while `mcu.bas`'s own comment on
+`McuLoadPeg` says the raw bytes it expects are "2 bytes little-endian
+per instruction" — `pegdemo.bas` followed the `mcu.bas` comment
+literally (value byte before opcode byte), and this has now been
+**confirmed correct on real hardware**: the manual's big-endian
+notation describes the instruction word's numeric value, while
+`McuLoadPeg`'s wire format for cmd 40 is genuinely little-endian, as
+documented in the library.
+
+---
+
+## Summary for the manual
+
+| Example | zx81sd copy | Source changes | Compile flags |
+|---|---|---|---|
+| comecoquitos.bas | no copy needed if the original isn't touched | none | `--string-base 1 --array-base 1` |
+| snake_en.bas | `snake_sd81.bas` | 1 line (UDG address) | none special |
+| flights.bas | `flights_sd81.bas` | 3 kinds of change (POKE removed, 4× PEEK COORDS, 8× key case) | none special |
+| 4inarow.bas | no copy needed | none | none special |
+| scroll.bas | no copy needed | none (the fix was in the `stdlib/scroll.bas` library) | none special |
+| winscroll.bas | no copy needed | none (no library override needed either — no ROM dependency) | none special |
+| putchars.bas/puttile.bas (unofficial, `putcharstile.bas`) | `putcharstile.bas` in `examples/sd81/` | none for putchars.bas; `puttile.bas` needed a zx81sd override (hardcoded screen/attr base constants, same fix as print42/64) | none special |
+| maskedsprites.bas | `maskedsprites_sd81.bas` | `WaitForNewFrame` rewritten (EI+HALT → VSYNC_TICK); `cb/maskedsprites.bas` library ported to the mapper (block 7) — **in progress, still not fully working** | none special |
+| pong.bas (unofficial) | `pong.bas` in `examples/sd81/` | 1 ASM line (VSYNC_TICK namespace) | none special |
+| block7test.bas (unofficial) | `block7test.bas` in `examples/sd81/` | n/a (written directly for zx81sd) | none special |
+| print42.bas/print64.bas (libraries) | not applicable (not examples) | none — the fix was in `stdlib/print42.bas`/`print64.bas` | none special |
+| filesystem.bas, wavplayer.bas, vgmplayer.bas, pegdemo.bas, joystick.bas, randomaccess.bas (unofficial) | all in `examples/sd81/` | n/a (written directly for zx81sd) | none special |
+
+Pattern identified for future examples:
+
+1. Look for `POKE`/`PEEK` on numeric literals: it's almost always a
+ Spectrum sysvar that needs translating to its zx81sd equivalent
+ address (see `src/lib/arch/zx81sd/runtime/sysvars.asm`).
+2. `INKEY$` comparisons against uppercase control keys (`"S"`, `"Q"`,
+ etc.): with the **current** keyboard scheme (direct press =
+ lowercase, `SHIFT+letter` = uppercase) this already works exactly
+ like on a Spectrum and **needs no change at all** — the case change
+ documented above for `flights.bas` corresponds to an earlier version
+ of `keyscan.asm` (see the "no longer needed" note in section 3.3)
+ and is kept here only as a historical record of that
+ already-generated copy.
+3. Inline ASM code calling routines wrapped in `push namespace core`
+ (like `VSYNC_TICK`) needs the `.core.` prefix if the `ASM` block
+ calling it isn't already inside that namespace (see the `pong.bas`
+ case above).
+4. If a new library needs its own sysvars (`CHARS`/`UDG`/`ATTR_P`/
+ `SCREEN_ADDR`/...), **don't do `#include once ` inside
+ the file** — only reference the symbols with the `.core.` prefix,
+ trusting that the rest of the runtime (`CLS`/`PRINT`) already
+ included it. See the `print42.bas`/`print64.bas` case above and
+ [PRECAUTIONS.md](PRECAUTIONS.md).
+5. `stdlib/input.bas` defines `input()` as a function
+ (`a$ = input(maxChars)`), not the native `INPUT a$` statement — once
+ `#include ` is used, `input a$` fails with
+ `error: Cannot convert string to a value`. See the MCU examples case
+ above.
diff --git a/src/arch/zx81sd/doc/MAP.md b/src/arch/zx81sd/doc/MAP.md
new file mode 100644
index 000000000..880005ead
--- /dev/null
+++ b/src/arch/zx81sd/doc/MAP.md
@@ -0,0 +1,833 @@
+# Debug test log (DRAW3 / arc session)
+
+Note on paths: this log was originally written in the companion,
+test-only repository for the port (where the `.bas` files mentioned
+here all lived in a `tests_debug/` directory). The examples considered
+mature enough to publish have already been copied into this
+repository, under [`examples/sd81/`](../../../../examples/sd81/)
+(`flights_sd81.bas`, `snake_sd81.bas`, `maskedsprites_sd81.bas`,
+`pong.bas`, `block7test.bas` — see [BASIC_CHANGES.md](BASIC_CHANGES.md)).
+The rest of the one-off debugging sources mentioned here (`diag1-6`,
+`t_arc*`, `trig_test`, `heaptest`, `keytest`...) only remain in the
+companion repository, not this one.
+
+Compiling each one (from the root of this repository):
+```
+python -m src.zxbc.zxbc examples\sd81\.bas --arch zx81sd -o .bin
+python src\arch\zx81sd\tools\split_sd81.py .bin
+```
+(see [USAGE.md](USAGE.md) for the packager's detail.)
+
+| Source | SD81 prefix | What it tests | Result obtained |
+|------------------------|--------------|---------------------------------------------------------------------|---------------------|
+| `str_test.bas` | STRTEST | PRINT/STR$ of FLOAT (Phase 3) | OK: 7 / 7.5 / -3.25 / 0 / 123.5 / -0.5 |
+| `trig_test.bas` | TRIGTST | SQR/SIN/COS/EXP/LN/ATN with trivial arguments (Phase 4) | OK: 3 / 1.41421 / 0 / 1 / 2.71828 / 1 / 3.14159 |
+| `trig2_test.bas` | TRIG2 | SIN/COS with NON-trivial arguments (1, 1.5708, 3.14159) | OK: 0.8147(4?) / 0.5403 / 0.99999 / -0.99999 / 0 |
+| `draw_arc_test.bas` | DRAWARC | DRAW+DRAW+CIRCLE combined (first arc attempt) | Only a vertical line appears, CIRCLE never shows |
+| `t_line.bas` | TLINE | `DRAW 20,0` (2 args, no angle, no FP) | OK: horizontal line |
+| `t_circle.bas` | TCIRC | `CIRCLE 60,30,20` alone | OK: correct circle |
+| `t_arc.bas` | TARC | `DRAW 20,0,3.14159` (arc alone, horizontal offset, 180°) | BUG: vertical line (not horizontal, no arc) |
+| `t_arc2.bas` | TARC2 | `DRAW 30,10,1.5708` (asymmetric offsets, 90°) | BUG: vertical line with a slight rightward tilt |
+| `t_arc3.bas` | TARC3 | `PLOT 100,96` + `DRAW 20,0,3.14159` (centered, with margin) | BUG: chaotic star-like pattern of several lines from the center; one goes off-screen into the attribute area |
+| `diag1.bas` | DIAG1 | Inline ASM: calls CD-PRMS1 (L247D) directly with z=40, A=pi/2, and prints mem-1/mem-3/mem-4/mem-0/line count | Blank screen — bug in the test itself (`#include` inside `ASM` puts executable code in the linear flow). Replaced by diag2 |
+| `diag2.bas` | DIAG2 | Same as diag1 but without manual includes: BASIC variables + normal PRINT | OK: exact expected values → CD-PRMS1 numerically correct |
+| `diag4.bas` | DIAG4 | With `-D DRAW3_DEBUG`: only prints N (number of segments captured by the draw3.asm hook) | OK: 16 → the capture hook works |
+| `diag5.bas` | DIAG5 | diag4 + buffer traversal loop (2 SD81 pages) | Didn't print; dropped, replaced by diag6 (1 page) |
+| `diag6.bas` | DIAG6/DIAG7 | diag4 + loop, 1 page. Its $152D dump + write breakpoints at $8004 located the bug | OK: 16/16. DIAG7 = identical, recompiled after the fix |
+| `arcfix.bas` | ARCFIX | Reference test identical to a real Spectrum: `PLOT 100,100` + 2 arc DRAWs + CIRCLE | **OK after the fix**: hook + circle, matching the real Spectrum photo |
+
+## DRAW3 (arc) bug — RESOLVED
+
+**Root cause** (nothing to do with the FP calculator, whose math turned
+out to be exact): `src/lib/arch/zx81sd/runtime/pixel_addr.asm` was
+destroying register **D** (using it as scratch for V=191−Y). But
+`draw.asm` (inherited from zx48k) saves the Bresenham Y coordinate in D
+around the call (`ld d,b / call PIXEL_ADDR / ld b,d`), because the
+Spectrum ROM's PIXEL-ADD ($22AC) preserves DE. As a result, every line
+with a vertical component started internally with Y=191−y1, corrupting
+COORDS and the drawing. Horizontal lines (t_line) and CIRCLE (which
+doesn't go through that path) came out fine, which threw off the
+initial investigation.
+
+**Fix**: PIXEL_ADDR rewritten to use B as scratch (it was already being
+destroyed, same as the ROM) and preserve D and E. Verified on hardware
+with ARCFIX (identical to the real Spectrum's result) and DIAG7.
+
+**Localization method**: a `#ifdef DRAW3_DEBUG` hook in draw3.asm
+(enabled with `-D DRAW3_DEBUG`) that captures, for every segment:
+|Dy|,|Dx|, signs and the previous COORDS in a buffer
+(`DRAW3_DEBUG_BUF`); a memory dump of the buffer in EightyOne's
+debugger + write breakpoints on COORDS ($8004/$8005). The dump showed
+the accumulated FP position was perfect and that COORDS ended up wrong
+after every line → the bug was in __DRAW/PIXEL_ADDR, not in
+fp_calc.asm or draw3.asm.
+
+## Source files modified/created in this session (FP engine + arc)
+
+- `src/lib/arch/zx81sd/runtime/fp_calc.asm` — Phases 1-5 (the
+ CALCULATE engine, trig/log/exp/sqrt, and now STK-TO-A/STK-TO-BC/
+ CD-PRMS1 for the arc). Also had `#include once ` added
+ (an unrelated bug: it's always included in every zx81sd binary, so it
+ had to be self-sufficient).
+- `src/lib/arch/zx81sd/runtime/draw3.asm` — NEW. An override of
+ `zx48k/runtime/draw3.asm` that replaces calls to fixed ROM addresses
+ with the routines ported in `fp_calc.asm`. Includes trace
+ instrumentation behind `#ifdef DRAW3_DEBUG` (off by default; enabled
+ by compiling with `-D DRAW3_DEBUG`).
+- `src/lib/arch/zx81sd/runtime/pixel_addr.asm` — FIX for the arc bug:
+ now preserves D and E (it used to destroy D, breaking draw.asm's
+ Bresenham on every non-horizontal line).
+- `src/lib/arch/zx81sd/runtime/fp_tostr.asm`, `printf.asm`, `str.asm` —
+ from Phase 3 (PRINT/STR$ of FLOAT), unchanged in this session.
+
+## Sound: BEEP and PLAY (SD81's AY ZonX chips)
+
+| Source | SD81 prefix | What it tests | Result |
+|-----------------|--------------|------------------------------------------------------------|-----------|
+| `beeptest.bas` | BEEPTS2 | Variable BEEP (FP runtime) + constant BEEP (13/14 clock fix in __BEEPER) | OK: scale + DO/DO' + LA 440 |
+| `playtest.bas` | PLAYTS2 | PLAY over 3 channels + AY/beeper comparison | OK (watch out: lowercase notes = one octave down!) |
+| `aycal.bas` | AYCAL | AY vs beeper pairing by semitone | Used to detect the offset |
+| `aycal2.bas` | AYCAL2 | Timeable durations + direct pairing | FFT: beeper 434.5 (correct, emulator pacing), AY 220 |
+| `aycal3.bas` | AYCAL3 | Same with UPPERCASE notes | OK: unison 440/440 |
+
+Lesson from the "phantom octave" investigation: in PLAY's MML (128K
+BASIC semantics), lowercase notes sound one octave below the current
+octave. The initial tests used lowercase and the AY was sounding at
+220 Hz *by design*. The emulator, the divider table (1.625 MHz) and the
+beeper (3.25 MHz) were all correct. Verified with an FFT over recorded
+audio output. As a bonus, a real latent EightyOne bug got fixed along
+the way (the AY's clock kept the hardware-dialog card's setting instead
+of the ZonX one forced by the SD81: it was missing a call to
+Sound.InitDevices() after forcing machine.aytype).
+
+## MCU library (SD81 Booster) — `zx81sd/stdlib/mcu.bas` + `joy.bas`
+
+| Source | SD81 prefix | What it tests | Result |
+|----------------|--------------|------------------------------------------------------------|-----------|
+| `joytest.bas` | JOYTS2 | `Joy("QAOPM")` (cmd 21) + local validation + INKEY$ echo | OK |
+| `mcutest.bas` | MCUTST | VERSION, GET/SETBYTE, PWD, SAVE+LOAD+verify, DEL, FREE, RTC, BAT, DIR, AY2 by registers, AyPlay | pending |
+| `maptest.bas` | MAPTST | `Map(block,page)`/`MapGet` — the $E7 mapper: signatures in 2 pages switching block 5, and verification | OK |
+| `exttest.bas` | EXTTST | Non-MCU extensions: `HexPoke` (*HEX), `MemMove` (*LDIR/*LDDR, stdlib), `StrInv`/`StrBold` (*INV/*BOLD) | OK |
+| `ftest.bas` | FTEST | F_* handles: SAVE, F_OPEN_ZX81 (cmd 58), F_SEEK, F_READ with verification, F_WRITE+reread, F_CLOSE, DEL | OK |
+| `lstest.bas` | LSTEST | Native LOAD/SAVE/VERIFY ... CODE statements → SD (runtime override load.asm/save.asm, cmd 9/10): SAVE+LOAD+verify, VERIFY ok/corrupt (ERR 26), missing file (ERR 26) | OK |
+
+Architecture: `mcu.bas` contains the protocol primitives in ASM
+(McuSend/McuRecv/McuSendBlock/McuRecvBlock — the *Block ones are the
+critical path for LOAD/SAVE/F_READ/F_WRITE) and wrappers for every
+command in the manual (system, files, F_* handles, hardware, voice,
+AY2/VGM/PEG, RTC/BAT). Automatic ASCII↔ZX81 conversion in the text
+commands. `joy.bas` is a thin layer over `mcu.bas`.
+
+Protocol notes (extracted from the emulator's SD81Booster.cpp):
+- After EVERY operation on $A7, the change in bit 7 of $AF is expected.
+ NEVER write to $AF (MCU reset).
+- Z80→MCU strings: length byte + data (the MCU converts ZX81→ASCII
+ except for "raw" commands: JOY, BINARY_SAY, F_OPEN).
+- MCU→Z80 streams (PWD/DIR/TYPE/FREE_TXT): request each character by
+ writing CMD_NEXTCH ($0D); end = EOT ($6F); the status arrives after.
+- Fixed-length responses (LOAD, FREE, RTC, BAT, F_READ): a burst of
+ bytes reading $A7 with a clock wait between each one.
+- F_OPEN: confirmed in the firmware (COMMANDS.cpp) that the MCU
+ assigns the handle and returns it (the manual was wrong and has been
+ corrected). Added F_OPEN_ZX81 (58) to the library and the emulator.
+- OPENDIR/GETROWLEN/GETROW (16-18) aren't emulated in EightyOne: only
+ test them on real hardware.
+
+## Integration test: comecoquitos.bas (official zxbasic example)
+
+| Source | SD81 prefix | What it tests | Result |
+|---|---|---|---|
+| `examples/english/comecoquitos.bas` | COMECO | Complete 1985 game: FP, strings/slices, arrays, UDGs, block graphics, color/FLASH/BRIGHT, INKEY$, BEEP, RND | OK — identical to the Spectrum .tap |
+| `fpleak.bas` | FPLEAK | Detector for FP stack leaks by block (reads $8024 after each idiom) | Helped narrow it down; the "leak" was UDG corruption |
+| `blocktest.bas` | BLKTST | The 16 block graphics CHR$(128)-143 | OK after the PO_GR_1 fix |
+
+Three runtime bugs caught with this game (commits 9903c866 and
+9a24059e):
+1. UDG pointed 128 bytes past the end of the font (96 chars, not 256):
+ POKE USR CHR$ was clobbering runtime code → hang on the calculator's
+ first use. Fix: a dedicated 21-UDG area.
+2. INKEY$ returned uppercase; the Spectrum's L mode (and the era's
+ programs) use lowercase. Fix: lowercase keyscan table.
+3. PO_GR_1 (blocks CHR$(128)-143) generated corrupt patterns (OR into
+ the wrong register + swapped left/right quadrants). Fix: the ROM's
+ literal algorithm.
+
+RECIPE for classic examples transcribed from Sinclair BASIC: compile
+with `--string-base 1 --array-base 1` (1-based indexing). Without it,
+string-slicing collisions come out shifted by one position (not a bug
+in the port: it happens the same way on zx48k).
+
+## Heap at $8100 + EightyOne tape traps — RESOLVED (2026-07-04)
+
+| Source | SD81 prefix | What it tests | Result |
+|---|---|---|---|
+| `tests_debug/heaptest.bas` | HEAPA..HEAPE | Dynamic memory manager (3 phases: char-by-char growth, large REALLOCs, STR$ in a loop) with the heap at different addresses | Bisection that isolated the bug |
+| `tests_debug/memtest.bas` | MEMTST | R/W pattern dependent on address in $8100-$BFFF (2 passes) | OK — ruled out hardware/paging |
+| `tests_debug/inputtest.bas` | INTEST | Minimal, isolated INPUT() | Reproduced the hang |
+| `examples/sd81/flights_sd81.bas` | FLIGHT | Flight simulator: PEEK COORDS ($8004/5), FP-intensive, INPUT | Adaptation of examples/flights.bas |
+
+Two chained bugs, caught on 2026-07-04:
+
+1. **Compiler (`src/arch/zx81sd/backend/main.py`)**: `heap_size`/
+ `heap_address` were registered with `ADD_IF_NOT_DEFINED`, but the
+ generic Z80 backend already defines them earlier (4768 / None) →
+ zx81sd's values ($8100 / 16127) were never applied and the heap
+ ended up inline (DEFS) inside the executable area, wasting 4768
+ bytes and limiting the heap. Fix: direct assignment of
+ `OPTIONS.heap_size/heap_address` (the CLI can still override it).
+
+2. **Emulator (`Eightyone2/src/ZX81/rompatch.cpp`, `PatchTest`)**: the
+ ZX81 ROM's tape traps ($0207/$02FF/$031E/$0356) were triggered by
+ comparing PC + a **flat** `memory[pc]` read, which keeps reading the
+ ROM even though the SD81 has RAM mapped. With the heap in EQU, the
+ runtime moves $12A0 bytes down and the __DIVU16_FAST division lands
+ on $02FF → the SAVE trap sets `DE=1` mid-division → stable garbage
+ quotient → an infinite digit loop in __PRINTU_LOOP (PUSH AF with no
+ matching pop) → the stack descends, wiping out the runtime. Builds
+ with an inline heap were immune by coincidence: the 4 trap addresses
+ fell inside the DEFS block (data, the PC never passes through
+ there). Fix: `PatchTest` reads the byte with `zx81_PatchPeek()`
+ (mapper-aware). This bug does NOT exist on real hardware (there are
+ no traps).
+
+Methodology that solved it: deterministic simulation of the binary
+with Python's `z80` library (pip install z80) — code-area integrity
+diff after each stretch + breakpoints comparing registers against
+EightyOne. The binary was correct in pure Z80 → the divergence was in
+the emulator. Harness in the session's scratchpad (runsim*.py,
+reproducible).
+
+## PRINT scroll jumping into the Spectrum ROM — RESOLVED (2026-07-04)
+
+Third bug in the flights.bas chain (wind=10, dir=100 → HALT): zx81sd's
+`print.asm` had kept, from zx48k, the fallback
+`__SCROLL_SCR EQU 0DFEh` (the Spectrum ROM's CL-SC-ALL routine). On
+zx81sd there's no ROM: the first PRINT that overflowed the screen did a
+CALL to whatever compiled BASIC line happened to occupy $0DFE → wild
+execution → a gosub RETURN popping garbage → HALT. It depended on the
+input because the number of digits typed moved the cursor: with wind
+"1"/"0" the text never overflowed; with "10"/"100" it did. No previous
+test caught this because they all use PRINT AT (never scroll).
+
+Fix: the buffer-based implementation (`__ZXB_ENABLE_BUFFER_SCROLL`,
+scrolls via SCREEN_ADDR/SCREEN_ATTR_ADDR) is now the only branch of
+__SCROLL_SCR in zx81sd/runtime/print.asm. Verified with the Python
+simulator + scripted keyboard: the game's main loop keeps SP stable
+over thousands of steps. Future note: check for any other EQU/CALL to
+absolute Spectrum ROM addresses when porting files from zx48k (grep
+already done: none remain in the zx81sd runtime).
+
+## New keyboard scheme: uppercase, CAPS LOCK and symbols — 2026-07-04
+
+| Source | SD81 prefix | What it tests | Result |
+|---|---|---|---|
+| `tests_debug/keytest.bas` | KEYTST | Interactive INKEY$: prints the ASCII code + character of every key | Verified by exhaustive simulation (see below); pending manual testing on the emulator/hardware |
+
+The ZX81's physical keyboard doesn't distinguish upper/lowercase per
+key: SHIFT+letter gives a symbol, not the uppercase of that letter.
+Until now `keyscan.asm` only ever returned lowercase. New scheme
+(`src/lib/arch/zx81sd/runtime/io/keyboard/keyscan.asm`, rewritten from
+scratch):
+
+- No modifier: lowercase (same as before — comecoquitos, snake,
+ flights, row4 keep working without touching a line, because none of
+ them use SHIFT).
+- `SHIFT + letter`: UPPERCASE of that letter.
+- `SHIFT + "2"`: toggles a persistent CAPS LOCK (silent, prints
+ nothing).
+- CAPS LOCK active: lowercase becomes uppercase; SHIFT still gives
+ uppercase the same way (it's an OR, no interaction — decision made
+ with the user).
+- `"."` alone: `.`
+- `SHIFT + "."`: `,` (same as a real ZX81).
+- `"." + another key`: the symbol printed on the ZX81 keyboard for that
+ key (`:` with Z, `)` with O, RUBOUT with 0, etc. — the ZX81's old
+ SHIFT table, now reachable with "." instead of SHIFT, since SHIFT has
+ been redefined to give uppercase).
+
+Required rewriting the scan: it used to stop at the first row with
+anything pressed (one key at a time was enough). Now two dedicated port
+reads are needed for SHIFT (row 0) and "." (row 7, column 1), plus a
+scan of the other rows looking for a third key, excluding those two
+positions (`FIND_OTHER` routine). The `SHIFT+"2"` combo uses a
+persistent state byte with edge detection so it doesn't toggle
+repeatedly while held down.
+
+Verified with a Python simulation harness (`z80`, same methodology
+already used for the heap bugs): `__ZX81SD_KEYSCAN` is called directly,
+injecting the exact row bits of each combination through the I/O
+callback (Z alone, SHIFT+Z, "."+Z, SHIFT+2 held for 4 consecutive
+polls, etc.), without dealing with the complexity of a real keyboard.
+The table and state offsets (`_KBD_*`, all LOCAL to the PROC, don't
+show up in the `.map`) were located by searching for
+`UNSHIFT_TABLE`'s byte pattern ("zxcvasdfg") in the binary and
+computing the rest by fixed offset (each table takes 39 bytes). All 12
+cases in the design table matched exactly, including the CAPS LOCK
+debounce.
+
+### 2026-07-04 fix: the "." modifier moved from keyscan to input.bas
+
+When testing it on the emulator, the user found that `"."+key`
+(intended to give the ZX81 symbol by pressing both at once, like
+SHIFT+letter) was impractical from `INPUT`: `PRIVATEInputWaitKey`
+commits to the `"."` key as soon as it detects it alone, without giving
+the second key time to actually arrive at the same time (unlike
+SHIFT+letter, which can comfortably be held with the other hand). The
+user's correct diagnosis: *"symbol handling shouldn't live in keyscan
+but in INPUT.bas"*.
+
+Fix: `keyscan.asm` now treats `"."` like any other key — unshifted
+gives `.`, with SHIFT gives `,` (same as a real ZX81), with none of the
+"third key" logic for the dot (the row-7 exclusion in `FIND_OTHER` was
+removed entirely, no longer needed). The `UNSHIFT_TABLE`/
+`SYMBOL_TABLE`/`CAPS_TABLE` tables were promoted from `LOCAL` to file
+scope (`__ZX81SD_` prefix) and a new routine was added,
+`__ZX81SD_SYMBOL_FOR(char)`, which does the reverse
+UNSHIFT_TABLE→SYMBOL_TABLE lookup given an already-decoded character.
+
+Symbol composition now lives in `stdlib/input.bas` as a sequential
+"dead key": on reading `"."`, the `input()` function reads the NEXT key
+separately (with no simultaneity requirement) and calls
+`PRIVATEInputSymbolFor()`; if there's a symbol, it's appended; if not,
+the literal dot is appended and the second key is processed normally
+(DEL, ENTER, or one more character).
+
+A bug of my own caught during the implementation (before the user ever
+saw it): `"."` + `"0"` resolves to RUBOUT (12) via `SYMBOL_TABLE` (it's
+the actual symbol the ZX81 prints over the "0" key), which would delete
+the previous character when typing any decimal ending in ".0" (very
+common: "3.0", "10.0"...). That value was explicitly excluded in
+`input.bas` — RUBOUT is already reachable unambiguously with SHIFT+0
+(which is comfortable to hold at the same time). Verified with a
+full-program simulation (scripted typing "1",".","0",ENTER →
+`a$="1.0"` correctly, and "."," o",ENTER → `a$=")"`), not just the
+isolated function.
+
+`tests_debug/keytest.bas` (KEYTST) is still the interactive `INKEY$`
+tester; `tests_debug/inputtest.bas` (INTEST) is the same old mini
+`INPUT()` test, now also useful for testing symbol composition by hand.
+
+### 2026-07-04 refinement: two dots in a row = a literal dot
+
+With the previous design, `"."` + a letter with an associated symbol
+(e.g. Z → `:`) always resolved to that symbol — there was no way to
+literally write a dot followed by that letter. User request: pressing
+`"."` twice in a row should confirm the first dot as literal (the
+redundant comma that used to come out of `"."+"."`, via
+`SYMBOL_TABLE`, is no longer needed — it's already reachable directly
+and unambiguously with `SHIFT+"."`), and whatever key comes after is
+read as a fresh keypress with no combination. So, to write ".Z" you
+type "." "." "Z".
+
+Implemented in `input.bas`: if the second key read after a "." is also
+a ".", the dot is appended, the second keypress is discarded (prints
+nothing by itself, `LastK=0`) and the loop goes back to reading a fresh
+key. Verified with the same full-program simulation:
+`"."+"."` → `a$="."`; `"."+"."+"z"` → `a$=".z"` (without forming the
+`:` symbol).
+
+## scroll.bas — RESOLVED 2026-07-04 (new library, example unchanged)
+
+| Source | SD81 prefix | What it tests | Result |
+|---|---|---|---|
+| `examples/scroll.bas` | SCROLL | The 4 pixel-by-pixel scrolls (Right/Down/Left/Up) over a 60×60 px window, 30 rounds | Simulated 1200M ticks with no HALT/RST38; pending viewing on the emulator (the example does 1920 scrolls of up to 100×100 px, takes a while) |
+
+`src/lib/arch/zx48k/stdlib/scroll.bas` had no zx81sd override — the
+zx48k version was used as-is, and its 8 subs (`ScrollRight/Left/Up/
+Down` + their `*Aligned` variants) all call `call 22ACh`, the Spectrum
+ROM's *PIXEL-ADD* routine. On zx81sd there's no ROM mapped: that
+address lands in the middle of the program's compiled code, and the
+reported HALT (`RST 38` in the trace) was just whatever byte happened
+to be there.
+
+Fix: `src/lib/arch/zx81sd/stdlib/scroll.bas`, an identical copy except
+the 8 calls to `$22AC` are replaced with `call PIXEL_ADDR` (our own
+routine, `runtime/pixel_addr.asm`, already used by `plot.asm`/
+`draw.asm`). The register contract is IDENTICAL to the ROM's (A=191,
+B=Y, C=X → HL=offset, A=X AND 7, destroys B, preserves D/E), so not a
+single line of the scroll loop bodies needed touching, only the call
+site. `SP.PixelDown`/`SP.PixelUp` (from zx48k/runtime/SP/) didn't need
+copying: they're pure arithmetic over `SCREEN_ADDR`, no ROM involved,
+and already worked the same way on zx81sd (resolved through the normal
+zx48k fallback mechanism when there's no override).
+
+`examples/scroll.bas` itself needed no source change — like
+`4inarow.bas`, the problem was entirely in the library, not the
+program. Added to `BASIC_CHANGES.md` with the same note.
+
+## maskedsprites.bas — RESOLVED 2026-07-04 (source change, not a library one)
+
+| Source | SD81 prefix | What it tests | Result |
+|---|---|---|---|
+| `examples/maskedsprites.bas` → `examples/sd81/maskedsprites_sd81.bas` | MASKED | Masked sprites (AND+OR) with MSFS, 10 animated sprites | Simulated 1000M ticks with no HALT/RST38/illegal write; PC advances across a wide range of addresses (not stuck) |
+
+Unlike `scroll.bas`, here the problem WAS in the example itself
+(`WaitForNewFrame`, defined directly in `examples/maskedsprites.bas`,
+not in the `cb/maskedsprites.bas` library): it does `EI` + `HALT`
+waiting for the Spectrum ROM's 50Hz IM1 interrupt, comparing against
+the ROM's `FRAMES` counter at absolute address `23672`. On zx81sd
+interrupts are permanently disabled (the whole runtime runs with `DI`;
+the `$0038` vector is only a `DI;HALT` trap, not a real handler) — that
+`HALT` never wakes up. Confirmed by the trace: the simulator ended up
+with `m.halted=True` exactly at `WaitForNewFrame`'s `HALT` after ~31M
+ticks.
+
+Fix in `examples/sd81/maskedsprites_sd81.bas`: `WaitForNewFrame`
+rewritten to use `VSYNC_TICK` (`runtime/vsync.asm`, already used by
+`PAUSE`) instead of `EI+HALT` — polls the SD81 Booster's hardware
+VSYNC pulse counter through a port ($AFh), with no dependency on
+interrupts. The original algorithm did ONE initial `HALT` and then a
+loop that checked `FRAMES` WITHOUT waiting again (trusting the
+interrupt to keep incrementing it in the background); since nothing
+increments it on its own on zx81sd, the loop calls `VSYNC_TICK`
+explicitly on every round it still needs to wait.
+`GetInterruptStatusInBorder` was left untouched (never called in the
+main loop, only appears commented out — kept only so the end-of-file
+compile check doesn't fail with "unused function").
+
+### 2026-07-04 update: MSFS genuinely ported to the mapper (block 7)
+
+The `$5B5C`/`$7FFD` risk from above **no longer applies**: at the
+user's request ("why don't we use the block 7 we have for banking?"),
+`src/lib/arch/zx81sd/stdlib/cb/maskedsprites.bas` was created, a full
+override of the shared library (which stays untouched, Boriel's rule).
+Design (see also `BASIC_CHANGES.md`):
+
+- **Key finding**: MSFS's functions (`RegisterSpriteImageInMSFS`,
+ `FindFirstUnusedBlockInMSFS`, etc.) are bank/address-agnostic — they
+ only call `GetBankPreservingRegs`/`SetBankPreservingINTs` and read/
+ write the BASIC variable `MaskedSpritesFileSystemStart`. By
+ rewriting those two primitives (using port `$E7` over **block 7**,
+ `$E000-$FFFF` — reserved in our memory map exactly for "data
+ banking, maps, sprites") and that address's calculation in
+ `InitMaskedSpritesFileSystem()` (fixed at `$E000` instead of
+ "whatever's left up to `$FFFF`", which assumed flat Spectrum RAM),
+ the rest of the file (hundreds of lines of block/bitmap arithmetic)
+ was copied literally without touching a line.
+- `CheckMemoryPaging()` returns `0` (honest: zx81sd has no Spectrum
+ bank-5/7-style dual visible screen) with no effect on MSFS, because
+ MSFS's functions don't consult that function to decide whether to
+ use the bank — they always do it unconditionally.
+- `SetVisibleScreen`/`GetVisibleScreen`/`ToggleVisibleScreen`/
+ `CopyScreen5ToScreen7`/`CopyScreen7ToScreen5`/`SetDrawingScreen5`/
+ `SetDrawingScreen7`/`ToggleDrawingScreen` → safe stubs (real
+ double-screen buffering isn't covered; dead code in this example
+ since `memoryPaging=0`, but they no longer touch `$5B5C`/`$7FFD` in
+ case something calls them directly in the future).
+
+**Real bug found during the implementation** (not by the user, caught
+with the simulation harness itself): my first attempt wrote
+`SetBankPreservingINTs`/`GetBankPreservingRegs` in plain BASIC instead
+of hand-written ASM. That broke the register contract documented in
+the original file itself ("Preserves: D, E, H, L") which
+`RegisterSpriteImageInMSFS`'s ASM code and friends take for granted
+(for example, so as not to lose `spriteImageAddr`, which arrives in
+HL) — a compiled BASIC function uses registers freely inside with no
+guarantee of preserving them. Result: all 6 test sprites registered at
+the SAME address (`$0C07`) instead of distinct ones. Both primitives
+were rewritten in hand-written ASM, with the exact same contract as
+the original (only touching A, B, C).
+
+Verified by simulation: the 6 register addresses (`regHero0`,
+`regFoe00`, `regFoe20-23`) come out sequential every 96 bytes exactly
+(`$E010, $E070, $E0D0, $E130, $E190, $E1F0`, matching
+`$E000+n*96+16`), without triggering `STOP`, and the main loop
+(`WaitForNewFrame`) is reached repeatedly with no hang after 1000
+million simulation ticks with no `HALT`/illegal write.
+
+Known limitation of the Python simulator used in this session: it
+doesn't model the memory mapper (`OUT` to `$E7` is a no-op in the
+simulation, all RAM is treated as flat) — it can't validate that the
+*physical* page swap genuinely works, only that the Z80 logic is
+correct assuming it does. The definitive validation is on the
+emulator/real hardware.
+
+### 2026-07-04, second round: still going to HALT on real hardware
+
+With the fix above already compiled, on the real emulator it still
+triggered `__STOP` (same symptom: `RegisterSpriteImageInMSFS` returns
+0). The trace confirmed the problem was in `SetBankPreservingINTs`,
+which did the `OUT` to port `$E7` by hand instead of calling `Map()`
+(mcu.bas): it wrote `A=7` (block, without combining it with the page)
+with `B=page`. My initial hypothesis was that the hardware might be in
+simple mode (where the data byte must carry page AND block combined,
+`(page AND 31)<<3 | block`, and with only `A=7` it would be interpreted
+as page=0) — **the user corrected this**: the SD81 loader leaves the
+mapper in full mode from the `LOAD *MAP 7,63` line onward, until the
+next reset, so that specific explanation doesn't fit (in full mode only
+A's 3 low bits matter, the same in both versions). The exact cause was
+still unconfirmed at the time of writing this.
+
+`SetBankPreservingINTs` was changed to call `Map()` (code already
+proven in other contexts) instead of repeating the port logic by hand,
+preserving D,E,H,L around the call with manual `push`/`pop` (`Map()`
+itself preserves nothing). This had a side effect: `Map()` was no
+longer referenced from BASIC anywhere (only from hand-written ASM), and
+the compiler's dead-code eliminator removed it from the binary →
+`Undefined GLOBAL label '._Map'`. Solved with an explicit, redundant
+BASIC call to `Map(7, MaskedSprites_MSFS_Page)` inside
+`InitMaskedSpritesFileSystem()` (commented as such — the compiler's
+usage analysis doesn't count calls made from ASM).
+
+Verified again in simulation (the same 6 sequential addresses, no
+`STOP`) — but it still failed on the real emulator. The switch to
+`Map()` was, as the user pointed out, a no-op ("the library's Map does
+exactly the same thing as the raw out").
+
+### REAL root cause (third round, 2026-07-04): the FSB is never initialized
+
+The user's trace showed `FindFirstUnusedBlockInMSFS` scanning the
+ENTIRE free-space bitmap (the FIND-INT loop, 254 lines of RRCA/DEC E)
+and exiting through the `full` branch (`SCF/RET` → `JR C` → `LD HL,0`):
+**every block appeared "used"**. `__EQ16` (the initial suspect) worked
+perfectly — `regHero0` really was 0.
+
+Cause: **neither our version nor the original zx48k one ever clears the
+FSB** (the bitmap bytes at `start+2..start+1+l`). On the Spectrum
+that's not needed: the ROM's RAM test leaves all memory zeroed at
+boot, so the bitmap is born "all free" for free. On zx81sd, block 7's
+page arrives with factory garbage → every bit set to 1 → "no free
+blocks" → `RegisterSpriteImageInMSFS` returns 0 → `STOP`.
+
+**Why the Python simulator gave a false OK twice**: its RAM also starts
+at zero, just like a Spectrum's after the ROM test — exactly the
+condition that hides the bug. Methodology lesson added to the harness:
+to validate code that reads uninitialized memory, fill all unloaded RAM
+with garbage (`$FF`) before simulating. With dirty RAM, the unfixed
+binary reproduces the `STOP` (the trace's failure mode) and the fixed
+binary passes completely (6 correct registers, main loop reached).
+
+Fix (3 lines + a comment in `InitMaskedSpritesFileSystem`): a
+`FOR j = start+2 TO start+1+l: poke j,0: NEXT` loop after computing the
+FSB's size.
+
+Along the way, it was also confirmed with
+`examples/sd81/block7test.bas` (BLOCK7 prefix) that the mapper works
+perfectly: different patterns written to pages 20 and 63 of block 7
+survive the page switch (independent content per page). The mapper was
+never the problem.
+
+### 2026-07-04, fourth round: sprites as vertical lines — resident page
+
+With the FSB fix, on the real emulator MSFS now initialized correctly
+(screen: `Init MSFS at 57344`, `Free Blocks = 85`, and all 6 registers
+with the EXACT values predicted by the simulation) but the sprites were
+being drawn as vertical lines instead of their actual graphics.
+
+Cause (a design oversight of this override, not the original):
+`SaveBackgroundAndDrawSpriteRegisteredInMSFS` — the function that draws
+in the main loop and manufactures shifted images on demand — accesses
+MSFS **without wrapping it in Get/SetBank**. In the original design it
+doesn't need to: on 128K, bank 7 stays mapped at `$c000`
+(`SetDrawingScreen7`) and on 48K, MSFS is always visible in flat RAM.
+Our first version "released" block 7 back to page 63 when restoring
+after `Init`/`Register...` → drawing read masks and graphics from page
+63 (garbage) → vertical lines. The simulator couldn't detect this:
+with no mapper modeled, page 20 and 63 are the same flat RAM.
+
+Fix: MSFS's page stays **resident** in block 7 from init onward —
+`SetBankPreservingINTs` with a value ≠ 7 only records the number, it
+doesn't unmap (the "release back to page 63" was an invention of this
+override, nothing needs it). Documented in the library's header: if a
+program uses block 7 for its own banking, it must remap its own page
+and call `SetBankPreservingINTs(7)` before using MSFS again.
+
+## Chroma81 colour mode not activated by Spectrum mode alone — RESOLVED (2026-07-05)
+
+Found on real hardware, not caught by the emulator: activating
+Superfast HiRes Spectrum mode (`POKE 2045,172` / `ld ($07FD),a`) turns
+on the Spectrum-compatible screen layout, but does **not** by itself
+turn on Chroma81 colour on a real SD81 Booster — everything showed up
+monochrome despite `INK`/`PAPER`/attributes being written correctly.
+EightyOne shows colour regardless of this step, which is an **emulator
+bug** (color should also require the same explicit activation to match
+real hardware) — needs reporting/fixing in the EightyOne repository
+separately, not something to work around here.
+
+Fix: `tools/boot1.asm` (stage 1 bootstrap) now also writes to the
+Chroma81 port (`$7FEF`) right after activating Spectrum mode:
+
+```
+ld bc, $7FEF ; Chroma81 port: set known state
+ld a, 39 ; bit5=1 color on, bit4=0 char-code mode
+out (c), a
+```
+
+`tools/boot1.bin` reassembled with `zxbasm.py` (34 bytes, was 27 —
+exactly the 3 new instructions' bytes: `01 EF 7F` / `3E 27` / `ED 79`).
+This is the shared stage 1 loader used by every zx81sd program, so no
+program needs recompiling — just copy the updated `BOOT1.BIN` to the
+SD card.
+
+## Screen/attributes not cleared at program start — RESOLVED (2026-07-06)
+
+Reported after real-hardware testing: block 6 (screen RAM, $C000-$DFFF)
+is physical RAM that survives a reset, unlike a real Spectrum ROM cold
+boot which always starts from a cleared display file. Any zx81sd
+program that doesn't call `CLS` itself (many quick debug tests just
+`PRINT` sequentially) could show leftover pixels/attributes from
+whatever ran before it.
+
+Fix: `runtime/bootstrap.asm`'s `SD81_INIT_SYSVARS` (already run via
+`#init` before every program, right after setting `SCREEN_ADDR`/
+`SCREEN_ATTR_ADDR`/`ATTR_P`) now also clears the physical screen:
+6144 bytes at `$C000` to 0, 768 bytes at `$D800` to the just-set
+`ATTR_P` value ($38 = INK 0/PAPER 7). Same approach as the shared
+`cls.asm`, inlined here to avoid depending on CLS being linked into
+every binary.
+
+This is in the shared bootstrap, so every zx81sd binary gets it
+automatically on recompilation — no per-test changes needed. Verified
+by simulation: pre-filled screen/attr RAM with garbage before boot,
+confirmed both regions read back as fully cleared (0 / $38) right
+before the program's own first PRINT, via a Python harness tracing
+PC/BC/HL through the LDIR loops.
+
+## SD81DBufOn/SD81DBufOff/SD81WaitVSync — double buffer library, port $E7 (2026-07-07)
+
+New `stdlib/dbuf.bas`: wraps the SD81 Booster's present-blit double
+buffer feature. Ported from `SD81-Booster/EXAMPLES/DBUF/bounce.asm`
+(reference demo authored alongside the FPGA feature), adapted for
+zxbasic because memory-mapped IO is disabled by `tools/boot1.asm`
+(`POKE 2056,0`) before any compiled program runs, so the classic
+`POKE 2057` control path is unusable from zxbasic — everything goes
+through port `$E7`'s pseudo-block-8 encoding instead (`OUT` with
+A=8, B=32+frontBlk to enable, B=0 to disable). `boot1.asm` also leaves
+the mapper in full-paging mode permanently, which is exactly the state
+this path requires, so it works unconditionally on every zx81sd binary.
+
+The front buffer is *not* one of the program's own blocks 0-5 (code/
+data, see `_PAGE_MAP` in `backend/main.py`), nor block 6 (the live
+screen) nor block 7 (data banking, `Map()` in `mcu.bas`) — it lives in
+the FPGA's private screen shadow RAM, addressed with its own
+independent 0-7 index. Per the hardware spec, blocks 4/5 are the
+recommended choice and 0-3/6-7 should be avoided; the library and demo
+use 5.
+
+`examples/sd81/bounce_sd81.bas` (mirrored to the private repo as
+`tests_debug/bounce_sd81.bas`, packaged as `BOUNCE.P`/`BOUNCEP8.BIN`) is
+a straight port of the reference demo's logic (erase/~6ms delay/move/
+draw synced to VSYNC, SPACE toggles dbuf live, M freezes movement, Q
+quits) — much shorter than the original because zx81sd's boot already
+sets up the screen address, Superfast HiRes Spectrum mode, Chroma81
+colour and the initial screen/attribute clear (see the previous
+section), none of which bounce.asm can assume when running standalone.
+
+This is the **first test of the port $E7 dbuf path** — only the
+`POKE 2057` path had been validated on real hardware before this.
+Verified so far only in simulation (Python `z80` harness): confirmed
+`SD81DBufOn(5)` emits exactly `OUT $E7` with A=8/B=37 (bit5 enable,
+front=5) and the border changes to green as expected; the main loop
+runs correctly through many frames (10M+ simulated T-states) with
+`PaintBall` writing the expected 32-byte block each time. The simulator
+doesn't model the FPGA's video timing/blit, so whether the dbuf
+actually eliminates tearing on screen can only be confirmed on real
+hardware — that's the point of this demo.
+
+### 2026-07-07 fix: `Move()` bouncing early (~y=128 instead of 176)
+
+Reported after real-hardware testing: the ball in `bounce_sd81.bas`
+bounced roughly mid-screen instead of reaching the bottom, as if it
+had hit an early floor.
+
+Root cause: `Move()`'s original version widened `bally (ubyte) + dy
+(byte)` to an `integer` for the range check. The generated code
+(`ld a,(_bally) / ld h,a / ld a,(_dy) / add a,h / ld l,a / add a,a /
+sbc a,a / ld h,a`) computes the 8-bit sum correctly in the low byte,
+then **sign-extends that 8-bit result based on its own bit 7** to fill
+the high byte. That's correct if both operands were meant to be signed
+bytes, but `bally` is genuinely unsigned (0-176) — `bally=126, dy=2`
+gives an 8-bit sum of 128 ($80, bit 7 set), which gets sign-extended to
+-128 instead of +128. `if newY < 0` then true firing the bounce 48
+pixels early.
+
+Fix: keep `Move()` entirely in `ubyte` (mod-256) arithmetic, matching
+`bounce.asm`'s own `add a,b / cp 177` approach — never widen the
+ubyte+byte sum to a signed integer at all, compare unsigned instead
+(`if newY >= 177`). Verified by simulation: `bally` now climbs smoothly
+through 128, 130, ... past the point where it used to bounce.
+
+General lesson for this port: **`ubyte + byte` widened to `integer`
+sign-extends the 8-bit sum's own bit 7, not the semantically correct
+signed value** — safe when both operands are small/signed, wrong when
+one operand (like a screen coordinate) is genuinely unsigned and can
+exceed 127. Do that arithmetic in a type wide enough from the start
+(assign the ubyte to an integer/uinteger variable *before* adding), or
+stay in 8-bit space with an unsigned comparison if the range allows it,
+as done here.
+
+### 2026-07-14: RST $38 behaves like a real HALT instead of hanging forever
+
+User request: interrupts stay disabled for the whole program (by
+design), so RST $38/IM1 "should never be reached" — but if something
+did reactivate them, the old `di`+`halt` handler locked the machine up
+permanently. Changed it to wait for the next VSYNC pulse (port `$AF`,
+bits 6-1 = pulse counter, reset on read) and `ret`, approximating what
+a real Z80 HALT does (resume after the next interrupt) instead of
+hanging.
+
+**Found along the way: `src/lib/arch/zx81sd/runtime/vectors.asm` was
+dead code, never `#include`d by anything.** The actual RST vector table
+is emitted directly as Python-generated ASM text in
+`backend/main.py::emit_prologue()` (`org $0038` / `di` / `halt`, etc.)
+— a duplicate of what `vectors.asm` described, added in the same
+original commit but apparently never wired together. First attempt at
+this fix edited `vectors.asm` and got silently ignored (compiled
+binaries kept the old `di`/`halt` bytes at $0038) until byte-inspecting
+the actual output caught it. Fixed the real source in `main.py` instead
+and deleted `vectors.asm` to remove the misleading duplicate — if
+`vectors.asm` (or any other zx81sd runtime `.asm` file) needs editing
+again, first grep for whether it's actually `#include`d/`#require`d by
+anything, don't assume a file with a plausible name and location is
+live.
+
+Verified: compiled binary now has `DB AF E6 7E 28 FA C9` at `$0038`
+(`in a,($AF)` / `and $7E` / `jr z,-6` / `ret`) instead of `F3 76`
+(`di`/`halt`); simulated a stray RST $38 with a port callback that
+returns "no pulse" 3 times then "pulse" — confirmed the loop reads the
+port exactly 4 times (3 waits + the one that breaks it) before
+falling through to `ret`.
+
+**Also discussed and closed: returning to the ZX81 BASIC prompt after a
+program ends**, instead of the permanent `di`/`halt` at
+`__END_PROGRAM`. Not feasible, for two independent reasons:
+
+1. **No software reset path exists in the FPGA at all** (confirmed by
+ the hardware's author): `nRESET` in `SD81.v` is a plain input wire
+ driven by the board's own reset circuit — nothing in the Z80-facing
+ register set can pulse it. There is no port write or mapper page
+ value that re-triggers the power-on sequence from software.
+2. Even if there were, `tools/boot1.asm` remaps block 0 away from the
+ ZX81's own ROM/RAM to run the compiled program, and `USAGE.md`'s own
+ loader documentation notes the mapper's full-paging mode "doesn't go
+ back to simple mode until the next reset" — a one-way transition by
+ design. And the original BASIC session's own RAM (variables, display
+ file, stack) has already been overwritten by the compiled program by
+ the time it runs, so there's no state left to return *to* anyway.
+
+Bottom line: the closest thing to "exit to BASIC" a zx81sd program can
+offer is `RST 0` (restarts the *current* program, already wired in the
+vector table above) — a genuine return to a ZX81 BASIC prompt requires
+the physical reset. Not revisiting unless the hardware itself gains a
+software-triggerable reset line.
+
+## `array.asm` corrupted code on every multi-dimensional array access — RESOLVED (2026-07-22)
+
+Found while porting a third-party game (ZXOilPanic, heavy user of 2D
+arrays like `sprite(29,3)`, `down(4,1)`, etc.) to zx81sd. Caught live on
+real hardware with EightyOne's debugger: a write breakpoint on the Z80
+stack pointer's own descent showed a `PUSH DE` corrupting `FP_STKEND`
+(`$8024`), which cascaded into the floating-point calculator operating
+on garbage addresses, eventually executing screen/attribute memory as
+code and crashing (`RST 8`, `RST 30`, or a wild jump into `$C000+`,
+depending on what garbage byte the CPU happened to land on).
+
+Root cause, once traced back far enough: `src/lib/arch/zx48k/runtime/
+array/array.asm` (`__ARRAY`, the shared N-dimensional array indexing
+routine used by **every** zxbasic program that declares a
+multi-dimensional array) uses `LBOUND_PTR EQU 23698` — the real
+Spectrum's `MEMBOT` system variable — as scratch storage for 4 pointer
+pairs (8 bytes: `LBOUND_PTR`/`UBOUND_PTR`/`RET_ADDR`/`TMP_ARR_PTR`).
+On a real Spectrum that's safe ROM-reserved RAM. On zx81sd, address
+23698 ($5C92) falls **inside the program's own compiled code** (block 2,
+$4000-$5FFF) instead of a real sysvar — every array access silently
+corrupted a few bytes of code there. This is a **general zx81sd port
+bug**, not specific to this game: any zx81sd program using
+multi-dimensional arrays was at risk, it just hadn't been exercised
+heavily enough by earlier examples/tests to surface (none of them used
+2D+ arrays this intensively).
+
+Fix: new `src/lib/arch/zx81sd/runtime/array/array.asm`, an override of
+the shared file (Boriel's rule: the shared one stays untouched) with
+the only change being `LBOUND_PTR EQU ARRAY_SCRATCH` instead of `EQU
+23698` — `ARRAY_SCRATCH` is a new dedicated 8-byte sysvar
+(`sysvars.asm`, `SYSVAR_BASE + $83`, right after `FP_MEM_AREA`, still
+well within the $8000-$80FF sysvar block before the heap at $8100).
+
+Verified by simulation: 1.2 billion T-states with a write-monitor on
+both the old MEMBOT address (zero writes now) and the two known
+hang-vector addresses (`$0008`/`$0009`, `$0030`/`$0031` — never
+reached). Previously, with the same binary before this fix, the
+program reliably crashed within a few hundred million T-states via one
+of those two RST vectors.
+
+## Same bug, two more places: `chr.asm` and `arith/divf.asm` — RESOLVED (2026-07-22)
+
+The array.asm fix above turned out to be one instance of a **systemic
+pattern** across the shared zx48k runtime: a project-wide grep for
+`EQU 2[34]...` found roughly a dozen files that hardcode raw Spectrum
+system-variable addresses as local scratch/temp storage, bypassing
+`sysvars.asm` entirely (so zx81sd's own sysvars.asm override never
+gets a chance to relocate them). Confirmed two more of these actually
+firing while continuing to debug ZXOilPanic on real hardware after the
+array.asm fix — the corruption just moved to a different address once
+the binary's layout shifted:
+
+- `runtime/chr.asm` (the `CHR$()` function, called 6 times per score
+ update in this game) uses `TMP EQU 23629` (Spectrum's `DEST`) to
+ stash the return address while calling `__MEM_ALLOC`. On zx81sd,
+ 23629 ($5C4D) landed inside `__DIVU32` (32-bit division) in the
+ build being tested.
+- `runtime/arith/divf.asm` (floating-point division, used by
+ `score/66.0` in this game) uses `TMP EQU 23629` (same address as
+ chr.asm) and `ERR_SP EQU 23613` to save/restore a stack recovery
+ point around the division (a "longjmp" trick for divide-by-zero,
+ matching the real Spectrum ROM's error-trapping convention). This
+ code runs on **every** division, not just on error — worth noting
+ that on zx81sd this recovery mechanism doesn't actually do anything
+ useful anyway, since `__ERROR` does `DI`+`HALT` directly rather than
+ restoring `SP` from `ERR_SP` to unwind — but it still unconditionally
+ writes to both addresses every time regardless.
+
+Fix, same pattern as array.asm: new `runtime/chr.asm` and
+`runtime/arith/divf.asm` overrides, each with only the address changed
+(`CHR_SCRATCH` at `$808B`, `DIVF_SCRATCH` — TMP+ERR_SP, 4 bytes — at
+`$808D`, both in `sysvars.asm` after `ARRAY_SCRATCH`).
+
+Verified by simulation with a corrected VSYNC-pulse mock (the earlier
+simulations in this session had used an oversimplified port-toggle mock
+that doesn't match the real bit-6-1 pulse-counter semantics `PAUSE`
+expects, which risked mid-testing false "hangs"): monitored writes
+across the **entire** program's address space (not just block 2) for
+200 million T-states — zero unexpected corruption, versus reliably
+corrupting code within a few hundred thousand T-states before these
+two fixes.
+
+**Not yet audited**: the same grep turned up further untouched
+hardcodes in `runtime/arith/modf16.asm` (`TEMP EQU 23698`, same as
+array.asm's old MEMBOT) and `runtime/break.asm`'s `PPC` — the former
+isn't exercised by this particular game (no `MOD` operator used) so it
+was left alone for now; a full systemic audit of every zx48k runtime
+file for this pattern is worth doing separately at some point, this
+session only fixed the ones actually observed corrupting a real
+program.
+
+## Stray EI in ported code corrupted arithmetic at random — RESOLVED
+
+**Symptom (ZXOilPanic)**: `sprite(13,2)` — a plain 2D-array read —
+returned -1 instead of 2072, but only when the game ran at full speed:
+single-stepping in the debugger never reproduced it, the array's data
+in RAM was verified intact (no writes ever hit it), and an isolated
+test reading the same array/index worked perfectly. The wrong value
+(-1) made the sprite blit read from `@sprites - 1`, painting garbage.
+
+**Root cause**: the game's `tilexy` asm block ends with `EI` (and the
+BeepFX engine's exit path too) — normal on the Spectrum, but zx81sd's
+runtime runs with interrupts disabled at all times. After the first
+`tilexy` call, interrupts stayed enabled, and the ZX81 fires IM1 INTs
+continuously (A6 low during refresh), landing on `$0038`. Our
+`RST38_WAIT` vector clobbered **A and flags** (`in a,($AF)` / `and`),
+so any interrupted computation (e.g. `__ARRAY`'s offset arithmetic)
+resumed with corrupted A/F. Classic heisenbug: stepping in EightyOne
+doesn't deliver INTs the same way, so it vanished under the debugger.
+
+**Fix (two layers)**:
+1. Game side: `#ifndef __ZX81SD__` around both `EI`s (ported programs
+ must never re-enable interrupts on zx81sd).
+2. Runtime side (defense-in-depth, `backend/main.py`): the `$0038`
+ vector now does `push af` / wait for VSYNC pulse / `pop af` / `ret`,
+ so a stray INT costs only time instead of corrupting registers.
+
+**Lesson**: when porting Spectrum code, grep for `\bei\b` and `\bhalt\b`
+in every asm block. A heisenbug that disappears under single-stepping
+on zx81sd should immediately suggest interrupts got re-enabled.
diff --git a/src/arch/zx81sd/doc/PORTING_GUIDE.md b/src/arch/zx81sd/doc/PORTING_GUIDE.md
new file mode 100644
index 000000000..4d4a6b5dc
--- /dev/null
+++ b/src/arch/zx81sd/doc/PORTING_GUIDE.md
@@ -0,0 +1,212 @@
+# Step-by-step guide: porting a Spectrum ZX BASIC program to zx81sd
+
+This is a checklist, not a tutorial — each step links to the doc that
+has the full detail. Work through it roughly in order; most of it only
+matters if the specific pattern shows up in the program you're
+porting. Everything here was distilled from real ports (official
+`examples/`, and a full third-party game, ZXOilPanic) that hit one or
+more of these in practice — see [MAP.md](MAP.md) for the investigation
+traces if you want the "why" behind any of them.
+
+## 0. The one rule that makes everything else optional
+
+**Keep the exact same `.bas` source compiling for both `zx48k` and
+`zx81sd`.** Don't fork the file, don't overwrite Spectrum-targeting
+code unconditionally — wrap every zx81sd-specific change:
+
+```basic
+#ifdef __ZX81SD__
+ ' zx81sd-specific code
+#else
+ ' original Spectrum code
+#endif
+```
+
+This works identically inside `asm ... end asm` blocks (the
+preprocessor runs at the text level, before the BASIC/ASM split).
+Compile for each target with:
+
+```
+python zxbc.py --arch=zx48k -o out_spectrum.bin
+python zxbc.py --arch=zx81sd -D __ZX81SD__ -o out_zx81sd.bin -M out_zx81sd.map
+```
+
+(or the one-shot [`build_sd81.py`](../tools/build_sd81.py) for the
+zx81sd side, see step 8). `zxbasic` never defines `__ZX81SD__`
+automatically — you always pass it by hand with `-D`.
+
+## 1. There is no ROM: hunt down hardcoded Spectrum addresses
+
+Grep the source (and any library `.bas`/`.asm` it pulls in) for:
+- Raw numeric `POKE`/`PEEK`/`CALL` addresses (4-5 digit decimals, or
+ hex like `$5C..`/`$22..`).
+- Anything that looks like a Spectrum ROM sysvar name in a comment
+ (`UDG`, `COORDS`, `BORDCR`, `FRAMES`, `ATTR_P`...).
+
+Every one of these needs translating via the **full equivalence
+table** in [SYSVARS.md](SYSVARS.md) (Spectrum address ↔ zx81sd
+address, for every sysvar zx81sd exposes). A few you'll hit almost
+immediately even in simple programs:
+
+- **Screen/attribute base address**: Spectrum `$4000`/`$5800` →
+ zx81sd `$C000`/`$D800`. Common in raw `LDIR`/`PEEK`/`POKE` screen
+ code that doesn't go through `SCREEN_ADDR`/`SCREEN_ATTR_ADDR`.
+- **ULA port** (border color / beeper, `OUT`/`IN` on `$FE`/254):
+ zx81sd emulates the same border+beeper bit layout at `$FB`/251
+ instead. Full detail and the keyboard-port caveat in SYSVARS.md's
+ "Quick reference" table — **don't** just s/254/251/ on a raw `OUT`,
+ route it through the shadow-byte mechanism `BORDER`/`BEEP` already
+ use, or you'll clobber whichever of border/beeper you didn't intend
+ to touch.
+- **AY sound chip ports** (Spectrum 128's `$FFFD`/`$BFFD` register
+ latch/data) → zx81sd's SD81 Booster ZonX-81 interface at `$CF`/`$0F`
+ instead — see `stdlib/play.bas`'s `_PLAY_WRITE_TO_REGISTER` for the
+ reference. If the program pokes raw note dividers into AY registers
+ directly (not through `PLAY`), also recompute them for the SD81's
+ 1.625 MHz AY clock (vs the Spectrum 128's 1.7734 MHz) — same
+ SYSVARS.md section has the formula.
+
+See [PRECAUTIONS.md](PRECAUTIONS.md) section 1 for the full rationale
+and [BASIC_CHANGES.md](BASIC_CHANGES.md) for a line-by-line catalog of
+every case already found in the official examples.
+
+## 2. Never `EI`, never wait on `HALT`
+
+zx81sd runs the **entire program** with interrupts disabled. Grep
+every ASM block (inline `asm...end asm` and any hand-written `.asm`
+library) for `\bei\b` and `\bhalt\b`:
+
+- `EI` (with or without a following `HALT`) is dangerous — see
+ [PRECAUTIONS.md](PRECAUTIONS.md) section 4 for exactly how bad
+ (a real, hours-long heisenbug came from a single stray `EI` at the
+ end of a sprite-blit routine, corrupting unrelated array reads
+ later on).
+- A `HALT`-based wait for the Spectrum's 50Hz interrupt never wakes
+ up on zx81sd. Replace it with `Pause n` (BASIC) or `call
+ .core.VSYNC_TICK` (ASM) — see PRECAUTIONS.md section 4 for the
+ exact substitution and namespace-prefix gotcha.
+
+**Diagnostic tell**: if a bug reliably disappears while single-stepping
+in the debugger but reproduces at full run speed, suspect a stray `EI`
+first, before anything else.
+
+## 3. Register contracts and ASM label namespaces
+
+If the program includes hand-written `.asm` that calls into runtime
+routines directly (not just through BASIC statements), check
+[PRECAUTIONS.md](PRECAUTIONS.md) sections 2 and 5: some routines have
+non-negotiable register contracts (a replaced ROM routine must
+preserve exactly what the original did), and zx81sd's namespace
+mangling means a bare label reference from inside an `asm` block that
+isn't already inside `push namespace core` needs the full `.core.`
+prefix or you'll get `Undefined GLOBAL label`.
+
+## 4. The keyboard is the ZX81's, not the Spectrum's
+
+Any code that reads the keyboard by hand (not through `INKEY$`/
+`INPUT`) via raw port I/O needs rework — the ZX81's keyboard matrix
+layout is different from the Spectrum's, even where a port number
+happens to coincide (see SYSVARS.md's ULA-port note). Full detail in
+[PRECAUTIONS.md](PRECAUTIONS.md) section 3. Also remember: **the ZX81
+keyboard has no shift-driven case distinction** — `SHIFT`+letter
+produces a symbol, not uppercase, a common surprise when porting a
+game that expects to read letter case directly (see
+`zx81sd-keyboard-case` project memory if you're the original author
+of this port — not a zxbasic-level fact, a physical hardware one).
+
+## 5. `zxbasm` binary literal syntax
+
+If the program has hand-written `.asm` using the trailing-`b` Pasmo/
+Zilog binary literal convention (`10110010b`), it won't assemble —
+`zxbasm` only accepts the leading form: `%10110010` or `0b10110010`.
+Convert with a straightforward `sed`/regex pass before anything else,
+it's a pure syntax issue with no semantic risk.
+
+## 6. Dead-code elimination and `@function` references
+
+If a subroutine or array is only ever referenced via its **address**
+(`@myFunction`, `@myArray`) from inside another function's body — never
+called directly, and never referenced from global/top-level code —
+zxbasic's dead-code eliminator can fail to see it as "used" and strips
+it, causing an `Undefined GLOBAL label` at link time (a real gap: the
+optimizer's call-graph walker tracks `CALL`/`FUNCCALL` nodes, not
+`ADDRESS` nodes, and address-of-inside-a-function-body is a separate
+code path that isn't marked as "accessed"). Workaround: a throwaway
+address-of reference at **global** scope keeps it alive:
+
+```basic
+' Compiler dead-code-elimination gap: @sprites is only ever taken from
+' inside a function body, which isn't enough to mark it as used.
+dim __keep_sprites as uinteger: __keep_sprites = @sprites
+```
+
+If you're banking a large data table (sprites, a level map...) into
+block 7 to fit under the `$8000` budget (step 8) by wrapping a `sub`'s
+`asm` body in a manual `ORG $E000`/restore, `@name` stops pointing at
+the relocated data entirely (a different, related gap — see
+[PRECAUTIONS.md](PRECAUTIONS.md) section 9) — you'll need both this
+dead-code workaround *and* to stop using `@name` as an address.
+
+## 7. Sysvar scratch bugs already fixed in the runtime — no action needed
+
+Several shared `zx48k/runtime/` files historically hardcoded a raw
+Spectrum sysvar address as scratch storage (bypassing `sysvars.asm`
+entirely) — on zx81sd that address lands inside the program's own
+compiled code, corrupting it. `array.asm` (multi-dimensional array
+reads), `chr.asm` (`CHR$()`), and `arith/divf.asm` (floating-point
+division) all had this bug and are **already fixed** with zx81sd
+overrides — nothing to do here, just don't be surprised if you find
+the override files while reading the runtime. `arith/modf16.asm`
+(the `MOD` operator) has the same pattern and is **not yet fixed**
+(not triggered by any program so far) — if a program using `MOD`
+shows array-adjacent corruption, this is the first suspect. Full list
+and the exact addresses in [SYSVARS.md](SYSVARS.md)'s hardcoded-scratch
+tables.
+
+## 8. Compile, package, and copy to the right place
+
+```
+python src/arch/zx81sd/tools/build_sd81.py [PREFIX] --copy-to
+```
+
+Does the compile (with `-D __ZX81SD__` and `-M` for the symbol map)
+and the SD81 page-splitting + `.P`-loader generation in one command.
+**Get `--copy-to` right, or point it at the emulator/hardware's real
+SD folder by hand afterwards** — a stale package the emulator wasn't
+actually reading was itself the cause of a multi-hour false-alarm
+debugging session once, see [USAGE.md](USAGE.md) section 3b. If you
+only need a fresh `.map` without repackaging (e.g. after hand-editing
+the generated `.asm`), see `gen_map.py` in the same section.
+
+## 9. Test and debug
+
+- Simulating with Python's `z80` package before touching real
+ hardware/emulator catches a lot, but has real limits: an
+ oversimplified VSYNC-port mock can get a program stuck early, and a
+ bug that depends on genuine interrupt timing (step 2) **cannot** be
+ reproduced by a simulator that never delivers interrupts in the
+ first place. See [MAP.md](MAP.md) and USAGE.md section 4 for the
+ methodology and its known traps.
+- Live debugging in EightyOne's Debug Window: execution breakpoints
+ with hit-counts (`EXE=$addr`), write breakpoints (`WR=$addr`), and
+ the Memory viewer (cross-referenced against the `.map` file) are the
+ main tools — see MAP.md for worked examples of narrowing down a bug
+ this way, including how to avoid re-triggering on the wrong call
+ when many call sites share the same target address.
+- **`.map` must match the binary actually loaded.** Regenerate it on
+ every rebuild (`build_sd81.py`/`gen_map.py` both do); a mismatched
+ `.map` gives confidently-wrong addresses that waste debugging time.
+
+## See also
+
+- [PRECAUTIONS.md](PRECAUTIONS.md) — the detailed rationale behind
+ steps 1-4, one section each.
+- [SYSVARS.md](SYSVARS.md) — full Spectrum ↔ zx81sd sysvar and I/O
+ port equivalence table.
+- [BASIC_CHANGES.md](BASIC_CHANGES.md) — catalog of source changes
+ already needed in official examples.
+- [USAGE.md](USAGE.md) — compiling, packaging, and simulating in
+ detail.
+- [MAP.md](MAP.md) — full technical log, bug by bug, with the
+ investigation traces (the primary source almost every step above
+ was distilled from).
diff --git a/src/arch/zx81sd/doc/PRECAUTIONS.md b/src/arch/zx81sd/doc/PRECAUTIONS.md
new file mode 100644
index 000000000..b5b41f8f8
--- /dev/null
+++ b/src/arch/zx81sd/doc/PRECAUTIONS.md
@@ -0,0 +1,293 @@
+# Precautions when writing or porting software for zx81sd
+
+zx81sd makes ZX BASIC generate binaries that "look like" a Spectrum
+(the SD81 Booster interface emulates its screen, and much of the
+shared stdlib assumes Spectrum conventions), but **there is no
+Spectrum ROM anywhere**: no ROM `RST $28`, no routines at fixed
+addresses, no Spectrum sysvars at `$5C00+`. Almost every bug in this
+port has come from code (official `examples/` or the shared stdlib)
+that silently assumes one of these things. Check this list before
+porting anything.
+
+## 1. There's never a ROM: watch out for absolute addresses and sysvars
+
+Any `POKE`/`PEEK`/`CALL` to a fixed numeric address (23675, 23658,
+$22AC, $0DFE...) is almost certainly a **Spectrum ROM** sysvar or
+routine, which doesn't exist in zx81sd: that address lands in free RAM,
+or worse, in the middle of the program's own compiled code — executing
+or interpreting it as data causes silent corruption, wrong graphics, or
+a runaway `HALT`/reset that's very hard to trace back to its cause
+(several bugs in this port took whole sessions to diagnose because of
+this).
+
+- **Spectrum sysvars → zx81sd sysvars**: the full equivalence table
+ (every symbol, its Spectrum ROM address, and its zx81sd address) is
+ in [SYSVARS.md](SYSVARS.md) — check it first when a numeric address
+ turns up while porting code. The zx81sd definitions themselves live
+ in [`../../../lib/arch/zx81sd/runtime/sysvars.asm`](../../../lib/arch/zx81sd/runtime/sysvars.asm)
+ (all at `$8000+`, not `$5C00+`). Examples already solved:
+ `UDG` (23675 → `$8002`), `COORDS` (23677/23678 → `$8004`/`$8005`).
+ See [BASIC_CHANGES.md](BASIC_CHANGES.md) for the line-by-line detail
+ of every case found so far.
+- **ROM routines called directly** (`call $22AC` = PIXEL-ADD,
+ `call $0DFE` = CL-SC-ALL/scroll, `RST $28` = FP calculator...): if the
+ source or a shared library does this, it needs an override in
+ `src/lib/arch/zx81sd/` that replaces the call with our own routine,
+ keeping the **same register contract** as the ROM one (see below).
+ Example already solved: `stdlib/scroll.bas`.
+- **Preventive `grep`**: when porting a file from `zx48k/` to
+ `zx81sd/`, search for `EQU 0[0-9A-F]` / `call 0x` / suspicious
+ 4-5-digit literals before signing off on it, not only once something
+ actually fails.
+
+## 2. ASM routine register contracts are sacred
+
+Several runtime routines have explicit, non-negotiable register
+preservation contracts, because the code that calls them (inherited
+from zx48k, not something we can touch) depends on them to the letter.
+Examples:
+
+- `PIXEL_ADDR` (`runtime/pixel_addr.asm`): A=191, B=Y, C=X → HL=offset,
+ A=X AND 7; **destroys B, preserves D and E**. `draw.asm` stores the
+ Bresenham coordinate in D around the call, trusting this literally —
+ breaking it (as happened once with an attempt that used D as
+ scratch) corrupts every line with a vertical component while leaving
+ horizontal ones untouched, which is very misleading to diagnose.
+- `GetBankPreservingRegs`/`SetBankPreservingINTs` (MSFS,
+ `cb/maskedsprites.bas`): documented contract "preserves D,E,H,L".
+ Writing the replacement in plain BASIC instead of hand-written ASM
+ breaks this with no warning from the compiler (compiled BASIC code
+ uses registers freely internally) — a real bug of this kind made all
+ 6 test sprites register at the same wrong address. **Note**: the
+ MSFS/`maskedsprites.bas` port is still in progress (still not fully
+ working) — this specific example is here to illustrate the class of
+ bug, not to confirm the library is finished.
+
+**Practical rule**: if you're going to replace an ASM routine that has
+a documented register contract (or one that can be inferred from
+looking at its callers and what they assume), reimplement it in
+hand-written ASM preserving that exact contract. A BASIC function
+(`SUB`/`FUNCTION`), however simple it looks, is NOT a valid replacement
+unless the contract is "none".
+
+## 3. The keyboard is the ZX81's, not the Spectrum's
+
+The SD81 Booster doesn't have a Spectrum keyboard: it rescans the
+ZX81's own physical 40-key keyboard (`runtime/io/keyboard/keyscan.asm`).
+Differences that matter when porting/writing code:
+
+- Direct key press: lowercase. `SHIFT+letter`: UPPERCASE of that letter
+ — exactly like a real Spectrum, unlike the original ZX81 (where
+ `SHIFT+letter` produced a symbol, not an uppercase letter). This
+ redefinition is a design decision of this port, see [MAP.md](MAP.md)
+ section "New keyboard scheme". As a result, **you can indeed
+ scan/compare `INKEY$` against uppercase letters**
+ (`IF INKEY$="S"`, meant to be played holding `SHIFT` Spectrum-style)
+ with no hardware limitation: just press `SHIFT+S`.
+- `SHIFT+"2"` toggles a persistent CAPS LOCK.
+- `"."` is a normal key (`.` unshifted, `,` shifted). The original
+ ZX81 symbols associated with each key (`:` on Z, `)` on O, etc.) are
+ reached with the `"." + key` sequence **only from `INPUT()`** (a
+ dead-key handled in `stdlib/input.bas`), not from raw `INKEY$` — there
+ is no reliable way to "hold both at once" on this keyboard, so the
+ composition is done by pressing `.` first and the second key after.
+ Pressing `.` **twice in a row** confirms the first dot as a literal
+ and cancels any pending combo: the key that comes after that second
+ dot is read as a fresh keypress, with no combination (lets you type
+ any key right after a dot without risking forming a symbol by
+ accident).
+
+## 4. There are no interrupts: never `EI`, never wait on `HALT` to synchronize
+
+The zx81sd runtime always runs with interrupts disabled (`DI`) for the
+**entire program**, not just around specific routines. Any ported code
+(typically inline ASM copied from a Spectrum source, not the stdlib)
+that does `EI` — anywhere, even if it's never followed by a `HALT` —
+is dangerous on zx81sd in two different ways:
+
+- `EI` followed by `HALT` waiting for the Spectrum ROM's 50Hz pulse
+ **hangs forever** — nothing ever wakes it up.
+- `EI` **on its own**, with no following `HALT`, is more insidious:
+ interrupts stay enabled for the rest of the program's execution, and
+ the ZX81 fires IM1 INTs continuously (roughly every scanline, tied
+ to video refresh). Every one of those jumps to `$0038`. That vector
+ preserves `AF` (`push af` / wait for VSYNC / `pop af` / `ret` — see
+ `backend/main.py`) precisely because this bug was found the hard
+ way, but it does **not** preserve anything else — if the interrupt
+ lands mid-computation in code that's mid-flight on `BC`/`DE`/`HL`
+ (typical of `__ARRAY`'s offset arithmetic, which uses the shadow
+ register set across an extended, uninterruptible-on-a-real-Spectrum
+ span), the result is silent, apparently-random data corruption whose
+ symptom shows up somewhere completely unrelated later. This exact
+ bug cost a multi-hour live-hardware debugging session on a ported
+ game (`sprite(N, col)` reading `-1` instead of its real value) before
+ being traced back to a stray `EI` at the end of an XOR-blit routine
+ — full writeup in [MAP.md](MAP.md) and the `oilpanic-portability`
+ project memory. **Grep every ported ASM block for `\bei\b` before
+ testing, the same way you'd grep for hardcoded addresses (section
+ 1).** A bug that reliably disappears when single-stepping in the
+ debugger but reproduces at full speed is the signature to watch for
+ — EightyOne doesn't deliver INTs identically while paused.
+
+- **Replacement**: `VSYNC_TICK` (namespace `core`, in
+ `runtime/vsync.asm`) polls the SD81 Booster hardware's real VSYNC
+ pulse counter through a port. `PAUSE` already uses it internally.
+- When calling it from an `ASM ... END ASM` block that isn't already
+ inside `push namespace core`, you need the full prefix:
+ `call .core.VSYNC_TICK` (omitting the prefix gives
+ `Undefined GLOBAL label '.VSYNC_TICK'` — an error already seen more
+ than once in this port).
+- A counter that used to be incremented only by the background
+ interrupt (`FRAMES`/23672 on the Spectrum) needs to be updated by
+ hand, calling `VSYNC_TICK` explicitly on every loop iteration that
+ still needs to wait, not just once at the start.
+
+## 5. ASM label namespaces and mangling
+
+A BASIC `DIM X` or `SUB`/`FUNCTION X` translates to the ASM label `_X`
+(a single leading underscore), **unless** the file wraps its code in
+`push namespace core ... pop namespace`, in which case you must
+reference it from outside as `.core._X` (variables) or `.core.X`
+(functions/routines). Getting this wrong in either direction produces
+`Undefined GLOBAL label`. If a file in this port doesn't use
+namespacing anywhere else, there's no need to wrap a new `ASM` block in
+`push namespace core` just because another file (like `vsync.asm`)
+does — just prefix the specific reference.
+
+## 6. The dead-code eliminator can't see calls made from hand-written ASM
+
+The compiler's "is this used?" analysis only counts calls made with
+BASIC call syntax (`Foo(x)`). A BASIC `SUB`/`FUNCTION`/variable
+referenced **only** from an `ASM ... END ASM` block (e.g. `call _Foo`)
+can be eliminated as dead code, producing `Undefined GLOBAL label
+'._Foo'` at link time — the symbol never made it into the final
+binary. Two ways out:
+
+- If it's pure data (a state byte, for example), declare it as raw ASM
+ (`ASM \n _Label: \n DEFB 0 \n END ASM` at file scope), not as `DIM`.
+- If it's a function that genuinely needs to be BASIC (because it
+ calls other stdlib things), add one real BASIC-level call (even if
+ redundant/not strictly needed at that point) somewhere reachable in
+ the code, so the usage analysis counts it.
+
+## 7. Debugging methodology without hardware
+
+To diagnose things without burning trial-and-error cycles on the
+emulator or real hardware, this port uses direct simulation of the
+binary with Python's `z80` package. Two lessons already learned the
+hard way (documented in more detail in [MAP.md](MAP.md)):
+
+- Checking the PC periodically every N coarse ticks can give a false
+ "stuck" reading if it just happens to always land on the same point
+ of a loop; use real breakpoints (compare `m.pc` against the exact
+ address, taken from the `.map`) or finer tick chunks.
+- The simulator's RAM starts at **zero**, just like a Spectrum's after
+ its ROM's RAM test. This hides uninitialized-memory bugs (it gave two
+ false "OK" results in a row in the MSFS free-space-bitmap bug). To
+ validate code that reads memory it doesn't initialize itself, fill
+ all unloaded RAM with `0xFF` before loading the binary, to reproduce
+ real hardware/card conditions.
+- The simulator **doesn't model the memory mapper** (`OUT` to port
+ `$E7` is a no-op): it can validate that the Z80 logic is
+ self-consistent, but not that the actual physical page-swapping
+ works — that's only confirmed on the emulator (EightyOne) or real
+ hardware.
+- If a binary fails on EightyOne but the Python simulation runs it
+ clean, suspect the emulator first (see the tape-trap bug already
+ found and fixed in `Eightyone2/src/ZX81/rompatch.cpp`, in the
+ emulator's own repository, not this one) before the runtime — those
+ traps don't exist on real hardware.
+
+## 8. Never do `#include once ` from your own library
+
+If you need zx81sd's own sysvars (`CHARS`/`UDG`/`ATTR_P`/
+`SCREEN_ADDR`/`SCREEN_ATTR_ADDR`...) from a new `stdlib/*.bas` file,
+**don't include it yourself with `#include once `** — just
+reference the symbols with the `.core.` prefix (see point 5) and trust
+that the rest of the runtime already brought it in.
+
+`sysvars.asm` drags in `bootstrap.asm` → `charset.asm`, and the latter
+does `INCBIN "specfont.bin"` (the complete font, binary bytes, not
+assembly text). If your file turns out to be the **first** one to
+include `sysvars.asm` in the whole compiled program (easy to happen: a
+`#include ` at the top of the user's source is
+processed before any `CLS`/`PRINT` that comes later in the text), that
+`INCBIN` gets emitted **right at the point in the source file where you
+put the `#include`**:
+
+- Placed at BASIC level (outside an `ASM ... END ASM` block): the
+ BASIC lexer tries to tokenize those binary bytes as if they were
+ source text → `error: illegal preprocessor character` on lines that
+ have nothing to do with the real problem (misattributed to the start
+ of the file).
+- Placed inside an `ASM` block (e.g. at the start of a function body):
+ the font binary gets emitted literally in the middle of that
+ function's compiled code — it compiles without error, but the CPU
+ "executes" those font bytes as if they were instructions the moment
+ control flow falls there, producing a `HALT` or erratic behavior at
+ an address that appears completely unrelated to the bug (found while
+ porting `print42.bas`/`print64.bas`, see
+ [BASIC_CHANGES.md](BASIC_CHANGES.md)).
+
+Any real program using your library will almost certainly also use
+`CLS`/`PRINT` somewhere, and those routines already require
+`sysvars.asm` — so leaving out the `#include` in your file is safe in
+practice, not a hack.
+
+## 9. Relocating a `sub`'s content with a manual `ORG` leaves `@name` behind
+
+If a program is too big to fit under the `$8000` sysvar/heap boundary
+(step 8 of [PORTING_GUIDE.md](PORTING_GUIDE.md)), a large constant data
+block (a sprite bitmap table, a level map...) can be banked out of the
+way by wrapping it in `ORG $E000` / restore-the-previous-`ORG` inside
+its `sub`'s `asm ... end asm` body, moving it into block 7 instead of
+the code/sysvar/heap area. This works for the **data bytes** — but
+`@name` (used from BASIC to get the sub's address, e.g. `poke @name,x`)
+keeps pointing at the sub's **original**, un-relocated position, not
+the `ORG`'d one.
+
+The reason: `@name` resolves to wherever zxbasic's own auto-generated
+`_name:` entry label ends up, and that label is emitted as literal ASM
+text **immediately before** your `asm` block's own content — i.e.
+before your `ORG` line has had any effect. Labels bind to the
+assembler's address counter at the point they're textually defined
+(ordinary, correct assembler behavior — `zxbasm` isn't doing anything
+wrong here), so redirecting the content elsewhere doesn't drag the
+label along with it. If nothing else ever gets emitted at the sub's
+"natural" position (because everything real was redirected away by the
+`ORG`), the auto-generated `_name` and `_name__leave` (end-of-function)
+labels end up at the very same address — a telltale sign in the
+debugger/disassembly that `@name` is stale: it'll resolve to
+`._name_leave`, not to your relocated data.
+
+Found while banking Mario GW's sprite bitmap table (`sub sprite()`,
+~5.4KB) into block 7: `init_sprites()`/`tilexy()`/`tilexyaddr()` all
+used `@sprite+N` to read/poke into it, silently computing addresses
+against the *old* position once the data itself moved to `$E000`.
+Symptom: no error at compile or load time — the game ran, the intro
+screen and music played fine, and it hung in an apparently unrelated
+infinite loop deep inside the sprite-blit routine (`tylexyasm`, stuck
+forever on `jr nz, sigline`), because the wrong addresses eventually
+corrupted the `(IX+n)` scratch fields the blit routine's own loop
+termination depends on — several steps removed from the actual bug.
+
+**Workaround**: don't use `@name` for a sub whose content you've
+manually `ORG`'d elsewhere — use the literal target address directly
+(e.g. `$E000`) everywhere `@name` was used, under `#ifdef __ZX81SD__`.
+The `sub` declaration itself must stay (it's still what emits the
+relocated bytes), but since nothing references it via `@name` any more,
+guard it against the dead-code eliminator (section 6 above) with a
+throwaway global reference, e.g.
+`dim __keep as uinteger: __keep = @name`.
+
+## See also
+
+- [PORTING_GUIDE.md](PORTING_GUIDE.md) — step-by-step checklist for
+ porting a Spectrum program, built from everything in this file.
+- [SYSVARS.md](SYSVARS.md) — full Spectrum ↔ zx81sd sysvar and I/O
+ port equivalence table.
+- [BASIC_CHANGES.md](BASIC_CHANGES.md) — catalog of source changes
+ already needed in official examples, with the general pattern to look
+ for in any new example.
+- [MAP.md](MAP.md) — full technical log, bug by bug, with the
+ investigation traces.
diff --git a/src/arch/zx81sd/doc/SYSVARS.md b/src/arch/zx81sd/doc/SYSVARS.md
new file mode 100644
index 000000000..85fdf411e
--- /dev/null
+++ b/src/arch/zx81sd/doc/SYSVARS.md
@@ -0,0 +1,165 @@
+# zx81sd system variables — equivalence table with the Spectrum ROM
+
+zx81sd has no ROM: every "system variable" the shared `zx48k/runtime/`
+code expects at a fixed Spectrum address is instead a real RAM
+variable allocated by zx81sd at `SYSVAR_BASE` (`$8000`) upwards, laid
+out in [`src/lib/arch/zx81sd/runtime/sysvars.asm`](../../../lib/arch/zx81sd/runtime/sysvars.asm).
+This table is the single place that maps each one back to the
+Spectrum ROM address it substitutes for, so a search for a Spectrum
+address (`23698`, `$5C7B`, etc.) found while porting code has
+somewhere to land.
+
+## Quick reference (the things every port hits first)
+
+| Concept | Spectrum | zx81sd | Source |
+|---------|:--------:|:------:|--------|
+| Pixel screen base | `$4000` (16384) | `$C000` (49152) | `SCREEN_ADDR`, see below |
+| Attribute screen base | `$5800` (22528) | `$D800` (55296) | `SCREEN_ATTR_ADDR`, see below |
+| ULA port (border color, `OUT`) | `$FE` (254) | `$FB` (251) | `border.asm` — `SD81_ULA_PORT` |
+| ULA port (beeper, `OUT`) | `$FE` (254), bit 4 | `$FB` (251), bits 4-3 | `beep.asm` — same port as border, see note below |
+| AY register-latch port (`OUT`) | `$FFFD` (65533, Spectrum 128) | `$CF` (207) | `stdlib/play.bas` — `_PLAY_WRITE_TO_REGISTER` |
+| AY data port (`OUT`) | `$BFFD` (49149, Spectrum 128) | `$0F` (15) | ditto — SD81 Booster's ZonX-81 AY interface |
+
+The ULA port note: on the real Spectrum, border color (bits 2-0) and
+beeper (bit 4) share the **same** write-only port `$FE`, so any code
+poking a raw byte to it needs to preserve the other's bits. zx81sd's
+SD81 Booster (Superfast HiRes Spectrum mode) emulates this exact
+sharing at `$FB` instead, bits laid out the same way (2-0 border,
+4-3 beeper) — `border.asm`/`beep.asm` keep a shadow byte
+(`__ZX81SD_ULA_SHADOW`) for the same reason the real ROM effectively
+needs one. **If a ported program pokes the ULA port directly instead
+of using `BORDER`/`BEEP`, change `$FE`→`$FB` (or `254`→`251`) and route
+it through the shadow byte, not a raw `OUT`** — see
+[PORTING_GUIDE.md](PORTING_GUIDE.md) step 3.
+
+Note: `$FE` is *also* a real, working port on zx81sd — but for the
+**native ZX81 keyboard matrix** (`break.asm` reads it directly for the
+BREAK key), not the Spectrum ULA. A Spectrum program that does its own
+manual keyboard scanning via `IN A,($FE)` (bypassing `INKEY$`) will
+read a **different** row/bit layout on zx81sd's actual hardware — that
+port number being valid on both machines for unrelated purposes is a
+coincidence worth being aware of, not a compatibility shortcut.
+
+The AY port note: besides the port numbers themselves, the AY chip's
+**clock speed differs too** — the SD81 Booster's FPGA clocks its AY at
+1.625 MHz (3.25 MHz / 2) versus the Spectrum 128's 1.7734 MHz. A
+program that pokes raw note-divider values into AY registers 0-13
+(rather than going through `PLAY`/`SetChipTone` etc.) needs those
+dividers recalculated for the new clock (`divider = round(1625000 /
+16 / frequency)`), or every note will play at the wrong pitch — see
+`stdlib/play.bas`'s `_Play_NoteDividers` table for the reference
+conversion, and the closed `sd81-sound-calibration` project memory for
+how this was measured and confirmed on real hardware (BEEP runs off a
+separate, unrelated 3.25 MHz clock — don't confuse the two chips'
+clocks when converting timings).
+
+See [PRECAUTIONS.md](PRECAUTIONS.md) section 1 for *why* this matters
+(zx81sd has no ROM at all — a numeric literal like `23675` is never
+valid on zx81sd, it lands inside the program's own compiled code) and
+section 8 for how to safely reference these from a new library file
+(`#require "sysvars.asm"`, never `#include once`).
+
+## Main block (mirrors `zx48k/runtime/sysvars.asm`)
+
+These are the ones the shared zx48k runtime/stdlib code accesses **by
+symbolic name** (`CHARS`, `UDG`, `ATTR_P`...) — since the symbol
+resolves against whichever `sysvars.asm` got linked in, zx81sd's own
+definitions satisfy them automatically, no override file needed.
+
+| Symbol | Spectrum ROM addr | zx81sd addr | Size | Purpose |
+|-------------|:-----------------:|:-----------:|:----:|---------|
+| `CHARS` | 23606 (`$5C36`) | `$8000` | 2B (DW) | Pointer to charset (8×8) |
+| `UDG` | 23675 (`$5C7B`) | `$8002` | 2B (DW) | Pointer to UDG charset |
+| `COORDS` | 23677 (`$5C7D`) | `$8004` | 2B (DW) | Last `PLOT` coordinate (X,Y) |
+| `FLAGS2` | 23681 (`$5C81`) | `$8006` | 1B | Screen flags (OVER/INVERSE/etc.) |
+| `ECHO_E` | 23682 (`$5C82`) | `$8007` | 1B | (reserved, unused by zx81sd) |
+| `DFCC` | 23684 (`$5C84`) | `$8008` | 2B (DW) | Next screen (bitmap) address for `PRINT` |
+| `DFCCL` | 23686 (`$5C86`) | `$800A` | 2B (DW) | Next screen-attribute address for `PRINT` |
+| `S_POSN` | 23688 (`$5C88`) | `$800C` | 2B | Cursor position (H=row, L=col) |
+| `ATTR_P` | 23693 (`$5C8D`) | `$800E` | 1B | Permanent attribute (INK/PAPER/...) |
+| `MASK_P` | 23694 (`$5C8E`) | `$800F` | 1B | Permanent mask (implicit `ATTR_P+1`, not a separate EQU on either side) |
+| `ATTR_T` | 23695 (`$5C8F`) | `$8010` | 1B | Temporary attribute |
+| `MASK_T` | 23696 (`$5C90`) | `$8011` | 1B | Temporary mask (implicit `ATTR_T+1`) |
+| `P_FLAG` | 23697 (`$5C91`) | `$8012` | 1B | Print flags (permanent OVER/INVERSE) |
+| `MEM0`/`MEMBOT` | 23698 (`$5C92`) | `$8013` | 5B | Temp buffer used by ROM char routines. **Same physical Spectrum address as `MEMBOT`** below — the real ROM reuses this scratch for two unrelated purposes depending on context; zx81sd keeps them as two clearly-named, non-overlapping zones instead (`MEM0` here, `ARRAY_SCRATCH` separately) |
+| `TV_FLAG` | 23612 (`$5C3C`) | `$8018` | 1B | Output-to-screen control flags |
+
+Not in the shared file, zx81sd-only additions used by the same
+symbolic-name mechanism:
+
+| Symbol | Nearest Spectrum concept | zx81sd addr | Size | Notes |
+|-------------|--------------------------|:-----------:|:----:|-------|
+| `ERR_NR` | `ERR_NR` — 23610 (`$5C3A`) | `$8019` | 1B | Error code (-1 = no error) |
+| `FRAMES` | `FRAMES` — 23672 (`$5C58`), 3B on real ROM | `$801A` | 2B (DW) | Software VSYNC frame counter — zx81sd has no hardware interrupt to auto-increment it (see `VSYNC_TICK`/`PAUSE`), only needs 16 bits |
+| `RANDOM_SEED_LOW` | `SEED` low word — 23670 (`$5C76`) | `$801C` | 2B (DW) | RNG seed |
+| `SCREEN_ADDR` | not a ROM sysvar — zxbasic's own indirection variable (`zx48k/runtime/sysvars.asm`: `DW 16384`) | `$801E` | 2B (DW) | Framebuffer pointer, init `$C000` (vs `16384` on Spectrum) |
+| `SCREEN_ATTR_ADDR` | ditto (`DW 22528`) | `$8020` | 2B (DW) | Attribute pointer, init `$D800` (vs `22528`) |
+
+## Floating-point calculator (`fp_calc.asm`)
+
+Real Spectrum ROM sysvars, but zx81sd points them at a **fixed**
+buffer instead of the ROM's dynamically-growing area (there's no
+"free memory between program and stack" concept here):
+
+| Symbol | Spectrum ROM addr | zx81sd addr | Size | Notes |
+|---------------|:-------------------:|:-----------:|:----:|-------|
+| `FP_STKBOT` | `STKBOT` — 23651 (`$5C63`) | `$8022` | 2B (DW) | Base of the FP number stack |
+| `FP_STKEND` | `STKEND` — 23653 (`$5C65`) | `$8024` | 2B (DW) | Next free slot in the FP stack |
+| `FP_BREG` | `BREG` — 23655 (`$5C67`) | `$8026` | 1B | Literal currently being parsed |
+| `FP_MEM` | `MEM` — 23656 (`$5C68`) | `$8027` | 2B (DW) | Pointer to the MEM area (6×5B cells) |
+| `FP_CALC_STACK` | n/a (dynamic on real ROM) | `$8029` | 60B | Fixed FP number stack (12 numbers max) |
+| `FP_MEM_AREA` | n/a (dynamic on real ROM) | `$8065` | 30B | Fixed MEM area (6×5B cells) |
+
+`FP_BREG` **must** sit immediately after `FP_STKEND` — the CALCULATE
+engine (`ENT-TABLE`/`fp-calc-2`/`dec-jr-nz`) exploits the real ROM's
+memory contiguity (`STKEND_hi` immediately followed by `BREG`) to load
+both with a single `LD BC,(FP_STKEND+1)`. Don't reorder these two.
+
+## Hardcoded-address scratch fixes (not exposed by symbolic name)
+
+These shared `zx48k/runtime/` files bypass `sysvars.asm` entirely and
+hardcode a raw Spectrum address as scratch storage — on zx81sd that
+address lands inside the program's own compiled code (block 2,
+`$4000-$5FFF`) instead of safe RAM, silently corrupting it. Each one
+needed its own zx81sd **override file** (only the address changed, see
+[MAP.md](MAP.md) for the full incident writeups) rather than a plain
+`sysvars.asm` entry, since the shared code never references them by a
+name `sysvars.asm` could satisfy:
+
+| Symbol (shared file's own name) | Spectrum addr | zx81sd override | zx81sd addr |
+|----------------------------------|:-------------:|------------------|:-----------:|
+| `LBOUND_PTR`/`UBOUND_PTR`/`RET_ADDR`/`TMP_ARR_PTR` (`array.asm`, aliases `MEMBOT`) | 23698 (`$5C92`) | `runtime/array/array.asm` | `ARRAY_SCRATCH` = `$8083` (8B) |
+| `TMP` (`chr.asm`, alias `DEST`) | 23629 (`$5C4D`) | `runtime/chr.asm` | `CHR_SCRATCH` = `$808B` (2B) |
+| `TMP`/`ERR_SP` (`arith/divf.asm`, aliases `DEST`/`ERR_SP`) | 23629 (`$5C4D`) / 23613 (`$5C3D`) | `runtime/arith/divf.asm` | `DIVF_SCRATCH` = `$808D` (4B: TMP 2B + ERR_SP 2B) |
+
+Total sysvar block size: `$91` bytes (`$8000`-`$8090`), well before the
+heap start at `$8100`.
+
+**Not yet audited / no override exists** (same antipattern, not yet
+observed corrupting a real program — see `oilpanic-portability.md`
+project memory and `MAP.md` for the hunting method if one of these
+ever bites):
+
+| Symbol | Spectrum addr | Shared file | Only triggered by |
+|--------|:-------------:|-------------|--------------------|
+| `TEMP` (alias `MEMBOT`) | 23698 (`$5C92`) | `runtime/arith/modf16.asm` | the `MOD` operator |
+
+A quick grep for the pattern across all of `zx48k/runtime`:
+`EQU 2[34][0-9][0-9][0-9]` — anything that turns up and *doesn't*
+already have a zx81sd override (see next section) is a candidate.
+
+## Shared files with hardcoded Spectrum addresses that zx81sd already
+## bypasses entirely (full rewrite, not just an address patch)
+
+These also matched the grep above, but zx81sd ships a **complete**
+override with no numeric Spectrum address left at all (not just a
+scratch-address swap) — listed here so they're not mistaken for
+unaudited risks:
+
+| Shared file | Hardcoded Spectrum addr(s) | zx81sd override |
+|-------------|------------------------------|------------------|
+| `runtime/break.asm` (`PPC`) | 23621 (`$5C45`) | `zx81sd/runtime/break.asm` |
+| `runtime/load.asm` (`BREG` alias) | 23655 (`$5C67`) | `zx81sd/runtime/load.asm` |
+| `runtime/save.asm` (`MEMBOT` alias) | 23698 (`$5C92`) | `zx81sd/runtime/save.asm` |
+| `runtime/val.asm` (`STKBOT`/`ERR_SP`/`CH_ADD`) | 23651/23613/23645 | `zx81sd/runtime/val.asm` |
+| `runtime/random.asm` (`RANDOM_SEED_LOW`) | 23670 (`$5C76`) | `zx81sd/runtime/random.asm` |
diff --git a/src/arch/zx81sd/doc/USAGE.md b/src/arch/zx81sd/doc/USAGE.md
new file mode 100644
index 000000000..a7c33137d
--- /dev/null
+++ b/src/arch/zx81sd/doc/USAGE.md
@@ -0,0 +1,133 @@
+# Usage: compiling, packaging and loading a program on zx81sd
+
+## 1. Compile
+
+From the root of this repository:
+
+```
+python -m src.zxbc.zxbc --arch zx81sd -o -M
+```
+
+- `--arch zx81sd` selects this architecture's backend and overrides
+ (see [../README.md](../README.md) — the port's golden rule).
+- `-M ` is optional but strongly recommended: it generates
+ the symbol map (address of every ASM/BASIC label), essential for
+ debugging with an emulator or simulating the binary (see
+ [MAP.md](MAP.md) for examples of Python simulation harnesses).
+- Examples transcribed from classic Sinclair BASIC (1-based
+ arrays/strings) usually also need `--string-base 1 --array-base 1`
+ (see [BASIC_CHANGES.md](BASIC_CHANGES.md), `comecoquitos.bas` example).
+
+There's also `python zxbc.py --arch zx81sd -f bin -o `
+(the traditional entry-point script); both forms are equivalent.
+
+## 2. Package for the ZX81
+
+The binary produced by the compiler is a flat image of up to 48KB
+(blocks 0-5 of the SD81 mapper). The ZX81 can only load, in one go,
+whatever fits in its own visible RAM, so that binary has to be split
+into 8KB pages and a BASIC loader generated that feeds them one by one
+into the SD81 Booster card, remapping the memory mapper between pages.
+
+That's what [`../tools/split_sd81.py`](../tools/split_sd81.py) does:
+
+```
+python src/arch/zx81sd/tools/split_sd81.py [PREFIX]
+```
+
+- `PREFIX` (optional) is the base name for the output files, in the
+ ZX81 charset (letters, digits, no underscore). If omitted, it's
+ derived from the input `.bin` file name.
+- It generates:
+ - `P8.BIN`, `P9.BIN`, ... — one per 8KB of the binary
+ (SD81 page 8 = block 0, page 9 = block 1, etc.)
+ - `_loader.txt` — the loader's BASIC listing in plain text
+ (for reading/debugging).
+ - `.P` — the same loader, already tokenized, ready to load
+ and run on the ZX81 from the SD card.
+
+The generated loader uses `LOAD THEN CLEAR`, `LOAD *MAP` and
+`LOAD FAST ... CODE`, extensions provided by the SD81 Booster firmware
+(they don't exist in the original ZX81 ROM). It does the following, in
+order:
+
+1. Reserves memory (`CLEAR`) and loads `BOOT1.BIN` (the stage 1 loader,
+ fixed for every program — source in
+ [`../tools/boot1.asm`](../tools/boot1.asm), already-assembled binary
+ in [`../tools/boot1.bin`](../tools/boot1.bin)).
+2. For each page of the binary: maps block 7 to that physical page
+ (`LOAD *MAP 7,`) and dumps the data there (`LOAD FAST ... CODE
+ 57344`, block 7's window at `$E000`).
+3. When done, leaves the mapper in "full page" mode (`LOAD *MAP
+ 7,63`) — from this point on the mapper doesn't go back to simple
+ mode until the next reset — and jumps to `BOOT1.BIN` (`RAND USR
+ 24576`), which does the final mapping of blocks 0-5 and starts the
+ program.
+
+## 3. Copy to the SD card
+
+Copy to the SD card, alongside the rest of your collection:
+
+- `BOOT1.BIN` (only once, it's the same for every program)
+- All the `P.BIN` files for the program
+- `.P`
+
+Then on the ZX81 (or in EightyOne pointing at the SD image): load and
+run it with `LOAD FAST ""` — no need to select anything
+afterwards, it runs straight from there.
+
+## 3b. Steps 1+2 in one command: `build_sd81.py`
+
+[`../tools/build_sd81.py`](../tools/build_sd81.py) chains the compile
+(step 1, including `-M`) and the packaging (step 2) into a single
+call, and can optionally copy the result straight to an emulator's
+virtual SD folder:
+
+```
+python src/arch/zx81sd/tools/build_sd81.py [PREFIX] [--copy-to DIR]
+```
+
+`__ZX81SD__` is always passed as `-D` (needed for game sources that
+guard architecture-specific code with `#ifdef __ZX81SD__`, see
+[BASIC_CHANGES.md](BASIC_CHANGES.md)); extra `-D MACRO` or unrecognized
+flags are forwarded to `zxbc.py` as-is. `--copy-to` doesn't copy
+`BOOT1.BIN` (shared across every program, usually already in place).
+
+A stale package the emulator doesn't actually see was itself the cause
+of a multi-hour false-alarm debugging session once (see MAP.md /
+`oilpanic-portability` project memory) — `--copy-to` exists mainly to
+make that mistake harder to repeat.
+
+## 3c. Regenerating just the `.map`, via the `.asm` stage: `gen_map.py`
+
+Sometimes the `.asm` intermediate is worth keeping around for
+inspection or hand-editing, and only a fresh, matching `.map` is
+needed afterwards — without paying for `split_sd81.py`'s page-split +
+`.P`-tokenize step again. [`../tools/gen_map.py`](../tools/gen_map.py)
+does the two-stage version of step 1 explicitly:
+
+```
+python src/arch/zx81sd/tools/gen_map.py [output_base]
+```
+
+1. `zxbc.py --arch=zx81sd -D __ZX81SD__ -f asm` (BASIC → `.asm`)
+2. `zxbasm.py -M` (`.asm` → `.bin` + `.map`; a `.bin` comes out too,
+ zxbasm always needs some output format)
+
+Note: `zxbasm.py`, unlike `zxbc.py`, has no `--arch`/include-path flag
+of its own, so it can't resolve `charset.asm`'s bare
+`INCBIN "specfont.bin"` through the normal architecture search path —
+`gen_map.py` works around this by temporarily copying `specfont.bin`
+next to the generated `.asm` before invoking `zxbasm.py`, then
+removing it again.
+
+## 4. Debugging without hardware: simulation with Python
+
+To diagnose hangs, HALTs or incorrect results without having to test on
+the emulator or real hardware every time, this port's development has
+used Python's `z80` package (`pip install z80`) to simulate the flat
+binary directly. The full methodology (including one important trap:
+the simulator's RAM starts at zero, which can hide uninitialized-memory
+bugs that do show up on real hardware) is documented in
+[MAP.md](MAP.md) — the "Heap at $8100 + EightyOne tape traps" section
+and the methodology notes on the MSFS/maskedsprites bug.
diff --git a/src/arch/zx81sd/tools/boot1.asm b/src/arch/zx81sd/tools/boot1.asm
new file mode 100644
index 000000000..6ed9ea28c
--- /dev/null
+++ b/src/arch/zx81sd/tools/boot1.asm
@@ -0,0 +1,104 @@
+; ===========================================================================
+; bootstrap_stage1.asm — Stage 1 Bootstrap ZX81 + SD81 Booster
+;
+; Resides at $6000 (block 3). The BASIC loader sends it there before
+; anything else because it's a neutral area: the ZX81's BASIC doesn't
+; need it, and no block has been remapped yet.
+;
+; Startup sequence (see ZX81-SD81-Adaptation-Plan.md):
+; 1. DI — disable interrupts (ZX81 FAST mode already does this, but
+; it's repeated as a precaution before touching paging)
+; 2. Activate Superfast HiRes Spectrum mode via the FPGA (POKE 2045 = $07FD)
+; 3. Activate Chroma81 colour mode (port $7FEF) -- required separately
+; from step 2 on real hardware, see note below
+; 4. Disable memory-mapped IO (POKE 2056 = $0808)
+; 5. Map block 0 -> page 8 (OUT ($E7), page=8, block=0)
+; Stage 2 ($0100-$0FFF on page 8) is now ready to run.
+; 6. JP $0100 — jumps to stage 2 in the freshly mapped RAM
+;
+; Notes:
+; - HFILE=$C000 is the FPGA's default value when activating mode 172.
+; If your hardware requires setting it explicitly, uncomment the
+; HFILE block at the end.
+; - Chroma81 colour (step 3): activating Spectrum mode (step 2) does
+; NOT turn color on by itself on real hardware -- confirmed on a
+; real SD81 Booster (2026-07-05); the EightyOne emulator shows
+; color regardless of this step, which is a known emulator bug
+; (color should also require this OUT, to match real hardware).
+; - Stage 1 does NOT initialize SP; stage 2 does (ld sp, $7FFF).
+; - Blocks 1-5 are mapped in stage 2 (already running from page 8).
+;
+; Compile:
+; zxbasm bootstrap_stage1.asm -o bootstrap_stage1.bin
+; or with pasmo:
+; pasmo --bin bootstrap_stage1.asm bootstrap_stage1.bin
+;
+; Load on the emulator / hardware at address $6000 (24576 decimal).
+; Run from BASIC with:
+; RANDOMIZE USR 24576
+; ===========================================================================
+
+ org $6000
+
+SD81_STAGE1:
+
+ di ; Interrupts disabled
+
+ ; --------------------------------------------------------------------------
+ ; Set the framebuffer address (HFILE)
+ ; high=2044 low=(2043)
+ ;
+
+ ld hl, $C000
+ ld ($07FB), hl
+
+ ; --------------------------------------------------------------------------
+ ; ------------------------------------------------------------------
+ ; Activate Superfast HiRes Spectrum mode
+ ; POKE 2045, 172 -> ld a, 172 / ld ($07FD), a
+ ; The SD81 Booster's FPGA interprets this as:
+ ; mode 172 ($AC) = Spectrum 256x192 from HFILE=$C000
+ ; ------------------------------------------------------------------
+ ld a, 172
+ ld ($07FD), a
+
+ ; ------------------------------------------------------------------
+ ; Activate Chroma81 colour mode
+ ; Activating Spectrum mode above does NOT turn color on by itself
+ ; on real hardware (confirmed on real SD81 Booster; the EightyOne
+ ; emulator shows color regardless, which is an emulator bug to
+ ; report/fix there). Chroma81 is a separate port ($7FEF) that must
+ ; be set explicitly.
+ ; ------------------------------------------------------------------
+ ld bc, $7FEF ; Chroma81 port: set known state
+ ld a, 39 ; bit5=1 color on, bit4=0 char-code mode
+ out (c), a
+
+ ; ------------------------------------------------------------------
+ ; Disable memory-mapped IO
+ ; POKE 2056, 0 -> ld ($0808), a
+ ; Avoids collisions between IN/OUT instructions and the RAM space
+ ; ------------------------------------------------------------------
+ xor a
+ ld ($0808), a
+
+ ; ------------------------------------------------------------------
+ ; Map block 0 ($0000-$1FFF) -> SD81 page 8
+ ; Port $E7: full 64-page mode, OUT (C), A with B=page, A=block
+ ; Page 8 holds the compiled binary (vectors + stage 2 + runtime)
+ ; ------------------------------------------------------------------
+ ld b, 8 ; destination SD81 page (8 = first user page)
+ ld a, 0 ; Z80 block to remap (block 0 = $0000-$1FFF)
+ ld c, $E7 ; SD81 paging port
+ out (c), a ; map (B=page, A=block)
+
+ ; ------------------------------------------------------------------
+ ; Jump to stage 2 in the freshly mapped RAM
+ ; From here on, block 0 holds page 8:
+ ; $0000-$0067 RST vectors
+ ; $0100 __START_PROGRAM (stage 2's entry point)
+ ; ------------------------------------------------------------------
+ jp $0100
+
+
+ end SD81_STAGE1
diff --git a/src/arch/zx81sd/tools/boot1.bin b/src/arch/zx81sd/tools/boot1.bin
new file mode 100644
index 000000000..bbaa78a56
Binary files /dev/null and b/src/arch/zx81sd/tools/boot1.bin differ
diff --git a/src/arch/zx81sd/tools/build_sd81.py b/src/arch/zx81sd/tools/build_sd81.py
new file mode 100755
index 000000000..8ce978cb1
--- /dev/null
+++ b/src/arch/zx81sd/tools/build_sd81.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python3
+"""
+build_sd81.py — one-shot build: BASIC source -> zx81sd binary -> SD81
+Booster package (page files + tokenized .P loader), in a single command.
+
+Chains the two steps that had to be run by hand throughout zx81sd
+development:
+ 1. zxbc.py --arch=zx81sd -D __ZX81SD__ -o .bin -M .map
+ (compiles the BASIC source; -M keeps the symbol map in sync with
+ the binary — a stale/missing .map was a repeated source of
+ confusion when live-debugging on real hardware/emulator)
+ 2. split_sd81.py .bin (splits into 8KB SD81 pages and
+ generates .P, the tokenized BASIC loader)
+
+Usage:
+ python build_sd81.py [output_base] [--copy-to DIR] [--asm]
+ [-O LEVEL] [-D MACRO ...] [zxbc.py extra args...]
+
+ output_base defaults to the source file's name (without extension),
+ uppercased — same convention split_sd81.py already enforces (must be
+ representable in the ZX81 charset: letters/digits, no underscore).
+
+ --copy-to DIR: after building, also copy the page files, the .P
+ loader, and boot1.bin (from this tools/ folder) to DIR (e.g. an
+ emulator's virtual-SD test folder).
+
+ --asm: also emit .asm, the generated assembly listing (a second
+ zxbc.py pass with -f asm, same source/optimize/defines). Off by
+ default (it's a much bigger file than the .map and rarely needed) —
+ turn it on when tracing a bug live against the .map/disassembly, to
+ see the actual instructions around a given address instead of just
+ its label. The .map (symbol addresses) is still always generated,
+ unconditionally, as before.
+
+ __ZX81SD__ is always defined; pass more -D flags to add extras.
+ Any unrecognized argument is forwarded to zxbc.py as-is (e.g. to
+ tweak heap size, warnings, etc.).
+
+Example:
+ python build_sd81.py C:/ClaudeCode/ZXOilPanic/oilpanic.bas OILPANIC ^
+ --copy-to C:/ClaudeCode/Eightyone2/EightyOne/SD81/test
+"""
+
+import argparse
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+TOOLS_DIR = Path(__file__).resolve().parent
+ZXBASIC_ROOT = TOOLS_DIR.parents[3] # tools -> zx81sd -> arch -> src ->
+ZXBC = ZXBASIC_ROOT / "zxbc.py"
+SPLIT_SCRIPT = TOOLS_DIR / "split_sd81.py"
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("source", help="BASIC source file (.bas)")
+ ap.add_argument("output_base", nargs="?", help="Output base name (default: source file name)")
+ ap.add_argument("--copy-to", metavar="DIR", help="Also copy the page files + .P loader here")
+ ap.add_argument("--asm", action="store_true", help="Also emit .asm (the generated assembly listing)")
+ ap.add_argument("-O", "--optimize", default="2", help="zxbc.py optimization level (default 2)")
+ ap.add_argument(
+ "-D",
+ "--define",
+ action="append",
+ default=[],
+ metavar="MACRO",
+ help="Extra -D macro for zxbc.py (repeatable). __ZX81SD__ is always defined.",
+ )
+ args, extra = ap.parse_known_args()
+
+ if not ZXBC.is_file():
+ sys.exit(f"Error: zxbc.py not found at {ZXBC} (unexpected repo layout)")
+ if not SPLIT_SCRIPT.is_file():
+ sys.exit(f"Error: split_sd81.py not found at {SPLIT_SCRIPT}")
+
+ source = Path(args.source).resolve()
+ if not source.is_file():
+ sys.exit(f"Error: source file not found: {source}")
+
+ base = args.output_base or source.stem
+ workdir = source.parent
+ bin_path = workdir / f"{base}.bin"
+ map_path = workdir / f"{base}.map"
+
+ zxbc_cmd = [
+ sys.executable,
+ str(ZXBC),
+ str(source),
+ "--arch=zx81sd",
+ "-O",
+ args.optimize,
+ "-o",
+ str(bin_path),
+ "-M",
+ str(map_path),
+ ]
+ for d in ["__ZX81SD__"] + args.define:
+ zxbc_cmd += ["-D", d]
+ zxbc_cmd += extra
+
+ print("== zxbc.py ==")
+ print(" ".join(zxbc_cmd))
+ subprocess.run(zxbc_cmd, check=True)
+
+ if args.asm:
+ asm_path = workdir / f"{base}.asm"
+ asm_cmd = [
+ sys.executable,
+ str(ZXBC),
+ str(source),
+ "--arch=zx81sd",
+ "-O",
+ args.optimize,
+ "-f",
+ "asm",
+ "-o",
+ str(asm_path),
+ ]
+ for d in ["__ZX81SD__"] + args.define:
+ asm_cmd += ["-D", d]
+ asm_cmd += extra
+
+ print()
+ print("== zxbc.py (asm) ==")
+ print(" ".join(asm_cmd))
+ subprocess.run(asm_cmd, check=True)
+
+ print()
+ print("== split_sd81.py ==")
+ split_cmd = [sys.executable, str(SPLIT_SCRIPT), str(bin_path), base]
+ print(" ".join(split_cmd))
+ subprocess.run(split_cmd, check=True, cwd=workdir)
+
+ if args.copy_to:
+ dest = Path(args.copy_to)
+ dest.mkdir(parents=True, exist_ok=True)
+ base_upper = base.upper()
+ copied = []
+ for pattern in (f"{base_upper}P*.BIN", f"{base_upper}.P"):
+ for f in workdir.glob(pattern):
+ shutil.copy2(f, dest / f.name)
+ copied.append(f.name)
+
+ boot1 = TOOLS_DIR / "boot1.bin"
+ if boot1.is_file():
+ shutil.copy2(boot1, dest / boot1.name)
+ copied.append(boot1.name)
+ else:
+ print(f" Warning: boot1.bin not found at {boot1}")
+
+ print()
+ print(f"== copied to {dest} ==")
+ if copied:
+ for name in sorted(copied):
+ print(f" {name}")
+ else:
+ print(f" Warning: nothing matched {base_upper}P*.BIN / {base_upper}.P in {workdir}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/arch/zx81sd/tools/gen_map.py b/src/arch/zx81sd/tools/gen_map.py
new file mode 100755
index 000000000..07158114a
--- /dev/null
+++ b/src/arch/zx81sd/tools/gen_map.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+"""
+gen_map.py — regenerate a zx81sd .map symbol file from BASIC source via
+the intermediate .asm stage, instead of compiling straight to .bin.
+
+Two-stage pipeline (this is what zxbc.py does internally in one step
+when given -f bin -M map; this script exposes the split explicitly):
+ 1. zxbc.py --arch=zx81sd -D __ZX81SD__ -f asm -o .asm
+ (BASIC -> assembly, kept around for inspection/hand-editing)
+ 2. zxbasm.py -M .map -o .bin .asm
+ (assembly -> binary + .map; zxbasm always needs *some* output
+ format, so a .bin comes out too as a byproduct)
+
+Useful when the .asm is being hand-inspected or edited between steps
+(a common retro workflow) and only a fresh, consistent .map is needed
+to match it — skips split_sd81.py's page-splitting / .P-tokenizing
+step entirely, since a plain zxbc.py -f bin -M compile already covers
+the common "just rebuild everything" case (see build_sd81.py).
+
+Usage:
+ python gen_map.py [output_base] [-O LEVEL] [-D MACRO ...]
+ [--keep-asm] [zxbc.py extra args...]
+
+ output_base defaults to the source file's name (without extension).
+ --keep-asm: don't delete the intermediate .asm afterwards (kept by
+ default anyway — flag exists only for symmetry/clarity, see below).
+
+Example:
+ python gen_map.py C:/ClaudeCode/ZXOilPanic/oilpanic.bas oilpanic
+"""
+
+import argparse
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+TOOLS_DIR = Path(__file__).resolve().parent
+ZXBASIC_ROOT = TOOLS_DIR.parents[3] # tools -> zx81sd -> arch -> src ->
+ZXBC = ZXBASIC_ROOT / "zxbc.py"
+ZXBASM = ZXBASIC_ROOT / "zxbasm.py"
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("source", help="BASIC source file (.bas)")
+ ap.add_argument("output_base", nargs="?", help="Output base name (default: source file name)")
+ ap.add_argument("-O", "--optimize", default="2", help="zxbc.py optimization level (default 2)")
+ ap.add_argument(
+ "-D",
+ "--define",
+ action="append",
+ default=[],
+ metavar="MACRO",
+ help="Extra -D macro for zxbc.py (repeatable). __ZX81SD__ is always defined.",
+ )
+ ap.add_argument(
+ "--keep-asm",
+ action="store_true",
+ help="No-op (the .asm is always kept) — accepted for clarity/symmetry only.",
+ )
+ args, extra = ap.parse_known_args()
+
+ if not ZXBC.is_file():
+ sys.exit(f"Error: zxbc.py not found at {ZXBC} (unexpected repo layout)")
+ if not ZXBASM.is_file():
+ sys.exit(f"Error: zxbasm.py not found at {ZXBASM} (unexpected repo layout)")
+
+ source = Path(args.source).resolve()
+ if not source.is_file():
+ sys.exit(f"Error: source file not found: {source}")
+
+ base = args.output_base or source.stem
+ workdir = source.parent
+ asm_path = workdir / f"{base}.asm"
+ bin_path = workdir / f"{base}.bin"
+ map_path = workdir / f"{base}.map"
+
+ zxbc_cmd = [
+ sys.executable,
+ str(ZXBC),
+ str(source),
+ "--arch=zx81sd",
+ "-O",
+ args.optimize,
+ "-f",
+ "asm",
+ "-o",
+ str(asm_path),
+ ]
+ for d in ["__ZX81SD__"] + args.define:
+ zxbc_cmd += ["-D", d]
+ zxbc_cmd += extra
+
+ print("== zxbc.py (BASIC -> asm) ==")
+ print(" ".join(zxbc_cmd))
+ subprocess.run(zxbc_cmd, check=True)
+
+ zxbasm_cmd = [
+ sys.executable,
+ str(ZXBASM),
+ str(asm_path),
+ "-o",
+ str(bin_path),
+ "-M",
+ str(map_path),
+ ]
+
+ # zxbasm has no --arch/--include-path flag at all (unlike zxbc.py), so
+ # it never learns zx81sd's architecture-specific include search path.
+ # A bare INCBIN filename (e.g. "specfont.bin", pulled in by
+ # charset.asm) is resolved with local_first=True against the .asm
+ # file's OWN directory — so temporarily drop a copy of specfont.bin
+ # next to the generated .asm and remove it again afterwards.
+ specfont_src = ZXBASIC_ROOT / "src" / "lib" / "arch" / "zx81sd" / "runtime" / "specfont.bin"
+ specfont_dst = workdir / "specfont.bin"
+ specfont_copied = False
+ if specfont_src.is_file() and not specfont_dst.exists():
+ shutil.copy2(specfont_src, specfont_dst)
+ specfont_copied = True
+
+ print()
+ print("== zxbasm.py (asm -> bin + map) ==")
+ print(" ".join(zxbasm_cmd))
+ try:
+ subprocess.run(zxbasm_cmd, check=True)
+ finally:
+ if specfont_copied:
+ specfont_dst.unlink(missing_ok=True)
+
+ print()
+ print(f"Generated: {asm_path}")
+ print(f"Generated: {bin_path}")
+ print(f"Generated: {map_path}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/arch/zx81sd/tools/split_sd81.py b/src/arch/zx81sd/tools/split_sd81.py
new file mode 100755
index 000000000..c478d7f4e
--- /dev/null
+++ b/src/arch/zx81sd/tools/split_sd81.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+"""
+split_sd81.py — ZX81+SD81 Booster binary splitter + loader generator
+
+Splits the flat binary generated by zxbc.py (--arch zx81sd, -f bin)
+into 8KB pages for loading from the ZX81 BASIC loader, and generates
+the BASIC listing (plain text, not tokenized) needed to load them.
+
+Page map:
+ Page 8 -> block 0 ($0000-$1FFF): vectors + stage2 + runtime + user code
+ Page 9 -> block 1 ($2000-$3FFF): continuation if the binary exceeds 8KB
+ Page 10 -> block 2 ($4000-$5FFF): same
+ Page 11 -> block 3 ($6000-$7FFF): same (external stage 1 resides here before the jump)
+ Page 12 -> block 4 ($8000-$9FFF): sysvars + heap (data, not executable without MC45)
+ Page 13 -> block 5 ($A000-$BFFF): heap continuation
+ Page 15 -> block 7 ($E000-$FFFF): data banking (large static tables placed
+ here via explicit ORG, out of the code/sysvar/heap budget)
+
+Any page that comes out 100% zero bytes is the assembler's own gap filler
+(zxbasm zero-fills address ranges nothing was ever written to — see
+src/zxbasm/memory.py), not real content: typically the unused span between
+the end of code and a data table deliberately ORG'd into block 7. Sysvars
+and heap are always freshly initialized at runtime, so this is safe to
+skip entirely — the page is neither packaged nor loaded (see split()).
+
+The binary starts at $0000 and includes no .tap/.tzx header. Each
+output file is named P.BIN (uppercase), where N is the page.
+
+The generated BASIC loader (plain text, not yet tokenized to .p)
+follows the sequence manually validated on hardware/emulator:
+
+ 2 FAST
+ 5 LOAD THEN CLEAR 24575
+ 10 LOAD FAST "BOOT1.BIN"CODE 24576
+ 20 LOAD *MAP 7,8
+ 25 LOAD FAST "P8.BIN"CODE 57344
+ 30 LOAD *MAP 7,9
+ 35 LOAD FAST "P9.BIN"CODE 57344
+ ...
+ LOAD *MAP 7,63
+ RAND USR 24576
+
+Note: "LOAD THEN CLEAR", "LOAD *MAP" and "LOAD FAST ... CODE" are
+extensions of the SD81 Booster firmware over the ZX81's BASIC (they
+don't exist in the original ROM). Block 7 ($E000/57344) is used as a
+temporary paging window to dump each physical page before BOOT1.BIN
+does the final mapping of blocks 0-5 through port $E7. The final
+"LOAD *MAP 7,63" forces the switch to full-paging mode (64 pages) and
+leaves block 7 in a neutral state before jumping to the program.
+
+Besides the plain-text listing, this also generates the real tokenized
+.p file (ready to load/run) using zx81_p_loader.py, a Python port of
+EightyOne's tokenizer (zx81BasicLoader.cpp / IBasicLoader.cpp).
+
+To load and run the resulting package on the ZX81 (or in EightyOne
+pointing at the SD image): `LOAD FAST ""` — nothing else needs
+selecting afterwards, it runs straight from there.
+
+Usage:
+ python split_sd81.py [output_base]
+
+ If output_base isn't given, the input's name without extension is
+ used.
+
+Example:
+ python split_sd81.py test_sd81.bin
+ -> TEST_SD81P8.BIN (first page, always present)
+ -> TEST_SD81P9.BIN (only if the binary exceeds 8 KB)
+ -> ...
+ -> test_sd81_loader.txt (loader's BASIC listing, plain text)
+ -> TEST_SD81.P (the same listing, tokenized)
+"""
+
+import os
+import sys
+
+from zx81_p_loader import build_p_file
+
+PAGE_SIZE = 8192 # 8KB per page
+FIRST_PAGE = 8 # SD81 page assigned to block 0
+CLEANUP_PAGE = 63 # neutral page: forces full-paging mode (64 pages)
+BOOT1_FILE = "BOOT1.BIN"
+BOOT1_ADDR = 24576 # $6000
+CLEAR_ADDR = 24575 # protects BOOT1.BIN (reserved right below it)
+LOAD_WINDOW_ADDR = 57344 # $E000, block 7's window
+
+
+def split(input_path: str, output_base: str) -> list[tuple[int, str]]:
+ with open(input_path, "rb") as f:
+ data = f.read()
+
+ if not data:
+ sys.exit(f"Error: {input_path} is empty")
+
+ pages_written: list[tuple[int, str]] = []
+ page_num = FIRST_PAGE
+ offset = 0
+
+ while offset < len(data):
+ raw_chunk = data[offset : offset + PAGE_SIZE]
+
+ # A full (unpadded) page that's 100% zero bytes is the assembler's own
+ # gap filler (zxbasm's MEMORY.dump() zero-fills any address range
+ # nothing was ever written to — see src/zxbasm/memory.py), not real
+ # program content. This shows up when a data table is deliberately
+ # ORG'd far away from the code (e.g. into block 7, $E000+, to keep
+ # it out of the code/sysvar/heap budget below $8000): everything in
+ # between is a zero-filled gap that never needs to reach the SD card,
+ # since sysvars/heap are freshly initialized at runtime anyway and
+ # nothing reads that memory before writing it first. Skip packaging
+ # and loading it — the runtime page-mapping (OUT to port $E7) for
+ # that block is unaffected, it just won't have been pre-loaded with
+ # this particular (irrelevant) content.
+ if len(raw_chunk) == PAGE_SIZE and raw_chunk.count(0) == PAGE_SIZE:
+ print(
+ f" Page {page_num} (block {page_num - FIRST_PAGE}): "
+ f"[{offset:#06x} - {offset + PAGE_SIZE - 1:#06x}] "
+ "all zero filler, skipped (not packaged/loaded)"
+ )
+ offset += PAGE_SIZE
+ page_num += 1
+ continue
+
+ chunk = raw_chunk
+ # Pad up to PAGE_SIZE with 0xFF (undefined value for unwritten FLASH/RAM)
+ if len(chunk) < PAGE_SIZE:
+ chunk = chunk + b"\xff" * (PAGE_SIZE - len(chunk))
+
+ out_path = f"{output_base}P{page_num}.BIN"
+ with open(out_path, "wb") as f:
+ f.write(chunk)
+
+ pages_written.append((page_num, out_path))
+ print(
+ f" Page {page_num} (block {page_num - FIRST_PAGE}): "
+ f"{out_path} "
+ f"[{offset:#06x} - {min(offset + PAGE_SIZE - 1, len(data) - 1):#06x}]"
+ )
+
+ offset += PAGE_SIZE
+ page_num += 1
+
+ return pages_written
+
+
+def generate_loader_lines(pages: list[tuple[int, str]]) -> list[tuple[int, str]]:
+ lines = []
+ lines.append((2, "FAST"))
+ lines.append((5, f"LOAD THEN CLEAR {CLEAR_ADDR}"))
+ lines.append((10, f'LOAD FAST "{BOOT1_FILE}"CODE {BOOT1_ADDR}'))
+
+ line_no = 20
+ for page_num, page_file in pages:
+ lines.append((line_no, f"LOAD *MAP 7,{page_num}"))
+ lines.append((line_no + 5, f'LOAD FAST "{page_file}"CODE {LOAD_WINDOW_ADDR}'))
+ line_no += 10
+
+ lines.append((line_no, f"LOAD *MAP 7,{CLEANUP_PAGE}"))
+ lines.append((line_no + 10, f"RAND USR {BOOT1_ADDR}"))
+
+ return lines
+
+
+def generate_loader_text(lines: list[tuple[int, str]]) -> str:
+ return "\n".join(f"{n} {cmd}" for n, cmd in lines) + "\n"
+
+
+def main():
+ if len(sys.argv) < 2:
+ print(__doc__)
+ sys.exit(1)
+
+ input_path = sys.argv[1]
+ output_base = sys.argv[2] if len(sys.argv) > 2 else os.path.splitext(input_path)[0]
+ output_base = os.path.basename(output_base).upper()
+
+ # Page and loader.p file names are written in the ZX81 charset
+ # (letters, digits and a few symbols: no underscore or other
+ # non-representable characters). Fail early and clearly instead of
+ # blowing up later while tokenizing the loader.
+ from zx81_p_loader import ascii_to_zx
+
+ for c in output_base:
+ try:
+ ascii_to_zx(c)
+ except ValueError:
+ sys.exit(
+ f"Error: base name {output_base!r} contains {c!r}, "
+ "not representable in the ZX81 charset (avoid '_' and similar)."
+ )
+
+ if not os.path.isfile(input_path):
+ sys.exit(f"Error: {input_path!r} not found")
+
+ size = os.path.getsize(input_path)
+ num_pages = (size + PAGE_SIZE - 1) // PAGE_SIZE
+ print(f"Binary: {input_path} ({size} bytes, {size / 1024:.1f} KB, {num_pages} page(s))")
+ print(f"Splitting into {PAGE_SIZE}-byte pages (starting SD81 page = {FIRST_PAGE})...")
+ print()
+
+ pages = split(input_path, output_base)
+
+ print()
+ print(f"Generated {len(pages)} file(s).")
+ if len(pages) == 1:
+ print("The program fits in a single page (8 KB).")
+ elif pages:
+ page_numbers = [p for p, _ in pages]
+ print(f"Pages {min(page_numbers)} to {max(page_numbers)} (some may have been skipped as filler).")
+
+ loader_lines = generate_loader_lines(pages)
+ loader_text = generate_loader_text(loader_lines)
+
+ # The .p is a real file name for the ZX81 (charset with no
+ # underscore): derived from output_base (same prefix as the
+ # pages), concatenated with no separator, just like "P8.BIN".
+ base_no_ext = os.path.splitext(input_path)[0]
+ loader_txt_path = f"{base_no_ext}_loader.txt"
+ loader_p_path = f"{output_base}.P"
+
+ with open(loader_txt_path, "w", newline="\n") as f:
+ f.write(loader_text)
+
+ p_data = build_p_file(loader_lines)
+ with open(loader_p_path, "wb") as f:
+ f.write(p_data)
+
+ print()
+ print(f"BASIC loader (plain text): {loader_txt_path}")
+ print("---")
+ print(loader_text, end="")
+ print("---")
+ print(f"Tokenized BASIC loader (.p): {loader_p_path} ({len(p_data)} bytes)")
+ print()
+ print("NOTE: 'LOAD THEN CLEAR', 'LOAD *MAP' and 'LOAD FAST ... CODE' are")
+ print("extensions of the SD81 Booster firmware, encoded with the same token")
+ print("mechanism as standard ZX81 BASIC (see zx81_p_loader.py).")
+ print(f"Copy {BOOT1_FILE}, the page files and {os.path.basename(loader_p_path)}")
+ print("to the SD card.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/arch/zx81sd/tools/zx81_p_loader.py b/src/arch/zx81sd/tools/zx81_p_loader.py
new file mode 100755
index 000000000..5b10bcdb2
--- /dev/null
+++ b/src/arch/zx81sd/tools/zx81_p_loader.py
@@ -0,0 +1,577 @@
+#!/usr/bin/env python3
+"""
+zx81_p_loader.py — .p file (tokenized BASIC) generator for the ZX81
+
+Direct Python port of the tokenization algorithm used by the EightyOne
+emulator (zx81BasicLoader.cpp / IBasicLoader.cpp), reference:
+ C:\\ClaudeCode\\Eightyone2\\src\\zx81\\zx81BasicLoader.cpp
+ C:\\ClaudeCode\\Eightyone2\\src\\BasicLoader\\IBasicLoader.cpp
+
+The SD81 Booster firmware's extended commands (LOAD THEN CLEAR,
+LOAD *MAP, LOAD FAST ... CODE, etc.) need no special handling: even
+though the ZX81 ROM interpreter doesn't accept that syntax, they're
+made up of:
+ - Standard ZX81 tokens (THEN, CLEAR, FAST, CODE, LOAD...) in
+ positions the ROM wouldn't use but that CAN be typed (e.g.
+ SHIFT+3 = THEN).
+ - Standalone words after an asterisk (e.g. *MAP, *VER) which are
+ plain letters, not tokens.
+So it's enough to tokenize the text just like any standard BASIC: the
+generic algorithm itself already produces the correct encoding.
+
+Not implemented (not needed for this project): @label tags, numeric
+blocks [DEC:...]/[HEX:...]/[BIN:...], \\xx escape sequences, graphics
+codes, or "alternate keyword spelling". Documented in IBasicLoader.cpp
+in case they're ever needed.
+"""
+
+import math
+import re
+import struct
+
+BLANK = "\x01" # internal marker: position already consumed
+NEWLINE = 0x76
+NUMBER_MARK = 0x7E # precedes the 5 bytes of an embedded number
+DOUBLE_QUOTE = 0xC0 # escaped quote ("") inside a string
+
+# Standard ZX81 tokens (from zx81BasicLoader::ExtractTokens, without
+# "alternate spelling" variants or ZXpand extensions)
+TOKENS = {
+ 64: "RND",
+ 65: "INKEY$",
+ 66: "PI",
+ 193: "AT ",
+ 194: "TAB ",
+ 196: "CODE ",
+ 197: "VAL ",
+ 198: "LEN ",
+ 199: "SIN ",
+ 200: "COS ",
+ 201: "TAN ",
+ 202: "ASN ",
+ 203: "ACS ",
+ 204: "ATN ",
+ 205: "LN ",
+ 206: "EXP ",
+ 207: "INT ",
+ 208: "SQR ",
+ 209: "SGN ",
+ 210: "ABS ",
+ 211: "PEEK ",
+ 212: "USR ",
+ 213: "STR$ ",
+ 214: "CHR$ ",
+ 215: "NOT ",
+ 216: "**",
+ 217: " OR ",
+ 218: " AND ",
+ 219: "<=",
+ 220: ">=",
+ 221: "<>",
+ 222: " THEN ",
+ 223: " TO ",
+ 224: " STEP ",
+ 225: " LPRINT ",
+ 226: " LLIST ",
+ 227: " STOP ",
+ 228: " SLOW ",
+ 229: " FAST ",
+ 230: " NEW ",
+ 231: " SCROLL ",
+ 232: " CONT ",
+ 233: " DIM ",
+ 234: " REM ",
+ 235: " FOR ",
+ 236: " GOTO ",
+ 237: " GOSUB ",
+ 238: " INPUT ",
+ 239: " LOAD ",
+ 240: " LIST ",
+ 241: " LET ",
+ 242: " PAUSE ",
+ 243: " NEXT ",
+ 244: " POKE ",
+ 245: " PRINT ",
+ 246: " PLOT ",
+ 247: " RUN ",
+ 248: " SAVE ",
+ 249: " RAND ",
+ 250: " IF ",
+ 251: " CLS ",
+ 252: " UNPLOT ",
+ 253: " CLEAR ",
+ 254: " RETURN ",
+ 255: " COPY ",
+}
+
+
+def ascii_to_zx(c: str) -> int:
+ """Port of zx81BasicLoader::AsciiToZX."""
+ if c.isalpha():
+ return (ord(c.upper()) - ord("A")) + 38
+ if c.isdigit():
+ return (ord(c) - ord("0")) + 28
+
+ table = {
+ " ": 0,
+ '"': 11,
+ "#": 12,
+ "$": 13,
+ ":": 14,
+ "?": 15,
+ "(": 16,
+ ")": 17,
+ "-": 22,
+ "+": 21,
+ "*": 23,
+ "/": 24,
+ "=": 20,
+ ">": 18,
+ "<": 19,
+ ";": 25,
+ ",": 26,
+ ".": 27,
+ }
+ if c in table:
+ return table[c]
+
+ raise ValueError(f"Invalid character: {c!r}")
+
+
+def zx81_float(value: float) -> bytes:
+ """Port of zx81BasicLoader::OutputFloatingPointEncoding."""
+ exponent = 0
+ mantissa = 0
+
+ if value != 0:
+ neg = value < 0
+ if neg:
+ value = -value
+
+ exponent = math.floor(1e-12 + (math.log(value) / math.log(2.0)))
+ if exponent < -129 or exponent > 126:
+ raise OverflowError("Number out of range")
+
+ mantissa_val = (value / (2.0**exponent)) - 1
+ mantissa_val *= 0x80000000
+ mantissa = math.floor(mantissa_val)
+
+ exponent += 129
+
+ return bytes(
+ [
+ exponent & 0xFF,
+ (mantissa >> 24) & 0xFF,
+ (mantissa >> 16) & 0xFF,
+ (mantissa >> 8) & 0xFF,
+ mantissa & 0xFF,
+ ]
+ )
+
+
+class _Line:
+ """Working state for a BASIC line (without a line number), mirroring
+ the mLineBuffer / mLineBufferOutput / mLineBufferPopulated buffers.
+
+ All arrays have length self.n + 1: the last slot (index n) is a
+ safety pad for when a token consumes the artificial space added at
+ the end to detect tokens with no closing space in the original
+ text (same as the oversized C++ buffer).
+ """
+
+ def __init__(self, text: str):
+ content = " " + text # ReadLine always prepends a space
+ self.n = len(content)
+ self.buf = list(content) + [BLANK] # +1 safety slot
+ self.out = [BLANK] * (self.n + 1)
+ self.populated = [False] * (self.n + 1)
+ # BlankLineStart: no embedded line number, leaves 1 real space
+ self.buf[0] = " "
+
+
+def _mask_copy(chars: list) -> list:
+ return list(chars)
+
+
+def _mask_out_strings(tokenised: list):
+ """Port of IBasicLoader::MaskOutStrings."""
+ text = "".join(tokenised)
+ q1 = text.find('"')
+ if q1 == -1:
+ return
+ rem = " REM "
+ r1 = text.find(rem)
+ if r1 != -1 and r1 < q1:
+ return
+
+ within = False
+ i = q1
+ n = len(tokenised)
+ while i < n:
+ if tokenised[i] == '"':
+ within = not within
+ elif within:
+ tokenised[i] = BLANK
+ else:
+ rest = "".join(tokenised[i + 1 :])
+ nq = rest.find('"')
+ if nq == -1:
+ return
+ nr = rest.find(rem)
+ if nr != -1 and nr < nq:
+ return
+ i += 1
+
+
+def _mask_out_rem_contents(tokenised: list):
+ """Port of IBasicLoader::MaskOutRemContents."""
+ text = "".join(tokenised)
+ rem = " REM "
+ pos = 0
+ while True:
+ r = text.find(rem, pos)
+ if r == -1:
+ return
+ q = text.find('"', pos)
+ if q != -1 and q < r:
+ q2 = text.find('"', q + 1)
+ if q2 == -1:
+ return
+ pos = q2 + 1
+ continue
+ start = r + len(rem)
+ for i in range(start, len(tokenised)):
+ tokenised[i] = BLANK
+ return
+
+
+def _extract_double_quote_characters(line: _Line):
+ """Port of zx81BasicLoader::ExtractDoubleQuoteCharacters (without
+ tokenized-REM support or alternate spelling, not needed here)."""
+ buf = line.buf
+ n = line.n
+ within_quote = False
+ i = 0
+ while i < n:
+ if not within_quote:
+ if buf[i] == '"':
+ within_quote = True
+ else:
+ chr1 = buf[i]
+ if chr1 == '"':
+ if i + 1 < n and buf[i + 1] == '"':
+ buf[i] = BLANK
+ line.out[i] = DOUBLE_QUOTE
+ line.populated[i] = True
+ i += 1
+ buf[i] = BLANK
+ else:
+ within_quote = False
+ i += 1
+
+
+def _do_tokenise(line: _Line, tokenised: list):
+ """Port of IBasicLoader::DoTokenise. Walks tokens from highest to
+ lowest code (equivalent to the reverse_iterator over a std::map),
+ mutating `tokenised` (search) and line.buf/out/populated (result)."""
+ for token_code in sorted(TOKENS.keys(), reverse=True):
+ token = TOKENS[token_code]
+ len_token = len(token)
+
+ start_char = token[0]
+ end_char = token[-1]
+
+ token_begins_with_space = start_char == " "
+ token_begins_with_alpha = start_char.isalpha()
+ token_ends_with_space = end_char == " "
+ token_ends_with_alpha = end_char.isalpha()
+
+ len_adjustment = 0
+ eff_len_token = len_token
+ if (
+ end_char in ("(", ")", "!", '"', "'", ",", ";", ":")
+ or (end_char == "#" and (len_token < 2 or token[-2] != " "))
+ or (end_char == "*" and token != "**")
+ ):
+ eff_len_token -= 1
+
+ guard = 0
+ while True:
+ guard += 1
+ if guard > 10000:
+ raise RuntimeError(f"Endless tokenizing loop for {token!r}")
+
+ text = "".join(tokenised)
+ pos = text.find(token)
+ if pos == -1:
+ break
+
+ prev_ok = (
+ token_begins_with_space
+ or not token_begins_with_alpha
+ or (pos == 0)
+ or (not tokenised[pos - 1].isalnum())
+ )
+ end_pos = pos + eff_len_token
+ next_ok = (
+ token_ends_with_space
+ or not token_ends_with_alpha
+ or (end_pos >= len(tokenised))
+ or (not tokenised[end_pos].isalnum())
+ )
+
+ if not (prev_ok and next_ok):
+ # Match glued to an identifier: not the token, "break"
+ # it locally so the search keeps going further ahead.
+ tokenised[pos] = "\x02"
+ continue
+
+ start_offset = 1 if token_begins_with_space else 0
+ end_offset = (eff_len_token - 1) if token_ends_with_space else (eff_len_token + len_adjustment)
+
+ for b in range(start_offset, end_offset):
+ tokenised[pos + b] = BLANK
+
+ for b in range(eff_len_token + len_adjustment):
+ idx = pos + b
+ if idx < len(line.buf):
+ line.buf[idx] = BLANK
+
+ output_index = pos + start_offset
+ if output_index < len(line.out):
+ line.out[output_index] = token_code
+ line.populated[output_index] = True
+
+
+def _start_of_number(line: _Line, tokenised: list, index: int) -> bool:
+ """Determines whether `index` is the start of a numeric literal.
+
+ Deliberately differs from the original
+ IBasicLoader::StartOfNumber on one point: here only the character
+ IMMEDIATELY before is checked (it walks back over real spaces but
+ stops as soon as it finds ANY non-space character, without walking
+ back further). The original walked back over ALL consecutive
+ spaces and checked the character before all of them, which in
+ standard BASIC is equivalent (tokens always consume their own
+ trailing space as BLANK, not as a literal ' ') but fails on SD81
+ custom commands like "*MAP 7,8": "MAP" isn't a recognized token,
+ so the space after it stays as a literal ' ', and the original
+ algorithm ended up looking at the (alphabetic) 'P' of MAP and
+ decided "7" was NOT a number. On a real ZX81, any digit typed after
+ a space generates its own embedded float regardless of the word
+ preceding it, so that behavior is replicated here (simpler and more
+ correct for this case).
+ """
+ if not (line.buf[index] == "." or line.buf[index].isdigit()):
+ return False
+
+ if index == 0:
+ return True
+
+ prev = line.buf[index - 1]
+ if prev == " ":
+ return True
+
+ return not (prev.isalpha() or prev.isdigit())
+
+
+_NUMBER_RE = re.compile(r"[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?")
+
+
+def _output_embedded_number(line: _Line, index: int, out: bytearray) -> int:
+ """Port of IBasicLoader::OutputEmbeddedNumber. Returns the new
+ index (already advanced past the number, ready for `i += 1`
+ outside)."""
+ n = line.n
+
+ text_no_spaces = ""
+ for j in range(index, n):
+ if line.buf[j] != BLANK:
+ text_no_spaces += line.buf[j]
+
+ m = _NUMBER_RE.match(text_no_spaces)
+ if not m or m.end() == 0:
+ raise ValueError(f"Invalid number at position {index}: {text_no_spaces!r}")
+ number_len_no_spaces = m.end()
+ value = float(m.group(0))
+
+ with_spaces_index = 0
+ for _ in range(number_len_no_spaces):
+ while line.buf[index + with_spaces_index] in (" ", BLANK):
+ with_spaces_index += 1
+ with_spaces_index += 1
+
+ end_index = index + with_spaces_index
+
+ i = index
+ while i < end_index:
+ chr_code = line.out[i]
+ if chr_code != BLANK and line.populated[i]:
+ out.append(chr_code)
+ i += 1
+
+ while i < n and line.buf[i] == " ":
+ out.append(ascii_to_zx(" "))
+ i += 1
+
+ out.append(NUMBER_MARK)
+ out.extend(zx81_float(value))
+
+ return i - 1
+
+
+def process_line_body(text: str) -> bytes:
+ """Tokenizes a BASIC line (without a line number) and returns the
+ already-encoded bytes (without the final NEWLINE, which is added
+ in encode_line)."""
+ line = _Line(text)
+ n = line.n
+
+ _extract_double_quote_characters(line)
+
+ # Working copy for token boundary detection: the real content
+ # (without line.buf's safety slot) + one extra real space to
+ # detect tokens with no closing space in the original text.
+ # Length n+1, same as line.buf, so indices stay aligned.
+ tokenised = list(line.buf[:n]) + [" "]
+
+ _mask_out_strings(tokenised)
+ _mask_out_rem_contents(tokenised)
+
+ _do_tokenise(line, tokenised)
+
+ # Fill any unconsumed position with its direct ZX81 code
+ for i in range(1, n):
+ if line.buf[i] != BLANK and not line.populated[i]:
+ line.out[i] = ascii_to_zx(line.buf[i])
+ line.populated[i] = True
+
+ body = bytearray()
+ i = 0
+ while i < n:
+ if line.populated[i]:
+ if _start_of_number(line, tokenised, i):
+ i = _output_embedded_number(line, i, body) + 1
+ continue
+ body.append(line.out[i])
+ i += 1
+
+ return bytes(body)
+
+
+def encode_line(line_number: int, text: str) -> bytes:
+ body = process_line_body(text) + bytes([NEWLINE])
+ return struct.pack(">H", line_number) + struct.pack(" bytes:
+ """lines: list of (line_number:int, text:str) tuples."""
+ return b"".join(encode_line(n, t) for n, t in lines)
+
+
+def build_p_file(lines: list) -> bytes:
+ """Port of zx81BasicLoader::OutputStartOfProgramData +
+ ProcessLine(*) + OutputEndOfProgramData. Returns the complete
+ contents of the .p file (from 0x4009)."""
+ data = bytearray()
+
+ def out_byte(b):
+ data.append(b & 0xFF)
+
+ def out_word(w):
+ data.append(w & 0xFF)
+ data.append((w >> 8) & 0xFF)
+
+ def change_word(offset, w):
+ data[offset] = w & 0xFF
+ data[offset + 1] = (w >> 8) & 0xFF
+
+ # --- OutputStartOfProgramData ---
+ out_byte(0x00) # VERSN
+ out_word(0x0000) # E_PPC
+ out_word(0x0000) # D_FILE
+ out_word(0x0000) # DF_CC
+ out_word(0x0000) # VARS
+ out_word(0x0000) # DEST
+ out_word(0x0000) # E_LINE
+ out_word(0x0000) # CH_ADD
+ out_word(0x0000) # X_PTR
+ out_word(0x0000) # STKBOT
+ out_word(0x0000) # STKEND
+ out_byte(0x00) # BERG
+ out_word(0x405D) # MEM
+ out_byte(0x00) # SPARE1
+ out_byte(0x02) # DF_SZ
+ out_word(0x0000) # S_TOP
+ out_word(0xFFFF) # LAST_K
+ out_byte(0x00) # DBOUNC
+ out_byte(0x37) # MARGIN
+ out_word(0x0000) # NXTLIN
+ out_word(0x0000) # OLDPPC
+ out_byte(0x00) # FLAGX
+ out_word(0x0000) # STRLEN
+ out_word(0x0C8D) # T_ADDR
+ out_word(0x4321) # SEED
+ out_word(0xE6E0) # FRAMES
+ out_word(0x0000) # COORDS
+ out_byte(0xBC) # PR_CC
+ out_word(0x1821) # S_POSN
+ out_byte(0x40) # CDFLAG
+ data.extend([0x00] * 32) # PRBUFF (32 empty bytes)
+ out_byte(0x76) # PRBUFF (byte 33, NEWLINE)
+ data.extend([0x00] * 30) # MEMBOT
+ out_word(0x0000) # SPARE
+
+ # --- BASIC lines ---
+ for line_number, text in lines:
+ data.extend(encode_line(line_number, text))
+
+ # --- OutputEndOfProgramData ---
+ START_OF_RAM = 16393 # 0x4009
+
+ dfile_address = START_OF_RAM + len(data)
+ data.extend([NEWLINE] * 25) # collapsed display file (empty)
+
+ vars_address = START_OF_RAM + len(data)
+ out_byte(0x80) # end of VARS (no variables)
+
+ eline_address = START_OF_RAM + len(data)
+
+ change_word(3, dfile_address) # D_FILE
+ change_word(5, dfile_address + 1) # DF_CC
+ change_word(7, vars_address) # VARS
+ change_word(11, vars_address + 1) # E_LINE
+ change_word(13, vars_address + 5) # CH_ADD
+ change_word(17, eline_address + 5) # STKBOT
+ change_word(19, eline_address + 5) # STKEND
+ change_word(32, vars_address) # NXTLIN (= VARS => no autorun)
+
+ return bytes(data)
+
+
+if __name__ == "__main__":
+ import sys
+
+ # Quick self-test with the manually validated loader
+ lines = [
+ (2, "FAST"),
+ (5, "LOAD THEN CLEAR 24575"),
+ (10, 'LOAD FAST "BOOT1.BIN"CODE 24576'),
+ (20, "LOAD *MAP 7,8"),
+ (25, 'LOAD FAST "TESTSD81P8.BIN"CODE 57344'),
+ (30, "LOAD *MAP 7,9"),
+ (35, 'LOAD FAST "TESTSD81P9.BIN"CODE 57344'),
+ (50, "LOAD *MAP 7,63"),
+ (60, "RAND USR 24576"),
+ ]
+
+ p_data = build_p_file(lines)
+ out_path = sys.argv[1] if len(sys.argv) > 1 else "loader_test.p"
+ with open(out_path, "wb") as f:
+ f.write(p_data)
+
+ print(f"Generated {out_path} ({len(p_data)} bytes)")
+ for i in range(0, len(p_data), 16):
+ chunk = p_data[i : i + 16]
+ hexpart = " ".join(f"{b:02X}" for b in chunk)
+ print(f"{i:04X}: {hexpart}")
diff --git a/src/arch/zxnext/__init__.py b/src/arch/zxnext/__init__.py
index 9a0377e6b..c79c6b5f5 100644
--- a/src/arch/zxnext/__init__.py
+++ b/src/arch/zxnext/__init__.py
@@ -1,4 +1,9 @@
-# vim:ts=4:et:sw=4:
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
import src.api.global_
from src.api.constants import TYPE
diff --git a/src/arch/zxnext/backend/_8bit.py b/src/arch/zxnext/backend/_8bit.py
index b21e1e9e5..25b5d34d4 100644
--- a/src/arch/zxnext/backend/_8bit.py
+++ b/src/arch/zxnext/backend/_8bit.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.arch.interface.quad import Quad
from src.arch.z80.backend import Bits8 as Z80Bits8
from src.arch.z80.backend.common import _int_ops
diff --git a/src/arch/zxnext/backend/__init__.py b/src/arch/zxnext/backend/__init__.py
index f71e1c5f7..e5731cc25 100644
--- a/src/arch/zxnext/backend/__init__.py
+++ b/src/arch/zxnext/backend/__init__.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.arch.z80.backend import (
HI16,
INITS,
diff --git a/src/arch/zxnext/backend/generic.py b/src/arch/zxnext/backend/generic.py
index 688c62852..7666a2864 100644
--- a/src/arch/zxnext/backend/generic.py
+++ b/src/arch/zxnext/backend/generic.py
@@ -1,6 +1,9 @@
-# ----------------------------------------------------------
-# Generic instructions
-# ----------------------------------------------------------
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
from src.api.config import OPTIONS
from src.arch.interface.quad import Quad
diff --git a/src/arch/zxnext/backend/main.py b/src/arch/zxnext/backend/main.py
index 21d2f0ddb..60e9b3d58 100644
--- a/src/arch/zxnext/backend/main.py
+++ b/src/arch/zxnext/backend/main.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.api.config import OPTIONS
from src.api.options import Action
from src.arch.z80.backend import Backend as Z80Backend
diff --git a/src/arch/zxnext/optimizer/__init__.py b/src/arch/zxnext/optimizer/__init__.py
index 91259bc01..912fe0e82 100644
--- a/src/arch/zxnext/optimizer/__init__.py
+++ b/src/arch/zxnext/optimizer/__init__.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from .main import Optimizer
__all__ = ("Optimizer",)
diff --git a/src/arch/zxnext/optimizer/basicblock.py b/src/arch/zxnext/optimizer/basicblock.py
index 6942163c1..2ad85e277 100644
--- a/src/arch/zxnext/optimizer/basicblock.py
+++ b/src/arch/zxnext/optimizer/basicblock.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from collections.abc import Iterable
from src.arch.z80.optimizer import Optimizer
diff --git a/src/arch/zxnext/optimizer/cpustate.py b/src/arch/zxnext/optimizer/cpustate.py
index 1b4154d87..f5c85ad72 100644
--- a/src/arch/zxnext/optimizer/cpustate.py
+++ b/src/arch/zxnext/optimizer/cpustate.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.arch.z80.optimizer.asm import Asm
from src.arch.z80.optimizer.cpustate import CPUState as CPUStateZ80
diff --git a/src/arch/zxnext/optimizer/main.py b/src/arch/zxnext/optimizer/main.py
index f2b7efd90..aa3fe91a7 100644
--- a/src/arch/zxnext/optimizer/main.py
+++ b/src/arch/zxnext/optimizer/main.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from src.arch.z80.optimizer import Optimizer as OptimizerZ80
from .basicblock import BasicBlock
diff --git a/src/arch/zxnext/optimizer/memcell.py b/src/arch/zxnext/optimizer/memcell.py
index b3190c36b..81047e2cd 100644
--- a/src/arch/zxnext/optimizer/memcell.py
+++ b/src/arch/zxnext/optimizer/memcell.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from functools import cached_property
from src.arch.z80.optimizer.memcell import MemCell as MemCellZ80
diff --git a/src/arch/zxnext/peephole/__init__.py b/src/arch/zxnext/peephole/__init__.py
index 834f1263a..a1cb75f89 100644
--- a/src/arch/zxnext/peephole/__init__.py
+++ b/src/arch/zxnext/peephole/__init__.py
@@ -1,3 +1,10 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
import os
# Optimization recipes paths for this arch
diff --git a/src/arch/zxnext/peephole/opts/060_o1_mul_de.opt b/src/arch/zxnext/peephole/opts/060_o1_mul_de.opt
index 962b9bc3a..75f4b0133 100644
--- a/src/arch/zxnext/peephole/opts/060_o1_mul_de.opt
+++ b/src/arch/zxnext/peephole/opts/060_o1_mul_de.opt
@@ -1,25 +1,25 @@
-;; Change sequence:
-;; ld h, XX
-;; ld d, h
-
-;; into:
-
-;; ld d, XX
-
-;; if it's used for an 8 bit multiplication (mul d, e)
-
-OLEVEL: 1
-OFLAG: 1
-
-REPLACE {{
- ld h, $1
- ld d, h
- ld e, a
- mul d, e
-}}
-
-WITH {{
- ld d, $1
- ld e, a
- mul d, e
-}}
+;; Change sequence:
+;; ld h, XX
+;; ld d, h
+
+;; into:
+
+;; ld d, XX
+
+;; if it's used for an 8 bit multiplication (mul d, e)
+
+OLEVEL: 1
+OFLAG: 1
+
+REPLACE {{
+ ld h, $1
+ ld d, h
+ ld e, a
+ mul d, e
+}}
+
+WITH {{
+ ld d, $1
+ ld e, a
+ mul d, e
+}}
diff --git a/src/ast/__init__.py b/src/ast/__init__.py
deleted file mode 100644
index cf950f96f..000000000
--- a/src/ast/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from .ast import Ast, NodeVisitor, types
-from .tree import Tree
-
-__all__ = (
- "Ast",
- "NodeVisitor",
- "Tree",
- "types",
-)
diff --git a/src/ast/ast.py b/src/ast/ast.py
deleted file mode 100644
index 0ad86e4b3..000000000
--- a/src/ast/ast.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# vim: ts=4:et:sw=4:
-
-# ----------------------------------------------------------------------
-# Copyleft (K), Jose M. Rodriguez-Rosa (a.k.a. Boriel)
-#
-# This program is Free Software and is released under the terms of
-# the GNU General License
-# ----------------------------------------------------------------------
-
-import types
-from collections.abc import Callable
-from typing import Any
-
-from .tree import Tree
-
-
-# ----------------------------------------------------------------------
-# Abstract Syntax Tree class
-# ----------------------------------------------------------------------
-class Ast(Tree):
- """Adds some methods for easier coding..."""
-
- __slots__: tuple[str, ...] = ()
-
- @property
- def token(self):
- return self.__class__
-
-
-class NodeVisitor:
- node_type: type = Ast
-
- def visit(self, node):
- stack = [node]
- last_result = None
-
- while stack:
- try:
- last = stack[-1]
- if isinstance(last, types.GeneratorType):
- stack.append(last.send(last_result))
- last_result = None
- elif isinstance(last, self.node_type):
- stack.append(self._visit(stack.pop()))
- else:
- last_result = stack.pop()
- except StopIteration:
- stack.pop()
-
- return last_result
-
- def _visit(self, node):
- meth = getattr(self, f"visit_{node.token}", self.generic_visit)
- return meth(node)
-
- def generic_visit(self, node: Ast):
- raise RuntimeError(f"No visit_{node.token}() method defined")
-
- def filter_inorder(
- self, node, filter_func: Callable[[Any], bool], child_selector: Callable[[Ast], bool] = lambda x: True
- ):
- """Visit the tree inorder, but only those that return true for filter_func and visiting children which
- return true for child_selector.
- """
- visited = set()
- stack = [node]
- while stack:
- node = stack.pop()
- if node in visited:
- continue
- visited.add(node)
- if filter_func(node):
- yield self.visit(node)
- if isinstance(node, Ast) and child_selector(node):
- stack.extend(node.children[::-1])
diff --git a/src/ast_/__init__.py b/src/ast_/__init__.py
new file mode 100644
index 000000000..221568150
--- /dev/null
+++ b/src/ast_/__init__.py
@@ -0,0 +1,15 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
+from .ast import Ast, NodeVisitor
+from .tree import Tree
+
+__all__ = (
+ "Ast",
+ "NodeVisitor",
+ "Tree",
+)
diff --git a/src/ast_/ast.py b/src/ast_/ast.py
new file mode 100644
index 000000000..69da2bb68
--- /dev/null
+++ b/src/ast_/ast.py
@@ -0,0 +1,42 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+from collections.abc import Callable, Generator
+from typing import Any, Final
+
+from .tree import Tree
+from .visitor import GenericNodeVisitor
+
+__all__: Final[tuple[str, ...]] = "Ast", "NodeVisitor"
+
+
+# ----------------------------------------------------------------------
+# Abstract Syntax Tree class
+# ----------------------------------------------------------------------
+class Ast(Tree):
+ """Adds some methods for easier coding..."""
+
+ __slots__: tuple[str, ...] = ()
+
+ @property
+ def token(self):
+ return self.__class__
+
+
+class NodeVisitor(GenericNodeVisitor[Ast]):
+ def _visit(self, node: Ast):
+ meth: Callable[[Ast], Generator[Ast | Any, Any]] = getattr(
+ self,
+ f"visit_{node.token}",
+ self.generic_visit,
+ )
+ return meth(node)
+
+ def generic_visit(self, node: Ast) -> Generator[Ast | Any, Any]:
+ for i, child in enumerate(node.children):
+ node.children[i] = yield self.visit(child)
+
+ yield node
diff --git a/src/ast_/exceptions.py b/src/ast_/exceptions.py
new file mode 100644
index 000000000..23102a633
--- /dev/null
+++ b/src/ast_/exceptions.py
@@ -0,0 +1,18 @@
+from typing import Final
+
+from src.api.exception import Error
+
+__all__: Final[tuple[str, ...]] = ("NotAnAstError",)
+
+
+class NotAnAstError(Error):
+ """Thrown when the "pointer" is not
+ an AST, but another thing.
+ """
+
+ def __init__(self, instance):
+ self.instance = instance
+ self.msg = "Object '%s' is not an Ast instance" % str(instance)
+
+ def __str__(self):
+ return self.msg
diff --git a/src/ast/tree.py b/src/ast_/tree.py
similarity index 89%
rename from src/ast/tree.py
rename to src/ast_/tree.py
index db2dd3678..d65e384e3 100644
--- a/src/ast/tree.py
+++ b/src/ast_/tree.py
@@ -1,25 +1,17 @@
+# --------------------------------------------------------------------
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
+# See the file CONTRIBUTORS.md for copyright details.
+# See https://www.gnu.org/licenses/agpl-3.0.html for details.
+# --------------------------------------------------------------------
+
from __future__ import annotations
import collections.abc
from collections.abc import Iterable, Iterator
from typing import Any
-from src.api.exception import Error
-
-__all__ = "ChildrenList", "NotAnAstError", "Tree"
-
-
-class NotAnAstError(Error):
- """Thrown when the "pointer" is not
- an AST, but another thing.
- """
-
- def __init__(self, instance):
- self.instance = instance
- self.msg = "Object '%s' is not an Ast instance" % str(instance)
-
- def __str__(self):
- return self.msg
+__all__ = "ChildrenList", "Tree"
class Tree:
diff --git a/src/ast_/visitor.py b/src/ast_/visitor.py
new file mode 100644
index 000000000..e6aea3ffd
--- /dev/null
+++ b/src/ast_/visitor.py
@@ -0,0 +1,40 @@
+__doc__ = "Implements a generic visitor class for Trees"
+
+from abc import abstractmethod
+from collections.abc import Generator
+from types import GeneratorType
+from typing import Final, NamedTuple
+
+__all__: Final[tuple[str, ...]] = ("GenericNodeVisitor",)
+
+
+class ToVisit[T](NamedTuple):
+ obj: T
+
+
+class GenericNodeVisitor[T]:
+ def visit(self, node: T | None) -> T | Generator[T | None] | None:
+ stack: list[T | GeneratorType] = [ToVisit[T](node) if node is not None else None]
+ last_result: T | None = None
+
+ while stack:
+ try:
+ stack_top = stack[-1]
+ if isinstance(stack_top, GeneratorType):
+ stack.append(stack_top.send(last_result))
+ last_result = None
+ elif isinstance(stack_top, ToVisit):
+ stack.pop()
+ stack.append(self._visit(stack_top.obj))
+ else:
+ last_result = stack.pop()
+ except StopIteration:
+ stack.pop()
+
+ return last_result
+
+ @abstractmethod
+ def _visit(self, node: T): ...
+
+ @abstractmethod
+ def generic_visit(self, node: T) -> Generator[T | None]: ...
diff --git a/src/lib/arch/zx48k/runtime/arith/mul32.asm b/src/lib/arch/zx48k/runtime/arith/mul32.asm
index fc6430221..641bc1efb 100644
--- a/src/lib/arch/zx48k/runtime/arith/mul32.asm
+++ b/src/lib/arch/zx48k/runtime/arith/mul32.asm
@@ -24,6 +24,3 @@ __TO32BIT: ; Converts H'L'HLB'C'AC to DEHL (Discards H'L'HL)
ret
pop namespace
-
-
-f
diff --git a/src/lib/arch/zx48k/runtime/spectranet.inc b/src/lib/arch/zx48k/runtime/spectranet.inc
index 052e61f22..73fbd2c1e 100644
--- a/src/lib/arch/zx48k/runtime/spectranet.inc
+++ b/src/lib/arch/zx48k/runtime/spectranet.inc
@@ -1,143 +1,143 @@
-;The MIT License
-;
-;Copyright (c) 2008 Dylan Smith
-;
-;Permission is hereby granted, free of charge, to any person obtaining a copy
-;of this software and associated documentation files (the "Software"), to deal
-;in the Software without restriction, including without limitation the rights
-;to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-;copies of the Software, and to permit persons to whom the Software is
-;furnished to do so, subject to the following conditions:
-;
-;The above copyright notice and this permission notice shall be included in
-;all copies or substantial portions of the Software.
-;
-;THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-;IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-;FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-;AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-;LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-;OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-;THE SOFTWARE.
-
-; This file can be included in assembly language programs to give
-; symbolic access to the public jump table entry points.
-
-; Avoid double inclusion
-#ifndef __SPECTRANET_INC__
-#define __SPECTRANET_INC__
-
-NAMESPACE Spectranet ; NAME PREFIX to avoid naming clash
-
-; Hardware page-in entry points
-MODULECALL equ $3FF8
-MODULECALL_NOPAGE equ $28
-PAGEIN equ $3FF9
-PAGEOUT equ $007C
-HLCALL equ $3FFA
-IXCALL equ $3FFD
-
-; Port defines
-CTRLREG equ $033B
-CPLDINFO equ $023B
-
-; Jump table entry points
-SOCKET equ $3E00 ; Allocate a socket
-CLOSE equ $3E03 ; Close a socket
-LISTEN equ $3E06 ; Listen for incoming connections
-ACCEPT equ $3E09 ; Accept an incoming connection
-BIND equ $3E0C ; Bind a local address to a socket
-CONNECT equ $3E0F ; Connect to a remote host
-SEND equ $3E12 ; Send data
-RECV equ $3E15 ; Receive data
-SENDTO equ $3E18 ; Send data to an address
-RECVFROM equ $3E1B ; Receive data from an address
-POLL equ $3E1E ; Poll a list of sockets
-POLLALL equ $3E21 ; Poll all open sockets
-POLLFD equ $3E24 ; Poll a single socket
-GETHOSTBYNAME equ $3E27 ; Look up a hostname
-PUTCHAR42 equ $3E2A ; 42 column print write a character
-PRINT42 equ $3E2D ; 42 column print a null terminated string
-CLEAR42 equ $3E30 ; Clear the screen and reset 42-col print
-SETPAGEA equ $3E33 ; Sets page area A
-SETPAGEB equ $3E36 ; Sets page area B
-LONG2IPSTRING equ $3E39 ; Convert a 4 byte big endian long to an IP
-IPSTRING2LONG equ $3E3C ; Convert an IP to a 4 byte big endian long
-ITOA8 equ $3E3F ; Convert a byte to ascii
-RAND16 equ $3E42 ; 16 bit PRNG
-REMOTEADDRESS equ $3E45 ; Fill struct sockaddr_in
-IFCONFIG_INET equ $3E48 ; Set IPv4 address
-IFCONFIG_NETMASK equ $3E4B ; Set netmask
-IFCONFIG_GW equ $3E4E ; Set gateway
-INITHW equ $3E51 ; Set the MAC address and initial hw registers
-GETHWADDR equ $3E54 ; Read the MAC address
-DECONFIG equ $3E57 ; Deconfigure inet, netmask and gateway
-MAC2STRING equ $3E5A ; Convert 6 byte MAC address to a string
-STRING2MAC equ $3E5D ; Convert a hex string to a 6 byte MAC address
-ITOH8 equ $3E60 ; Convert accumulator to hex string
-HTOI8 equ $3E63 ; Convert hex string to byte in A
-GETKEY equ $3E66 ; Get a key from the keyboard, and put it in A
-KEYUP equ $3E69 ; Wait for key release
-INPUTSTRING equ $3E6C ; Read a string into buffer at DE
-GET_IFCONFIG_INET equ $3E6F ; Gets the current IPv4 address
-GET_IFCONFIG_NETMASK equ $3E72 ; Gets the current netmask
-GET_IFCONFIG_GW equ $3E75 ; Gets the current gateway address
-SETTRAP equ $3E78 ; Sets the programmable trap
-DISABLETRAP equ $3E7B ; Disables the programmable trap
-ENABLETRAP equ $3E7E ; Enables the programmable trap
-PUSHPAGEA equ $3E81 ; Pages a page into area A, pushing the old one
-POPPAGEA equ $3E84 ; Restores the previous page in area A
-PUSHPAGEB equ $3E87 ; Pages into area B pushing the old one
-POPPAGEB equ $3E8A ; Restores the previous page in area B
-PAGETRAPRETURN equ $3E8D ; Returns from a trap to page area B
-TRAPRETURN equ $3E90 ; Returns from a trap that didn't page area B
-ADDBASICEXT equ $3E93 ; Adds a BASIC command
-STATEMENT_END equ $3E96 ; Check for statement end, exit at syntax time
-EXIT_SUCCESS equ $3E99 ; Use this to exit successfully after cmd
-PARSE_ERROR equ $3E9C ; Use this to exit to BASIC with a parse error
-RESERVEPAGE equ $3E9F ; Reserve a page of static RAM
-FREEPAGE equ $3EA2 ; Free a page of static RAM
-REPORTERR equ $3EA5 ; report an error via BASIC
-
-; Filesystem functions
-MOUNT equ $3EA8
-UMOUNT equ $3EAB
-OPENDIR equ $3EAE
-OPEN equ $3EB1
-UNLINK equ $3EB4
-MKDIR equ $3EB7
-RMDIR equ $3EBA
-SIZE equ $3EBD
-FREE equ $3EC0
-STAT equ $3EC3
-CHMOD equ $3EC6
-READ equ $3EC9
-WRITE equ $3ECC
-LSEEK equ $3ECF
-VCLOSE equ $3ED2
-VPOLL equ $3ED5
-READDIR equ $3ED8
-CLOSEDIR equ $3EDB
-CHDIR equ $3EDE
-GETCWD equ $3EE1
-RENAME equ $3EE4
-SETMOUNTPOINT equ $3EE7
-FREEMOUNTPOINT equ $3EEA
-RESALLOC equ $3EED
-
-
-; Definitions
-ALLOCFD equ 1
-FREEFD equ 0
-ALLOCDIRHND equ 3
-FREEDIRHND equ 2
-
-; POLL status bits
-BIT_RECV equ 2
-BIT_DISCON equ 1
-BIT_CONN equ 0
-
-NAMESPACE DEFAULT ; Clears namespace
-
-#endif
-
+;The MIT License
+;
+;Copyright (c) 2008 Dylan Smith
+;
+;Permission is hereby granted, free of charge, to any person obtaining a copy
+;of this software and associated documentation files (the "Software"), to deal
+;in the Software without restriction, including without limitation the rights
+;to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+;copies of the Software, and to permit persons to whom the Software is
+;furnished to do so, subject to the following conditions:
+;
+;The above copyright notice and this permission notice shall be included in
+;all copies or substantial portions of the Software.
+;
+;THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+;IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+;FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+;AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+;LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+;OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+;THE SOFTWARE.
+
+; This file can be included in assembly language programs to give
+; symbolic access to the public jump table entry points.
+
+; Avoid double inclusion
+#ifndef __SPECTRANET_INC__
+#define __SPECTRANET_INC__
+
+NAMESPACE Spectranet ; NAME PREFIX to avoid naming clash
+
+; Hardware page-in entry points
+MODULECALL equ $3FF8
+MODULECALL_NOPAGE equ $28
+PAGEIN equ $3FF9
+PAGEOUT equ $007C
+HLCALL equ $3FFA
+IXCALL equ $3FFD
+
+; Port defines
+CTRLREG equ $033B
+CPLDINFO equ $023B
+
+; Jump table entry points
+SOCKET equ $3E00 ; Allocate a socket
+CLOSE equ $3E03 ; Close a socket
+LISTEN equ $3E06 ; Listen for incoming connections
+ACCEPT equ $3E09 ; Accept an incoming connection
+BIND equ $3E0C ; Bind a local address to a socket
+CONNECT equ $3E0F ; Connect to a remote host
+SEND equ $3E12 ; Send data
+RECV equ $3E15 ; Receive data
+SENDTO equ $3E18 ; Send data to an address
+RECVFROM equ $3E1B ; Receive data from an address
+POLL equ $3E1E ; Poll a list of sockets
+POLLALL equ $3E21 ; Poll all open sockets
+POLLFD equ $3E24 ; Poll a single socket
+GETHOSTBYNAME equ $3E27 ; Look up a hostname
+PUTCHAR42 equ $3E2A ; 42 column print write a character
+PRINT42 equ $3E2D ; 42 column print a null terminated string
+CLEAR42 equ $3E30 ; Clear the screen and reset 42-col print
+SETPAGEA equ $3E33 ; Sets page area A
+SETPAGEB equ $3E36 ; Sets page area B
+LONG2IPSTRING equ $3E39 ; Convert a 4 byte big endian long to an IP
+IPSTRING2LONG equ $3E3C ; Convert an IP to a 4 byte big endian long
+ITOA8 equ $3E3F ; Convert a byte to ascii
+RAND16 equ $3E42 ; 16 bit PRNG
+REMOTEADDRESS equ $3E45 ; Fill struct sockaddr_in
+IFCONFIG_INET equ $3E48 ; Set IPv4 address
+IFCONFIG_NETMASK equ $3E4B ; Set netmask
+IFCONFIG_GW equ $3E4E ; Set gateway
+INITHW equ $3E51 ; Set the MAC address and initial hw registers
+GETHWADDR equ $3E54 ; Read the MAC address
+DECONFIG equ $3E57 ; Deconfigure inet, netmask and gateway
+MAC2STRING equ $3E5A ; Convert 6 byte MAC address to a string
+STRING2MAC equ $3E5D ; Convert a hex string to a 6 byte MAC address
+ITOH8 equ $3E60 ; Convert accumulator to hex string
+HTOI8 equ $3E63 ; Convert hex string to byte in A
+GETKEY equ $3E66 ; Get a key from the keyboard, and put it in A
+KEYUP equ $3E69 ; Wait for key release
+INPUTSTRING equ $3E6C ; Read a string into buffer at DE
+GET_IFCONFIG_INET equ $3E6F ; Gets the current IPv4 address
+GET_IFCONFIG_NETMASK equ $3E72 ; Gets the current netmask
+GET_IFCONFIG_GW equ $3E75 ; Gets the current gateway address
+SETTRAP equ $3E78 ; Sets the programmable trap
+DISABLETRAP equ $3E7B ; Disables the programmable trap
+ENABLETRAP equ $3E7E ; Enables the programmable trap
+PUSHPAGEA equ $3E81 ; Pages a page into area A, pushing the old one
+POPPAGEA equ $3E84 ; Restores the previous page in area A
+PUSHPAGEB equ $3E87 ; Pages into area B pushing the old one
+POPPAGEB equ $3E8A ; Restores the previous page in area B
+PAGETRAPRETURN equ $3E8D ; Returns from a trap to page area B
+TRAPRETURN equ $3E90 ; Returns from a trap that didn't page area B
+ADDBASICEXT equ $3E93 ; Adds a BASIC command
+STATEMENT_END equ $3E96 ; Check for statement end, exit at syntax time
+EXIT_SUCCESS equ $3E99 ; Use this to exit successfully after cmd
+PARSE_ERROR equ $3E9C ; Use this to exit to BASIC with a parse error
+RESERVEPAGE equ $3E9F ; Reserve a page of static RAM
+FREEPAGE equ $3EA2 ; Free a page of static RAM
+REPORTERR equ $3EA5 ; report an error via BASIC
+
+; Filesystem functions
+MOUNT equ $3EA8
+UMOUNT equ $3EAB
+OPENDIR equ $3EAE
+OPEN equ $3EB1
+UNLINK equ $3EB4
+MKDIR equ $3EB7
+RMDIR equ $3EBA
+SIZE equ $3EBD
+FREE equ $3EC0
+STAT equ $3EC3
+CHMOD equ $3EC6
+READ equ $3EC9
+WRITE equ $3ECC
+LSEEK equ $3ECF
+VCLOSE equ $3ED2
+VPOLL equ $3ED5
+READDIR equ $3ED8
+CLOSEDIR equ $3EDB
+CHDIR equ $3EDE
+GETCWD equ $3EE1
+RENAME equ $3EE4
+SETMOUNTPOINT equ $3EE7
+FREEMOUNTPOINT equ $3EEA
+RESALLOC equ $3EED
+
+
+; Definitions
+ALLOCFD equ 1
+FREEFD equ 0
+ALLOCDIRHND equ 3
+FREEDIRHND equ 2
+
+; POLL status bits
+BIT_RECV equ 2
+BIT_DISCON equ 1
+BIT_CONN equ 0
+
+NAMESPACE DEFAULT ; Clears namespace
+
+#endif
+
diff --git a/src/lib/arch/zx48k/stdlib/README b/src/lib/arch/zx48k/stdlib/README
index 401199747..7d24b7e8a 100644
--- a/src/lib/arch/zx48k/stdlib/README
+++ b/src/lib/arch/zx48k/stdlib/README
@@ -1,4 +1,4 @@
-This directory contains a library of "basic" BASIC functions.
-Some of them are related to ZX Spectrum BASIC (i.e. ATTR) while
-others are more standard to FREE BASIC (csrlin or pos)
-
+This directory contains a library of "basic" BASIC functions.
+Some of them are related to ZX Spectrum BASIC (i.e. ATTR) while
+others are more standard to FREE BASIC (csrlin or pos)
+
diff --git a/src/lib/arch/zx48k/stdlib/SP/Fill.bas b/src/lib/arch/zx48k/stdlib/SP/Fill.bas
index ab1a4ea39..3c979aee7 100644
--- a/src/lib/arch/zx48k/stdlib/SP/Fill.bas
+++ b/src/lib/arch/zx48k/stdlib/SP/Fill.bas
@@ -115,11 +115,6 @@ SPPFill:
call SPGetScrnAddr ; de = screen address, b = pixel byte
ex de,hl ; hl = screen address
-; ld b, h
-; ld c, l
-; call 22B0h ; Uses ROM Pixel ADDR
-; ld b, a
-
call bytefill ; b = fill byte
jr c, viable
pop bc
diff --git a/src/lib/arch/zx48k/stdlib/cb/maskedsprites.bas b/src/lib/arch/zx48k/stdlib/cb/maskedsprites.bas
new file mode 100644
index 000000000..27f390869
--- /dev/null
+++ b/src/lib/arch/zx48k/stdlib/cb/maskedsprites.bas
@@ -0,0 +1,1306 @@
+' ----------------------------------------------------------------
+' This file is released under the MIT License
+'
+' Copyright (C) 2026 Conrado Badenas
+' Ideas taken from
+' https://github.com/boriel-basic/zxbasic/blob/main/src/lib/arch/zx48k/stdlib/memorybank.bas
+' by Juan Segura (a.k.a. Duefectu),
+' https://github.com/oisee/antique-toy/blob/main/chapters/ch16-sprites/draft.md
+' by Alice Vinogradova (a.k.a. oisee), and
+' https://youtu.be/nBHXtI1Y-xU?t=434 and https://youtu.be/-AUmmzDiGlE?t=434
+' by Benjamín (a.k.a. RetrobenSoft)
+'
+' Print Masked (AND+OR) Sprites, version 2026.04.05
+' ----------------------------------------------------------------
+
+#ifndef __CB_MASKEDSPRITES__
+
+REM Avoid recursive / multiple inclusion
+
+#define __CB_MASKEDSPRITES__
+
+#include
+#include
+
+
+' ----------------------------------------------------------------
+' MaskedSprites_OUT_7FFD is a MACRO of ASM code,
+' which uses a hack to ensure a good reading of the IFF2 flip-flop.
+' Hack was found thanks to Pedro Picapiedra aka ProgramadorHedonista,
+' who pointed me to this line of the Z80 User Manual:
+' "If an interrupt occurs during execution of this instruction
+' [LD A,I or LD A,R], the Parity flag contains a 0."
+' ----------------------------------------------------------------
+#define MaskedSprites_OUT_7FFD \
+ ld c,a ; save A in C \
+ ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even \
+ jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI \
+ ld a,i ; read IFF2 again to ensure that IFF2=0=DI \
+1: ld a,c \
+ ld bc,$7ffd ; Memory Bank control port \
+ di ; Disable interrupts (IFF1,2=0) \
+ ld ($5b5c),a ; Update BANKM system variable \
+ out (c),a ; Set the bank \
+ ret po ; Return with DI if IFF2=0=DI at the beginning \
+ ei ; Return with EI if IFF2=1=EI at the beginning
+
+
+' ----------------------------------------------------------------
+' Set a RAM bank in addresses $c000-$ffff, update BANKM,
+' and return with INTerrupts preserved (unchanged)
+' Only works on 128K and compatible models.
+' Parameters:
+' Ubyte: bank number 0,1,2,3,4,5,6,7
+' Changes:
+' A, B, C
+' Preserves:
+' D, E, H, L are not used
+' ----------------------------------------------------------------
+SUB FASTCALL SetBankPreservingINTs(bankNumber AS UByte)
+ASM
+ and 7 ; make sure bankNumber = 0,1,2,3,4,5,6,7
+ ld c,a ; C = A = bankNumber
+ ld a,($5b5c) ; Read BANKM system variable
+ and %11111000 ; Reset bank bits
+ or c ; Set bank bits to bankNumber
+ MaskedSprites_OUT_7FFD
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' Get which RAM bank is set in addresses $c000-$ffff
+' Only works on 128K and compatible models.
+' Preserves:
+' B, C, D, E, H, L are not used
+' Returns:
+' UByte: bank number 0,1,2,3,4,5,6,7
+' ----------------------------------------------------------------
+#define MaskedSprites_GETBANK \
+ ld a,($5b5c) ; Read BANKM system variable \
+ and 7 ; Obtain bank bits
+FUNCTION FASTCALL GetBankPreservingRegs() AS UByte
+ASM
+ MaskedSprites_GETBANK
+END ASM
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Check whether memory paging works (128,+2,...) or not (16,48)
+' Returns:
+' UByte: 1 if paging works, 0 if it does not
+' ----------------------------------------------------------------
+FUNCTION FASTCALL CheckMemoryPaging() AS UByte
+ DIM b,i AS UByte
+ DIM p AS UInteger
+ CONST address AS UInteger = $fffe
+
+ b = GetBankPreservingRegs()
+ p = peek(UInteger, address)
+ for i=0 to 7
+ SetBankPreservingINTs(i)
+ if p<>peek(UInteger, address) then 'there is another bank at address
+ if i<>b then SetBankPreservingINTs(b)
+ return 1
+ end if
+ next i
+' Here, we are in an unpaged Spectrum (48), or maybe...
+' ...all banks hold the same value p at address.
+
+ poke UInteger address, p bXOR $5555 ' we change value at address in RAM7
+ SetBankPreservingINTs(0)
+ if p=peek(UInteger, address) then ' value at address in RAM0 is as before
+ SetBankPreservingINTs(7)
+ poke UInteger address, p
+ if 7<>b then SetBankPreservingINTs(b)
+ return 1
+ end if
+ poke UInteger address, p ' memory paging does not work, we restore value at address
+ return 0
+ASM
+
+END ASM
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Set the visible screen (either in bank5 or bank7)
+' and updates the system variable BANKM.
+' Only works on 128K and compatible models.
+' Parameters:
+' Ubyte: bank number 5 or 7
+' Preserves:
+' D, E, H, L are not used
+' ----------------------------------------------------------------
+SUB FASTCALL SetVisibleScreen(bankNumber AS UByte)
+ASM
+ ; A = bankNumber 5 or 7 with screen to be visible
+ and %00000010 ; bit1 = 0/1 for bank5/7
+ rla
+ rla ; bit3 = 0/1 for bank5/7
+ ld c,a
+ ld a,($5b5c) ; Read BANKM system variable
+ and %11110111 ; Reset bit3
+ or c ; Set screen in bank5/7
+ MaskedSprites_OUT_7FFD
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' Returns the bank of visible screen (either 5 or 7)
+' according to system variable BANKM.
+' Only works on 128K and compatible models.
+' Returns:
+' UByte: bank 5 or 7
+' ----------------------------------------------------------------
+FUNCTION FASTCALL GetVisibleScreen() AS UByte
+ASM
+ ld a,($5b5c) ; Read BANKM system variable
+ and %00001000 ; Get bit3 = 0/1 for bank5/7
+ rra
+ rra ; bit1 = 0/1 for bank5/7
+ or %00000101 ; A = 5/7 for bank5/7
+END ASM
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Toggles the visible screen (from 5 to 7, or from 7 to 5)
+' and updates the system variable BANKM.
+' Only works on 128K and compatible models.
+' ----------------------------------------------------------------
+SUB FASTCALL ToggleVisibleScreen()
+ SetVisibleScreen(2 bXOR GetVisibleScreen() )
+ '2 bXOR 5/7 = 7/5
+END SUB
+
+
+' ----------------------------------------------------------------
+' Copy contents of screen5 to screen7 (display file + attribs)
+' Only works on 128K and compatible models.
+' ----------------------------------------------------------------
+SUB FASTCALL CopyScreen5ToScreen7()
+ DIM b AS UByte
+
+ b = GetBankPreservingRegs()
+ if b<>7 then SetBankPreservingINTs(7)
+ MemCopy($4000,$c000,$1b00)
+ if b<>7 then SetBankPreservingINTs(b)
+END SUB
+
+
+' ----------------------------------------------------------------
+' Copy contents of screen7 to screen5 (display file + attribs)
+' Only works on 128K and compatible models.
+' ----------------------------------------------------------------
+SUB FASTCALL CopyScreen7ToScreen5()
+ DIM b AS UByte
+
+ b = GetBankPreservingRegs()
+ if b<>7 then SetBankPreservingINTs(7)
+ MemCopy($c000,$4000,$1b00)
+ if b<>7 then SetBankPreservingINTs(b)
+END SUB
+
+
+' ----------------------------------------------------------------
+' Set ScreenBufferAddr and AttrBufferAddr to screen5
+' Only works on 128K and compatible models.
+' ----------------------------------------------------------------
+SUB FASTCALL SetDrawingScreen5()
+ SetScreenBufferAddr($4000)' ld (.core.SCREEN_ADDR),hl
+ SetAttrBufferAddr($5800)' ld (.core.SCREEN_ATTR_ADDR),hl
+END SUB
+
+
+' ----------------------------------------------------------------
+' Put screen7 at $c000 (in case it is not), and
+' Set ScreenBufferAddr and AttrBufferAddr to screen7
+' Only works on 128K and compatible models.
+' Returns:
+' Bank7 is set at $c000, old bank is removed
+' UByte: bank that was at $c000 (to restore it manually IYW)
+' ----------------------------------------------------------------
+FUNCTION FASTCALL SetDrawingScreen7() AS UByte
+ DIM b AS UByte
+
+ b = GetBankPreservingRegs()
+ if b<>7 then SetBankPreservingINTs(7)
+ SetScreenBufferAddr($c000)' ld (.core.SCREEN_ADDR),hl
+ SetAttrBufferAddr($d800)' ld (.core.SCREEN_ATTR_ADDR),hl
+ RETURN b
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Toggle ScreenBufferAddr and AttrBufferAddr between screen5,7
+' Only works on 128K and compatible models.
+' ----------------------------------------------------------------
+SUB FASTCALL ToggleDrawingScreen()
+ SetScreenBufferAddr($8000 bXOR GetScreenBufferAddr() )
+ SetAttrBufferAddr($8000 bXOR GetAttrBufferAddr() )
+ '$8000 bXOR $4000/$c000 = $c000/$4000
+ '$8000 bXOR $5800/$d800 = $d800/$5800
+END SUB
+
+
+' ----------------------------------------------------------------
+' MaskedSpritesBackgroundSet = 0 or 1 is the Set of Backgrounds
+'
+' MaskedSpritesBackground(i) is the address where Background i begins
+'
+' NumberofMaskedSprites is a MACRO that should be #define-d
+' before #include-ing this file
+'
+' MaskedSprites_USE_STACK_TRANSFER is a MACRO that should be #define-d
+' if you want this library to use Stack PUSH+POP instructions to speed-up
+' transfer of information between different parts of the RAM
+' (this library will disable interrupts before using Stack Transfer)
+'
+' ChangeMaskedSpritesBackgroundSet() changes the Set of Backgrounds
+' Returns:
+' Byte: new value of MaskedSpritesBackgroundSet (IYW to use it)
+' ----------------------------------------------------------------
+dim MaskedSpritesBackgroundSet AS UByte = 0
+
+#define MaskedSpritesBackground(i) ( $db00+48*CAST(UInteger,i)+48*CAST(UInteger,NumberofMaskedSprites)*MaskedSpritesBackgroundSet )
+
+FUNCTION FASTCALL ChangeMaskedSpritesBackgroundSet() AS UByte
+ MaskedSpritesBackgroundSet = MaskedSpritesBackgroundSet bXOR 1
+ RETURN MaskedSpritesBackgroundSet
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' MaskedSprites_NEXT_ROW is a MACRO of ASM code, based on code from
+' https://zonadepruebas.com/viewtopic.php?f=15&t=8372&start=40#p81507
+' and found by Joaquin Ferrero
+' ----------------------------------------------------------------
+#define MaskedSprites_NEXT_ROW \
+ ld a,e ; 4 A = E \
+ sub 224 ; 7 A = E + 32 (SUB 224 is similar to +32) \
+ ; CF = 0/1 iff E >=/< 224 iff a third is/isn't crossed \
+ ld e,a ; 4 \
+ sbc a,a ; 4 A = 0/255 \
+ and 248 ; 7 A = 0/248 (248 = -8) \
+ add a,d ; 4 A = D/D-8 iff a third is/isn't crossed \
+ ld d,a ; 4 += 34 Ts
+
+
+' ----------------------------------------------------------------
+' Save background and Draw sprite in screen
+' Parameters:
+' UByte: X coordinate (0:left to 240:right)
+' UByte: Y coordinate (0:up to 176:down)
+' UInteger: Address where background will be saved
+' UInteger: Address where sprite image begins
+' ----------------------------------------------------------------
+SUB FASTCALL SaveBackgroundAndDrawSprite(X AS UByte, Y AS UByte, backgroundAddr AS UInteger, spriteImageAddr AS UInteger)
+ASM
+ PROC
+ LOCAL shiftright, shiftleft, noshift
+ LOCAL loopSR, loopSL, loopNS, loopR, loopL, branchSR, branchSL, branchNS
+ ; A = X
+ pop de ; returnAddr
+ exx
+ pop bc ; B = Y
+ ld c,a ; C = X
+; BEGIN code from https://skoolkid.github.io/rom/asm/22AA.html
+ rlca
+ rlca
+ rlca ; A = %c4c3c2c1c0c7c6c5
+ xor b
+ and %11000111
+ xor b ; A = %c4c3b5b4b3c7c6c5
+ rlca
+ rlca
+ ld e,a ; E = %b5b4b3c7c6c5c4c3
+ ld a,b
+ and %11111000
+ rra
+ rra
+ rra ; A = %.0.0.0b7b6b5b4b3
+ xor b
+ and %11111000
+ xor b
+ ld d,a ; D = %.0.0.0b7b6b2b1b0
+; END code from https://skoolkid.github.io/rom/asm/22AA.html
+ ld hl,(.core.SCREEN_ADDR)
+ add hl,de
+ ex de,hl ; DE = screenAddr where drawing will start
+ ld a,c; ; A = X
+ and 7
+ jr z,noshift ; jump if X is a multiple of 8 (unlikely)
+ ; continue if sprite must be shifted
+ cp 4 ; is >= 4 ?
+ jp nc,shiftleft ; shift left if X MOD 8 = 4,5,6,7
+ ; shift right if X MOD 8 = 1,2,3
+shiftright:
+ pop bc ; backgroundAddr
+ exx
+ pop hl ; spriteImageAddr
+ push de ; returnAddr
+ push ix
+ ld ixh,16 ; 16 scanlines
+ ld ixl,a ; IXl = X MOD 8 = 1,2,3,4
+loopSR:
+ ld a,(hl) ; mask1
+ inc hl
+ ld c,(hl) ; graph1
+ inc hl
+ ld d,(hl) ; mask2
+ inc hl
+ ld e,(hl) ; graph2
+ inc hl
+ push hl ; spriteImageAddr
+
+ ld hl,$FF00 ; H = 255 , L = 0
+ ld b,ixl
+loopR:
+ scf ; 4
+ rra ; 4; SCF + RRA injects a 1 in bit7 of A
+ rr d ; 8
+ rr h ; 8
+ srl c ; 8; ShiftRightLogical injects a 0 in bit7 of C
+ rr e ; 8
+ rr l ; 8
+ djnz loopR ; 4+4+8+8+8+8+8 = 48 Ts
+ ld b,a
+ push hl ; H,L = mask,graph 3rd byte
+ push de ; D,E = mask,graph 2nd byte
+ push bc ; B,C = mask,graph 1st byte
+ exx
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 1st byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 2nd byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 3rd byte done
+ dec e
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branchSR ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ exx
+ pop hl ; spriteImageAddr
+ dec ixh
+ jp nz,loopSR
+ pop ix
+ ret
+branchSR:
+ MaskedSprites_NEXT_ROW
+ exx
+ pop hl ; spriteImageAddr
+ dec ixh
+ jp nz,loopSR
+ pop ix
+ ret
+
+noshift:
+ pop bc ; backgroundAddr
+ pop hl ; spriteImageAddr
+ exx
+ push de ; returnAddr
+ exx
+ push ix
+ ld ixh,16 ; 16 scanlines
+loopNS:
+ ld a,(de) ; screen
+ ld (bc),a; ; save
+ inc bc
+ and (hl); ; mask
+ inc hl
+ or (hl) ; graph
+ inc hl
+ ld (de),a ; 1st byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ and (hl); ; mask
+ inc hl
+ or (hl) ; graph
+ inc hl
+ ld (de),a ; 2nd byte done
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branchNS ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ dec ixh
+ jp nz,loopNS
+ pop ix
+ ret
+branchNS:
+ MaskedSprites_NEXT_ROW
+ dec ixh
+ jp nz,loopNS
+ pop ix
+ ret
+
+shiftleft:
+ pop bc ; backgroundAddr
+ exx
+ pop hl ; spriteImageAddr
+ push de ; returnAddr
+ push ix
+ ld ixh,16 ; 16 scanlines
+ sub 8
+ neg ; A = 8 - oldA
+ ld ixl,a ; IXl = 8 - (X MOD 8) = 8 - 4,5,6,7 = 4,3,2,1
+loopSL:
+ ld a,(hl) ; mask1
+ inc hl
+ ld c,(hl) ; graph1
+ inc hl
+ ld d,(hl) ; mask2
+ inc hl
+ ld e,(hl) ; graph2
+ inc hl
+ push hl ; spriteImageAddr
+
+ ld hl,$FF00 ; H = 255 , L = 0
+ ld b,ixl
+loopL:
+ sll d ; 8; ShiftLeftLogical injects a 1 in bit0 of D
+ rla ; 4
+ rl h ; 8
+ sla e ; 8; ShiftLeftArithmetic injects a 0 in bit0 of E
+ rl c ; 8
+ rl l ; 8
+ djnz loopL ; 8+4+8+8+8+8 = 44 Ts
+ ld b,a
+ push de ; D,E = mask,graph 3rd byte
+ push bc ; B,D = mask,graph 2nd byte
+ push hl ; H,L = mask,graph 1st byte
+ exx
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 1st byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 2nd byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 3rd byte done
+ dec e
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branchSL ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ exx
+ pop hl ; spriteImageAddr
+ dec ixh
+ jp nz,loopSL
+ pop ix
+ ret
+branchSL:
+ MaskedSprites_NEXT_ROW
+ exx
+ pop hl ; spriteImageAddr
+ dec ixh
+ jp nz,loopSL
+ pop ix
+ ret
+ ENDP
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' Restore background in screen
+' Parameters:
+' UByte: X coordinate (0:left to 240:right)
+' UByte: Y coordinate (0:up to 176:down)
+' UInteger: Address where saved background begins
+' ----------------------------------------------------------------
+SUB FASTCALL RestoreBackground(X AS UByte, Y AS UByte, backgroundAddr AS UInteger)
+ASM
+ PROC
+ LOCAL loop2b, loop3b, branch2b, branch3b
+ ; A = X
+ pop de ; returnAddr
+ exx
+ pop bc ; B = Y
+ ld c,a ; C = X
+; BEGIN code from https://skoolkid.github.io/rom/asm/22AA.html
+ rlca
+ rlca
+ rlca ; A = %c4c3c2c1c0c7c6c5
+ xor b
+ and %11000111
+ xor b ; A = %c4c3b5b4b3c7c6c5
+ rlca
+ rlca
+ ld e,a ; E = %b5b4b3c7c6c5c4c3
+ ld a,b
+ and %11111000
+ rra
+ rra
+ rra ; A = %.0.0.0b7b6b5b4b3
+ xor b
+ and %11111000
+ xor b
+ ld d,a ; D = %.0.0.0b7b6b2b1b0
+; END code from https://skoolkid.github.io/rom/asm/22AA.html
+ ld hl,(.core.SCREEN_ADDR)
+ add hl,de
+ ex de,hl ; DE = screenAddr where restoring will start
+ pop hl ; backgroundAddr
+ exx
+ push de ; returnAddr
+ exx
+ ld a,c; ; A = X
+ ld bc,$10FF ; B = 16, C = 255 (up to 255 LDIs do not change B)
+ and 7
+ jr z,loop2b ; jump if X is a multiple of 8 (unlikely)
+ ; continue if restoring 3 bytes per scanline
+; 3bytes per scanline
+loop3b:
+ ldi ; 16 Ts vs 7+7+6+4=24 Ts
+ ldi
+ ldi ; 3 bytes background restored to screen
+ dec de ; last LDI could have increased D if initially E=253...
+ dec e ; ...so DEC DE restores D in that case
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branch3b ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ djnz loop3b
+ ret
+branch3b:
+ MaskedSprites_NEXT_ROW
+ djnz loop3b
+ ret
+; 2bytes per scanline
+loop2b:
+ ldi ; 16 Ts vs 7+7+6+4=24 Ts
+ ldi ; 2 bytes background restored to screen
+ dec de ; last LDI could have increased D if initially E=254...
+ dec e ; ...so DEC DE restores D in that case
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branch2b ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ djnz loop2b
+ ret
+branch2b:
+ MaskedSprites_NEXT_ROW
+ djnz loop2b
+ ret
+ ENDP
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' Structure of the Masked Sprites FileSystem (MSFS):
+'
+' MSFS consists of many blocks of 96 bytes
+' MSFS starts at address stored in MaskedSpritesFileSystemStart, e.g., 56736
+' MSFS length is a multiple of 96 bytes, e.g., 8736 = 91*96 bytes
+' With that length, MSFS ranges from 56736 to 65471
+' MSFS stores Images (mask+graph) of Masked Sprites
+' Blocks used are marked in the FSB (https://en.wikipedia.org/wiki/Free-space_bitmap)
+'
+' First block of the MSFS (superblock, block number = 0)
+' start+0 DEFB number of blocks in MSFS = bits of the FSB, e.g., 91
+' start+1 DEFB number of bytes of the FSB, e.g., 12 (91/8 = 11.4)
+' start+2-13 DEFS 12 is the FSB (12 bytes = 96 bits is enough for 91 blocks)
+' start+14-15 unused
+'
+' Block of an unshifted image (block number n = 0,...,90)
+' start+n*96 \
+' ... | 16 bytes unused for n>0, superblock for n=0
+' start+n*96+15 /
+' start+n*96+16-17 DEFW start+n*96+32 = start of unshifted image
+' start+n*96+18-19 DEFW address of block for image shifted 1 pixel, or 0 if not used
+' start+n*96+20-21 DEFW address of block for image shifted 2 pixels, or 0 if not used
+' ...
+' start+n*96+30-31 DEFW address of block for image shifted 7 pixels, or 0 if not used
+' start+n*96+32-95 DEFS 64 the unshifted image (mask+graph)
+'
+' Block number 0 (n=0) is special because it contains
+' 16 bytes for the superblock (including 2 unused bytes) +
+' 16 bytes for addressing unshifted and shifted versions of the first image +
+' 64 bytes for the first unshifted image
+'
+' Block of a shifted image (block number m = 1,...,90, note m>0)
+' start+m*96+0 DEFS 96 the shifted image (mask+graph)
+' ----------------------------------------------------------------
+dim MaskedSpritesFileSystemStart AS UInteger = 0
+' ----------------------------------------------------------------
+' MaskedSpritesFileSystemStart = address where Masked Sprites FileSystem starts
+'
+' InitMaskedSpritesFileSystem() inits the MSFS
+' Returns:
+' UInteger: value of MaskedSpritesFileSystemStart (IYW to use it)
+' ----------------------------------------------------------------
+FUNCTION FASTCALL InitMaskedSpritesFileSystem() AS UInteger
+ DIM b,c AS UByte
+ DIM l AS UInteger
+
+ c = CheckMemoryPaging()
+ if c then
+ b = GetBankPreservingRegs()
+ if b<>7 then SetBankPreservingINTs(7)
+ MaskedSpritesFileSystemStart = $db00+96*NumberofMaskedSprites
+ else
+ MaskedSpritesFileSystemStart = $db00+48*NumberofMaskedSprites
+ end if
+ l = -MaskedSpritesFileSystemStart
+ l = Int(l/96)
+ poke MaskedSpritesFileSystemStart, l ' MSFS blocks = FSB bits
+ if l=0 then STOP
+ l = 1+Int((l-1)/8)
+ poke MaskedSpritesFileSystemStart+1,l ' FSB bytes
+ if c then
+ if b<>7 then SetBankPreservingINTs(b)
+ end if
+ return MaskedSpritesFileSystemStart
+
+' Now, some assembly routines needed for next SUB/FUNCTIONs
+ASM
+; ----------------------------------------------------------------
+; Find memory addres in MSFS for the start of a block
+; Parameters:
+; L = blocknumber = n = 0,1,2,... (probably less than 200)
+; Preserves:
+; A, B, C are not used
+; Returns:
+; HL = start+n*96
+; DE = start
+; ----------------------------------------------------------------
+FindMemoryAdressForBlockInMSFS:
+ PROC
+ ld h,0
+ ld d,h
+ ld e,l ; HL = DE = n = blocknumber
+ add hl,de
+ add hl,de ; HL = DE*3
+ add hl,hl
+ add hl,hl
+ add hl,hl
+ add hl,hl
+ add hl,hl ; HL = DE*3*(2^5) = DE*96
+ ld de,(_MaskedSpritesFileSystemStart)
+ add hl,de; ; HL = start+n*96
+ ret
+ ENDP
+
+; ----------------------------------------------------------------
+; Find First Unused Block in MSFS and (optionally) Book it
+; Parameters:
+; CarryFlag = 0/1 don't/do Book it
+; Preserves:
+; C is not used
+; Returns:
+; CarryFlag = 0 if found, 1 if not found
+; A = FirstUnusedBlock if found
+; HL = start+A*96 if found
+; ----------------------------------------------------------------
+FindFirstUnusedBlockInMSFS:
+ PROC
+ LOCAL loop1, loop2, full, found, loop3, compute
+ ex af,af' ; saves CarryFlag
+ ld hl,(_MaskedSpritesFileSystemStart)
+ ld d,(hl)
+ ld e,d ; D = E = number of bits in the FSB = N
+ inc hl
+ inc hl ; HL points to the first byte in the FSB
+loop1:
+ ld a,(hl)
+ ld b,8
+loop2:
+ rrca
+ jr nc,found
+ dec e
+ jr z,full
+ djnz loop2
+ inc hl
+ jp loop1
+full: ; E = 0
+ scf ; CarryFlag=1 = ERROR
+ ret
+found: ; E = N,N-1,N-2,...,1
+ ex af,af'
+ jr nc,compute
+ ex af,af'
+ rlca ; undo last RRCA
+ or 1 ; mark this block as used
+loop3:
+ rrca ; finish the 8-bit rotation to...
+ djnz loop3 ; leave bits where they were
+ ld (hl),a ; effective booking
+compute:
+ ld a,d ; A = N
+ sub e ; A = 0,1,2,...,N-1 = blocknumber
+ ld l,a
+ call FindMemoryAdressForBlockInMSFS
+ and a ; CarryFlag=0 = OK
+ ret ; A = blocknumber, HL = start+A*96
+ ENDP
+END ASM
+
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Get Number of Free Blocks in MSFS
+' Returns:
+' UByte: number of free blocks in MSFS
+' ----------------------------------------------------------------
+FUNCTION FASTCALL GetNumberofFreeBlocksInMSFS() AS UByte
+ASM
+ PROC
+ LOCAL loop1, loop2, exit
+ call _GetBankPreservingRegs
+ cp 7 ; ZeroFlag=0 (NZ) iff _GetBankPreservingRegs returns A<>7
+ push af ; ZF and original RAM bank when FUNCTION was called
+ ld a,7
+ call nz,_SetBankPreservingINTs ; set RAM7 if _GetBankPreservingRegs returns A<>7
+ xor a ; A = 0 = number of reset bits in the FSB
+ ld hl,(_MaskedSpritesFileSystemStart)
+ ld d,(hl) ; D = number of bits in the FSB = N
+ ld e,a ; E = 0 always
+ inc hl
+ inc hl ; HL points to the first byte in the FSB
+loop1:
+ ld c,(hl)
+ ld b,8
+loop2:
+ rr c
+ ccf ; CarryFlag = 0/1 = bit in the FSB is set/reset
+ adc a,e ; A += CarryFlag = number of reset bits in the FSB
+ dec d
+ jr z,exit ; return A if all bits in FSB have been checked
+ djnz loop2
+ inc hl
+ jr loop1
+exit:
+ ex af,af' ; A' = number of free blocks in MSFS
+ pop af ; ZF and original RAM bank when FUNCTION was called
+ call nz,_SetBankPreservingINTs
+ ex af,af' ; A = number of free blocks in MSFS
+ ret
+ ENDP
+END ASM
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Register spriteImage in MSFS
+' Parameters:
+' UInteger: address where spriteImage begins
+' Returns:
+' UInteger: registry number in the MSFS = start+n*96+16 if OK
+' 0 if not OK
+' ----------------------------------------------------------------
+FUNCTION FASTCALL RegisterSpriteImageInMSFS(spriteImageAddr AS UInteger) AS UInteger
+ASM
+ PROC
+ LOCAL full, exit
+ call _GetBankPreservingRegs
+ cp 7 ; ZeroFlag=0 (NZ) iff _GetBankPreservingRegs returns A<>7
+ push af ; ZF and original RAM bank when FUNCTION was called
+ push hl ; spriteImageAddr
+ ld a,7
+ call nz,_SetBankPreservingINTs ; set RAM7 if it was not set
+ scf
+ call FindFirstUnusedBlockInMSFS ; and book it (SCF)
+ jr c,full
+ ld bc,16
+ add hl,bc
+ push hl ; HL = start+A*96+16
+ ld d,h
+ ld e,l
+ inc de ; DE = start+A*96+17
+ dec bc ; BC = 15
+ ld (hl),0
+ ldir ; reset RAM from start+A*96+16 to start+A*96+31 (incl.)
+ pop hl ; HL = start+A*96+16
+ ld b,h
+ ld c,l ; BC = start+A*96+16
+ ld (hl),e ; DE = start+A*96+32 (after last LDIR)
+ inc hl
+ ld (hl),d ; start+A*96+16-17 DEFW start+A*96+32
+ pop hl ; spriteImageAddr
+ push bc ; BC = start+A*96+16
+ ld bc,64
+ ldir ; transfer from spriteImageAddr to start+A*96+32
+ pop hl ; HL = start+A*96+16
+exit:
+ pop af ; ZF and original RAM bank when FUNCTION was called
+ call nz,_SetBankPreservingINTs
+ ret
+full:
+ pop hl ; spriteImageAddr
+ ld hl,0
+ jr exit
+ ENDP
+END ASM
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Register spriteGraph and spriteMask in MSFS
+' (useful when different Graphs share the same Mask)
+'
+' Data in spriteGraph and spriteMask MUST be in "putchars format"
+'
+' Parameters:
+' UInteger: address where spriteGraph begins
+' UInteger: address where spriteMask begins
+' Returns:
+' UInteger: registry number in the MSFS = start+n*96+16 if OK
+' 0 if not OK
+' ----------------------------------------------------------------
+FUNCTION FASTCALL RegisterSpriteGraphAndMaskInMSFS(spriteGraphAddr AS UInteger,spriteMaskAddr AS UInteger) AS UInteger
+ASM
+ PROC
+ LOCAL full, exit, loop1, loop2
+ pop de ; returnAddr
+ pop bc ; spriteMaskAddr
+ push de ; returnAddr
+; stack is empty. Now we will push data to be preserved
+ exx ; HL' = spriteGraphAddr, BC' = spriteMaskAddr
+ call _GetBankPreservingRegs
+ cp 7 ; ZeroFlag=0 (NZ) iff _GetBankPreservingRegs returns A<>7
+ push af ; ZF and original RAM bank when FUNCTION was called
+ ld a,7
+ call nz,_SetBankPreservingINTs ; set RAM7 if it was not set
+ scf
+ call FindFirstUnusedBlockInMSFS ; and book it (SCF)
+ jr c,full
+ ld bc,16
+ add hl,bc
+ push hl ; HL = start+A*96+16
+ ld d,h
+ ld e,l
+ inc de ; DE = start+A*96+17
+ dec bc ; BC = 15
+ ld (hl),0
+ ldir ; reset RAM from start+A*96+16 to start+A*96+31 (incl.)
+ pop hl ; HL = start+A*96+16
+ ld (hl),e ; DE = start+A*96+32 (after last LDIR)
+ inc hl
+ ld (hl),d ; start+A*96+16-17 DEFW start+A*96+32
+ dec hl
+ push hl ; return value HL = start+A*96+16
+ push de ; DE = start+A*96+32 DEFS 64 the unshifted image (mask+graph)
+ exx ; HL = spriteGraphAddr, BC = spriteMaskAddr
+ pop de ; DE = DEFS 64 the unshifted image (mask+graph)
+ push ix
+ push de ; DE = start+A*96+32
+ ld ixl,16
+loop1:
+ ld a,(bc) ; mask
+ ld (de),a
+ inc bc
+ inc de
+ ld a,(hl) ; graph
+ ld (de),a
+ inc hl
+ inc de
+ inc de
+ inc de
+ dec ixl
+ jp nz,loop1
+ pop de ; DE = start+A*96+32
+ inc de
+ inc de ; DE = start+A*96+32 +2
+ ld ixl,16
+loop2:
+ ld a,(bc) ; mask
+ ld (de),a
+ inc bc
+ inc de
+ ld a,(hl) ; graph
+ ld (de),a
+ inc hl
+ inc de
+ inc de
+ inc de
+ dec ixl
+ jp nz,loop2
+ pop ix
+ pop hl ; return value HL = start+A*96+16
+exit:
+ pop af ; ZF and original RAM bank when FUNCTION was called
+ call nz,_SetBankPreservingINTs
+ ret
+full:
+ ld hl,0
+ jr exit
+ ENDP
+END ASM
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Save background and Draw sprite registered in the MSFS
+' Parameters:
+' UByte: X coordinate (0:left to 240:right)
+' UByte: Y coordinate (0:up to 176:down)
+' UInteger: address where background will be saved
+' UInteger: registry number in the MSFS for the spriteImage
+' ----------------------------------------------------------------
+SUB FASTCALL SaveBackgroundAndDrawSpriteRegisteredInMSFS(X AS UByte, Y AS UByte, backgroundAddr AS UInteger, spriteImageReg AS UInteger)
+ASM
+ PROC
+ LOCAL full, makeShiftedImage, loopMSI, loopMSI1
+ LOCAL useShiftedImage, loopUSI, branchUSI, exitUSI
+ LOCAL noshift, loopNS, branchNS, exitNS
+ ; A = X
+ pop de ; returnAddr
+ exx
+ pop bc ; B = Y
+ ld c,a ; C = X
+; BEGIN code from https://skoolkid.github.io/rom/asm/22AA.html
+ rlca
+ rlca
+ rlca ; A = %c4c3c2c1c0c7c6c5
+ xor b
+ and %11000111
+ xor b ; A = %c4c3b5b4b3c7c6c5
+ rlca
+ rlca
+ ld e,a ; E = %b5b4b3c7c6c5c4c3
+ ld a,b
+ and %11111000
+ rra
+ rra
+ rra ; A = %.0.0.0b7b6b5b4b3
+ xor b
+ and %11111000
+ xor b
+ ld d,a ; D = %.0.0.0b7b6b2b1b0
+; END code from https://skoolkid.github.io/rom/asm/22AA.html
+ ld hl,(.core.SCREEN_ADDR)
+ add hl,de
+ ex de,hl ; DE = screenAddr where drawing will start
+ ld a,c; ; A = X, keep it secret, keep it safe
+ pop bc ; BC = backgroundAddr
+ pop hl ; HL = spriteImageReg
+ exx
+ push de ; returnAddr
+ exx
+; stack is empty. Now we will push data to be preserved
+ push ix
+ ld ixh,16 ; 16 scanlines
+ and 7
+ jr z,noshift ; jump if X is a multiple of 8 (unlikely)
+ push de ; DE = screenAddr where drawing will start
+ push bc ; BC = backgroundAddr
+ ld b,0
+ ld c,a ; BC = A = 1,2,3,4,5,6,7
+ add hl,bc
+ add hl,bc ; HL = start+n*96+16+C*2 DEFW address of block for image shifted C pixels, or 0 if not used
+ ld e,(hl)
+ inc hl
+ ld d,(hl) ; DE = address for image shifted C pixels, or 0 if not used
+ ld a,d
+ or e
+ jp z,makeShiftedImage
+;UseShiftedImage (USI)
+useShiftedImage:
+ ex de,hl ; HL = address for image shifted C pixels
+ pop bc ; BC = backgroundAddr
+ pop de ; DE = screenAddr where drawing will start
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even
+ jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI
+ ld a,i ; read IFF2 again to ensure that IFF2=0=DI
+1: ex af,af'
+ di
+ ld (exitUSI+1),sp
+ ld sp,hl
+#endif
+loopUSI:
+ ld a,(de) ; screen
+ ld (bc),a; ; save
+ inc bc
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ pop hl
+ and l ; mask
+ or h ; graph
+#else
+ and (hl); ; mask
+ inc l
+ or (hl) ; graph
+ inc hl
+#endif
+ ld (de),a ; 1st byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a; ; save
+ inc bc
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ pop hl
+ and l ; mask
+ or h ; graph
+#else
+ and (hl); ; mask
+ inc l
+ or (hl) ; graph
+ inc hl
+#endif
+ ld (de),a ; 2nd byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a; ; save
+ inc bc
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ pop hl
+ and l ; mask
+ or h ; graph
+#else
+ and (hl); ; mask
+ inc l
+ or (hl) ; graph
+ inc hl
+#endif
+ ld (de),a ; 3rd byte done
+ dec e
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branchUSI ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ dec ixh
+ jp nz,loopUSI
+exitUSI:
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ ld sp,$1234
+ pop ix
+ ex af,af'
+ ret po ; Return with DI if IFF2=0=DI at the beginning
+ ei ; Return with EI if IFF2=1=EI at the beginning
+ ret
+#else
+ pop ix
+ ret
+#endif
+branchUSI:
+ MaskedSprites_NEXT_ROW
+ dec ixh
+ jp nz,loopUSI
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ jp exitUSI
+#else
+ pop ix
+ ret
+#endif
+;NoShift (NS)
+noshift:
+ ld a,(hl) ; HL = spriteImageReg
+ inc hl
+ ld h,(hl)
+ ld l,a ; HL = start of unshifted image
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even
+ jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI
+ ld a,i ; read IFF2 again to ensure that IFF2=0=DI
+1: ex af,af'
+ di
+ ld (exitNS+1),sp
+ ld sp,hl
+#endif
+loopNS:
+ ld a,(de) ; screen
+ ld (bc),a; ; save
+ inc bc
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ pop hl
+ and l ; mask
+ or h ; graph
+#else
+ and (hl); ; mask
+ inc l
+ or (hl) ; graph
+ inc hl
+#endif
+ ld (de),a ; 1st byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ pop hl
+ and l ; mask
+ or h ; graph
+#else
+ and (hl); ; mask
+ inc l
+ or (hl) ; graph
+ inc hl
+#endif
+ ld (de),a ; 2nd byte done
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branchNS ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ dec ixh
+ jp nz,loopNS
+exitNS:
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ ld sp,$1234
+ pop ix
+ ex af,af'
+ ret po ; Return with DI if IFF2=0=DI at the beginning
+ ei ; Return with EI if IFF2=1=EI at the beginning
+ ret
+#else
+ pop ix
+ ret
+#endif
+branchNS:
+ MaskedSprites_NEXT_ROW
+ dec ixh
+ jp nz,loopNS
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ jp exitNS
+#else
+ pop ix
+ ret
+#endif
+;MakeShiftedImage (MSI)
+makeShiftedImage:
+ ld a,8
+ sub c ; C = X MOD 8 = 1,2,3,4,5,6,7 to the right
+ ld ixl,a ; IXl = 8 - C = 7,6,5,4,3,2,1 to the left
+ push hl ; HL = start+n*96+16+C*2 + 1
+ scf
+ call FindFirstUnusedBlockInMSFS ; and book it (SCF)
+ jr c,full
+ ex de,hl ; DE = start+m*96 = address for the shiftedimage-to-be
+ pop hl ; HL = start+n*96+16+C*2 + 1
+ ld (hl),d
+ dec hl
+ ld (hl),e; ; HL = start+n*96+16+C*2 DEFW address for the shifted image
+ ld b,0
+ sbc hl,bc
+ sbc hl,bc
+ ld a,(hl) ; HL = spriteImageReg
+ inc hl
+ ld h,(hl)
+ ld l,a ; HL = start of unshifted image
+ push de ; DE = address for image shifted C pixels
+ push de ; two PUSH because we will POP one just before useShiftedImage
+ exx
+ pop hl ; HL' = address for the shiftedimage-to-be
+ exx
+loopMSI:
+ ld a,(hl) ; mask1
+ inc hl
+ ld c,(hl) ; graph1
+ inc hl
+ ld d,(hl) ; mask2
+ inc hl
+ ld e,(hl) ; graph2
+ inc hl
+ push hl ; spriteImageAddr += 4
+ ld hl,$FF00 ; H = 255 , L = 0
+ ld b,ixl
+loopMSI1:
+ sll d ; 8; ShiftLeftLogical injects a 1 in bit0 of D
+ rla ; 4
+ rl h ; 8
+ sla e ; 8; ShiftLeftArithmetic injects a 0 in bit0 of E
+ rl c ; 8
+ rl l ; 8
+ djnz loopMSI1; 8+4+8+8+8+8 = 44 Ts
+ ld b,a
+ push de ; D,E = mask,graph 3rd byte
+ push bc ; B,D = mask,graph 2nd byte
+ push hl ; H,L = mask,graph 1st byte
+ exx
+ pop de ; D',E' = mask,graph 1st byte
+ ld (hl),d
+ inc hl
+ ld (hl),e
+ inc hl
+ pop de ; D',E' = mask,graph 2nd byte
+ ld (hl),d
+ inc hl
+ ld (hl),e
+ inc hl
+ pop de ; D',E' = mask,graph 3rd byte
+ ld (hl),d
+ inc hl
+ ld (hl),e
+ inc hl ; HL' += 6 in this loop
+ exx
+ pop hl ; spriteImageAddr
+ dec ixh
+ jp nz,loopMSI
+ pop de ; DE = address for image shifted C pixels
+ ld ixh,16 ; 16 scanlines
+ jp useShiftedImage
+full:
+ pop hl ; HL = start+n*96+16+C*2 + 1
+ pop bc ; BC = backgroundAddr
+ pop de ; DE = screenAddr where drawing will start
+ pop ix
+ ret
+ ENDP
+END ASM
+END SUB
+
+
+#endif
+
diff --git a/src/lib/arch/zx48k/stdlib/hex.bas b/src/lib/arch/zx48k/stdlib/hex.bas
index be29fea23..a92f4f175 100644
--- a/src/lib/arch/zx48k/stdlib/hex.bas
+++ b/src/lib/arch/zx48k/stdlib/hex.bas
@@ -21,10 +21,10 @@ REM Avoid recursive / multiple inclusion
' function HEX
'
' Parameters:
-' num : 32 bit unsigned integer numbre
+' num : 32 bit unsigned integer number
'
' Returns:
-' 4 chars str containing the HEX string representation
+' 8 chars str containing the HEX string representation
' ----------------------------------------------------------------
function FASTCALL hex(num as ULong) as String
asm
diff --git a/src/lib/arch/zx48k/stdlib/mj/fourspriter.bas b/src/lib/arch/zx48k/stdlib/mj/fourspriter.bas
index 1bb8beecd..0fa7e1793 100644
--- a/src/lib/arch/zx48k/stdlib/mj/fourspriter.bas
+++ b/src/lib/arch/zx48k/stdlib/mj/fourspriter.bas
@@ -8,7 +8,7 @@
'' -------------------------------------------------------------------------
'' Use/modify as you like. You may want to try Fourspriter MK3 by Britlion,
'' which works differently and is really faster. Check ZX Basic forums at
-'' http://www.boriel.com/forum/how-to-tutorials/topic400.html
+'' https://forum.boriel.com/how-to-tutorials/topic400.html
'' -------------------------------------------------------------------------
'' -------------------------------------------------------------------------
diff --git a/src/lib/arch/zx48k/stdlib/play.bas b/src/lib/arch/zx48k/stdlib/play.bas
new file mode 100644
index 000000000..7d19d43e9
--- /dev/null
+++ b/src/lib/arch/zx48k/stdlib/play.bas
@@ -0,0 +1,661 @@
+' ----------------------------------------------------------------------
+' This file is released under the MIT License
+'
+' Copyright (C) 2026
+' by Oleg S. Kostenko (a.k.a. Ollibony)
+' ----------------------------------------------------------------------
+
+#pragma once
+
+#pragma push(explicit)
+#pragma push(strict)
+
+#pragma explicit = true
+#pragma strict = true
+
+' ---------------------------------------------------------------------------------------------------------------------
+' Plays the given MML strings on the AY music chip.
+' The syntax is compatible with the Sinclair Basic Play routine.
+'
+' The documentation can be found here: https://fizyka.umk.pl/~jacek/zx/doc/man128/sp128p09.html
+'
+' More accurate (although not as well structured) documentation can be found in ZX Spectrum +3 manual here:
+' https://zxspectrumvault.github.io/Manuals/Hardware/SpectrumPlus3Manual.html (see Chapter 8 Part 19)
+'
+' This is work in progress.
+
+' The following commands are already implemented:
+' - cdefgab, CDEFGAB - gives pitch of note within current octave range
+' - $ - flattens note following it
+' - # - sharpens note following it
+' - & - denotes a rest
+' - 1-12 - sets length of notes
+' - _ - creates tied notes (sums multiple durations)
+' - O - followed by a number 0 to 8 sets current octave range
+' - V - followed by a number 0 to 15 sets volume of notes
+' - T - followed by a number 60 to 240 sets tempo of music
+' - N - separates two numbers (actually, any unexpected character does this, including space)
+' - () - specifies that the enclosed phrase must be repeated
+' - H - specifies that the Play command must stop
+' - W - followed by a number 0 to 7 sets volume effect
+' - X - followed by a number 0 to 65535 sets duration of volume effect
+' - U - turns on volume effect in the channel
+' - M - followed by a number from 0 to 63 specifies the channels mixer mode
+'
+' The following commands are not implemented yet:
+' - !! - comments
+' - Y, Z - MIDI control (also you can't now pass more than 3 parameters to Play).
+'
+' Notes:
+'
+' - Unlike Sinclair Basic Play routine, this one doesn't insert tiny pauses between adjacent notes.
+' I consider this to be a feature, rather than a bug.
+'
+' - There is no proper error handling implemented. In particular:
+' - Unknown commands are silently ignored.
+' - Out-of-range numbers cause undefined behavior.
+' - An opening bracket is silently ignored if nesting level limit was reached (brackets can be maximum 4 levels deep).
+'
+' - This routine is more flexible in the way it parses commands than Sinclair Basic Play routine.
+' Some combinations that give errors in Sinclair Basic, will play fine in this implementation.
+'
+' - This sub tends to provide more accurate timings than the original Sinclair Basic Play routine.
+' However, perfect timing is not guaranteed, it may fluctuate depending on the complexity of the melody.
+'
+' - There can be subtle difference in behaviour between this sub and Sinclair Play,
+' especially in undocumented edge cases (such as using ties together with triplets).
+'
+' - This sub disables interrupts at the start, and enables them in the end,
+' regardless of whether they were enabled or not before.
+'
+' - This sub assumes the standard ZX Spectrum CPU speed of approximately 3.5 MHz.
+'
+' - The strings are passed by value and thus are copied on the routine invocation.
+' The memory-effective version of this routine is yet to be implemented.
+'
+' - The compiler gives warning `[W150] Parameter 'microticks' is never used`.
+' This is false positive and, unfortunately, cannot be suppressed on library level.
+'
+' - For some obscure reason, this sub doesn't work if compiler optimization level is set to 1 or lower.
+' The default optimization level (2) is fine.
+'
+' - The `--enable-break` compiler option causes play slowdown. The sound chip is not silenced when Break is pressed,
+' so it may feel as if the music 'freezed'.
+' ---------------------------------------------------------------------------------------------------------------------
+declare sub Play(channel0 as string, channel1 as string = "", channel2 as string = "")
+
+
+' Implementation ------------------------------------------------------------------------------------------------------
+
+' How many ticks there are in a bar (a whole note).
+' A tick is a single iteration of the main processing loop.
+const _Play_TicksPerBar as ubyte = 96
+
+const _Play_NotesPerOctave as ubyte = 12
+const _Play_TotalOctaves as ubyte = 9
+
+' Maps note length to the corresponding number of ticks.
+dim _Play_NoteLengthsInTicks(1 to 12) as ubyte => { _
+ _Play_TicksPerBar / 16, _ '1 - semi-quaver
+ _Play_TicksPerBar / 16 * 1.5, _ '2 - dotted semi-quaver
+ _Play_TicksPerBar / 8, _ '3 - quaver
+ _Play_TicksPerBar / 8 * 1.5, _ '4 - dotted quaver
+ _Play_TicksPerBar / 4, _ '5 - crotchet
+ _Play_TicksPerBar / 4 * 1.5, _ '6 - dotted crotchet
+ _Play_TicksPerBar / 2, _ '7 - minim
+ _Play_TicksPerBar / 2 * 1.5, _ '8 - dotted minim
+ _Play_TicksPerBar, _ '9 - semi-breve
+ _Play_TicksPerBar / 24, _ '10 - triplet semi-quaver
+ _Play_TicksPerBar / 12, _ '11 - triplet quaver
+ _Play_TicksPerBar / 6 _ '12 - triplet crotchet
+}
+
+' Divider values that need to be sent to the audio chip registers to play the notes.
+' Note that the lowest notes in octave 0 are unplayable because of 12-bit overflow and probably wrong notes will be
+' played instead of them.
+' TODO: replace them with maximum possible values or zeros? See how it's done in Sinclair Play.
+' TODO: in Sinclair Play it is possible to play notes in higher octaves (using several sharps in a row).
+' Need to add more values to the table.
+dim _Play_NoteDividers(0 to _Play_NotesPerOctave * _Play_TotalOctaves - 1) as uinteger = { _
+_ ' C C# D D# E F F# G G# A A# B
+ 6779, 6398, 6039, 5700, 5380, 5078, 4793, 4524, 4270, 4031, 3804, 3591, _ 'octave 0
+ 3389, 3199, 3020, 2850, 2690, 2539, 2397, 2262, 2135, 2015, 1902, 1795, _ 'octave 1
+ 1695, 1600, 1510, 1425, 1345, 1270, 1198, 1131, 1068, 1008, 951, 898, _ 'octave 2
+ 847, 800, 755, 713, 673, 635, 599, 566, 534, 504, 476, 449, _ 'octave 3
+ 424, 400, 377, 356, 336, 317, 300, 283, 267, 252, 238, 224, _ 'octave 4
+ 212, 200, 189, 178, 168, 159, 150, 141, 133, 126, 119, 112, _ 'octave 5
+ 106, 100, 94, 89, 84, 79, 75, 71, 67, 63, 59, 56, _ 'octave 6
+ 53, 50, 47, 45, 42, 40, 37, 35, 33, 31, 30, 28, _ 'octave 7
+ 26, 25, 24, 22, 21, 20, 19, 18, 17, 16, 15, 14 _ 'octave 8
+}
+
+' Maps ascii code of a letter to the corresponding index of `_Play_NoteDividers` (octave 0).
+dim _Play_NoteIndexes(code("A") to code("G")) as ubyte = { _
+ /'A'/ 9, _
+ /'B'/ 11, _
+ /'C'/ 0, _
+ /'D'/ 2, _
+ /'E'/ 4, _
+ /'F'/ 5, _
+ /'G'/ 7 _
+}
+
+' Maps envelope shape number to its hardware equivalent.
+dim _Play_EnvelopeShapes(0 to 7) as ubyte = { _
+ /'0'/ %0000, _
+ /'1'/ %0100, _
+ /'2'/ %1011, _
+ /'3'/ %1101, _
+ /'4'/ %1000, _
+ /'5'/ %1100, _
+ /'6'/ %1110, _
+ /'7'/ %1010 _
+}
+
+' Pointer to the current channel context.
+' Made global for better performance, and also because it would be problematic to access it from nested subs if it were
+' local (see 'Implementation note' on `Play`).
+dim _Play_ContextPtr as uinteger
+
+' Switches to the first channel context.
+#define _PLAY_CTX_FIRST_CHANNEL() let _Play_ContextPtr = ChannelContextBufferPtr
+
+' Swithes to the next channel context.
+#define _PLAY_CTX_NEXT_CHANNEL() let _Play_ContextPtr = _Play_ContextPtr + ChannelContextSize
+
+' Gets the value of the given `type` at the given `offset` of the current channel context.
+#define _PLAY_CTX_GET(type, offset) (peek(type, _Play_ContextPtr + (offset)))
+
+' Sets the given `value` of the given `type` to the given `offset` of the current channel context.
+#define _PLAY_CTX_SET(type, offset, value) poke type _Play_ContextPtr + (offset), (value)
+
+' Arithmetically adds the given `value` to the value of the given `type` stored at the given `offset`
+' of the current channel context.
+#define _PLAY_CTX_ADD(type, offset, value) _PLAY_CTX_SET(type, offset, _PLAY_CTX_GET(type, offset) + (value))
+
+' Write the value to the given register of the sound chip.
+#define _PLAY_WRITE_TO_REGISTER(register, value) out $fffd, (register) : out $bffd, (value)
+
+
+' Main sub.
+'
+' Implementation notes:
+'
+' If you want to extend the inner subs or functions, or add new ones,
+' please beware that the current compiler version (v1.19.0-beta7 at the time of writing)
+' doesn't support accessing outer local vars from the inner sub/function,
+' if the inner sub/function has its own vars or params.
+'
+' The readability of the code was in many ways sacrificed in favor of performance,
+' which is necessary to achieve accurate timings.
+'
+' TODO: add sub variants that accept strings byref, and that accept an array.
+' TODO: check valid ranges of command parameters, handle integer overflow/underflow
+'
+sub Play(channel0 as string, channel1 as string = "", channel2 as string = "")
+
+ const CpuCyclesPerSecond as ulong = 3546900
+ const CpuCyclesPerMicrotick as ubyte = 27 ' see the `Wait` sub
+ const BeatsPerBar as ubyte = 4
+ const SecondsPerMinute as ubyte = 60
+ const ChannelCount as ubyte = 3
+
+ const DefaultTempo as ubyte = 120
+ const DefaultOctave as ubyte = 5
+ const DefaultNoteLength as ubyte = 5
+ const DefaultVolume as ubyte = 15
+ const DefaultMixer as ubyte = %11111000
+ const DefaultEnvelopeShape as ubyte = 0
+
+ ' Special volume value that enables hardware envelope.
+ const VolumeEnvelopeOn as ubyte = 16
+
+ ' General processing overhead compensation. Applied to every `Wait` invocation.
+ ' Determined experimentally.
+ ' TODO: adjust if needed after everything is implemented.
+ const TickGeneralOverheadInMicroticks as uinteger = 90
+
+ ' Overhead compensation for commands processing of a single channel.
+ ' Applied only on those ticks when there are commands processed for the channel.
+ ' Determined experimentally.
+ ' TODO: adjust if needed after everything is implemented.
+ const TickChannelCommandsOverheadInMicroticks as uinteger = 140
+
+ ' Channel numbers are zero-based, because it's better in terms of performance (less arithmetics in runtime needed).
+ const MaxChannel as ubyte = ChannelCount - 1
+
+ ' Maximum nesting level for brackets.
+ ' Initial level is 0. An opening bracket increases the level, a closing bracket decreases the level.
+ const MaxNestingLevel as ubyte = 4
+
+ ' Size of a single channel context in bytes. Don't forget to increase this if you add more context fields.
+ ' Note: this is used in macro `_PLAY_CTX_NEXT_CHANNEL`.
+ const ChannelContextSize as ubyte = 26
+
+ ' Channel context data is stored here.
+ dim ChannelContextBuffer(0 to ChannelContextSize * ChannelCount - 1) as ubyte
+
+ ' Pointer to context data.
+ ' Note: this is used in macro `_PLAY_CTX_FIRST_CHANNEL`.
+ dim ChannelContextBufferPtr as uinteger
+ ChannelContextBufferPtr = @ChannelContextBuffer(0)
+
+ ' Offsets of fields in a channel context.
+ ' If you add more fields, don't forget to increase `ChannelContextSize`.
+ const _CharPtr as ubyte = 0 ' (uinteger) Pointer to the current character in the channel string.
+ const _StringEndPtr as ubyte = 2 ' (uinteger) Pointer to the first byte after the last char of the channel
+ ' string.
+ const _TickBackCounter as ubyte = 4 ' (uinteger) How many ticks to wait before proceeding to the next command
+ ' in the channel string. Zero means we need to proceed now.
+ const _PrimaryNoteLengthInTicks as ubyte = 6 ' (uinteger) Current note length in ticks.
+ const _ActualNoteLengthInTicks as ubyte = 8 ' (uinteger) Actual note length in ticks.
+ ' Mostly the same as `_PrimaryNoteLengthInTicks`,
+ ' but may differ for triplets and ties.
+ const _ResetNoteLengthBackCount as ubyte = 10 ' (ubyte) How many notes left to play before actual note length must
+ ' be reset to primary note length.
+ const _BaseDividerIndex as ubyte = 11 ' (ubyte) Divider index (see `_Play_NoteDividers`) that corresponds to
+ ' note C of the current octave.
+ const _SemitoneAdjustment as ubyte = 12 ' (byte) How many semitones to add or subtract from the next note.
+ const _FinishedFlag as ubyte = 13 ' (ubyte) If nonzero, then the channel has finished playing.
+ const _Volume as ubyte = 14 ' (ubyte) Current volume.
+ ' If equals to `VolumeEnvelopeOn`, the envelope generator is used.
+ const _NestingLevel as ubyte = 15 ' (ubyte) Current brackets nesting level.
+ const _ReturnPtrs as ubyte = 16 ' (uinteger * (MaxNestingLevel+1))
+ ' Stack of pointers to return to on closing brackets.
+ ' The zeroth element always points to the string start,
+ ' for infinite repeat when there is an unpaired closing bracket.
+
+ ' Current tempo as beats per minute. A 'beat' is a 1/4-length note.
+ dim Tempo as ubyte
+
+ ' Current tempo as microticks per tick.
+ ' For 'microtick' definition, see the `CpuCyclesPerMicrotick` const.
+ ' For 'tick' definition, see the `_Play_TicksPerBar` const.
+ dim MicroticksPerTick as uinteger
+
+ ' Current hardware envelope shape.
+ ' See `_Play_EnvelopeShapes` for possible values.
+ dim EnvelopeShape as ubyte
+
+ dim LastChar as ubyte ' Last char read by `ReadChar` sub.
+ dim LastNumber as uinteger ' Last number read by `ReadNumber` sub.
+
+ ' Reads a char from the current channel string and puts it to `LastChar` variable.
+ ' Puts 0 if there's nothing left to read.
+ ' This is a sub, not a function, for performance reasons.
+ sub ReadChar
+ if _PLAY_CTX_GET(uinteger, _CharPtr) = _PLAY_CTX_GET(uinteger, _StringEndPtr) then
+ LastChar = 0
+ return
+ end if
+
+ LastChar = peek(_PLAY_CTX_GET(uinteger, _CharPtr))
+ _PLAY_CTX_ADD(uinteger, _CharPtr, 1)
+ end sub
+
+ ' Reads the number from the current channel string and puts it in `LastNumber` variable.
+ ' Puts 0 if the number is unreadable.
+ sub ReadNumber
+ LastNumber = 0
+
+ do
+ ReadChar
+
+ if LastChar >= code("0") and LastChar <= code("9") then
+ LastNumber = LastNumber * 10 + LastChar - code("0")
+ else
+ ' The number has ended.
+ if LastChar <> 0 then
+ ' Step back if the string has not ended.
+ _PLAY_CTX_ADD(uinteger, _CharPtr, -1)
+ end if
+ exit do
+ end if
+ loop
+ end sub
+
+ ' Sets octave for the current channel.
+ sub SetOctave(octave as ubyte)
+ _PLAY_CTX_SET(ubyte, _BaseDividerIndex, octave * _Play_NotesPerOctave)
+ end sub
+
+ ' Sets `MicroticksPerTick` according to the current `Tempo` value.
+ sub UpdateMicroticksPerTick
+ MicroticksPerTick = _
+ CpuCyclesPerSecond * SecondsPerMinute * BeatsPerBar _
+ / CpuCyclesPerMicrotick / _Play_TicksPerBar / Tempo
+ end sub
+
+ ' Waits for the given amount of microticks.
+ ' One microtick is 27 CPU cycles (if you change it, also change `CpuCyclesPerMicrotick` const).
+ ' TODO: is it possible to suppress the 'unused parameter' warning?
+ sub fastcall Wait(microticks as uinteger)
+ asm
+ proc
+ local loop
+
+ ld bc, 1 ; bc = 1
+ or a ; reset carry flag
+ loop:
+ sbc hl, bc ; microticks = microticks - bc ; 15 cycles
+ jr nz, loop ; if microticks <> 0 then goto loop ; 12 cycles
+
+ endp
+ end asm
+ end sub
+
+ ' Set tone pitch on the sound chip for a channel.
+ sub SetChipTonePitchDivider(channel as ubyte, divider as uinteger)
+ _PLAY_WRITE_TO_REGISTER(channel * 2, divider band $ff)
+ _PLAY_WRITE_TO_REGISTER(channel * 2 + 1, divider >> 8)
+ end sub
+
+ ' Set pitch for noise generator of the sound chip.
+ sub SetChipNoisePitchDivider(divider as ubyte)
+ _PLAY_WRITE_TO_REGISTER(6, divider)
+ end sub
+
+ ' Set volume on the sound chip for a channel. A special value `VolumeEnvelopeOn` means use envelope generator.
+ sub SetChipVolume(channel as ubyte, volume as ubyte)
+ _PLAY_WRITE_TO_REGISTER(channel + 8, volume)
+ end sub
+
+ ' Sets envelope shape on the sound chip.
+ ' See `_Play_EnvelopeShapes` for possible values.
+ sub SetChipEnvelopeShape(shape as ubyte)
+ _PLAY_WRITE_TO_REGISTER(13, shape)
+ end sub
+
+ ' Sets envelope period on the sound chip.
+ sub SetChipEnvelopePeriod(period as uinteger)
+ _PLAY_WRITE_TO_REGISTER(11, period band $ff)
+ _PLAY_WRITE_TO_REGISTER(12, period >> 8)
+ end sub
+
+ ' Set mixer mode on the sound chip.
+ ' Bits 0, 1, 2 - if zero, enable tone on channels A, B, C correspondingly
+ ' Bits 3, 4, 5 - if zero, enable noise on channels A, B, C correspondingly
+ ' Bits 6, 7 - i/o ports, better set 1 there for now. (MIDI control? I don't know)
+ sub SetChipMixer(value as ubyte)
+ _PLAY_WRITE_TO_REGISTER(7, value)
+ end sub
+
+ ' If macro `_PLAY_BENCHMARK_MODE` is defined, then interrupts are not disabled, and the system timer is used to
+ ' measure the duration of play. The duration in frames is printed on the screen after playing.
+ ' Note that interrupts add overhead and inaccuracies to timings, so this mode is not intended for fine-tuning
+ ' timings. This should only be used for differential analysis of code optimization efficiency.
+ #ifndef _PLAY_BENCHMARK_MODE
+ asm
+ di
+ end asm
+ #endif
+
+ _PLAY_CTX_FIRST_CHANNEL()
+ _PLAY_CTX_SET(uinteger, _CharPtr, @channel0)
+
+ _PLAY_CTX_NEXT_CHANNEL()
+ _PLAY_CTX_SET(uinteger, _CharPtr, @channel1)
+
+ _PLAY_CTX_NEXT_CHANNEL()
+ _PLAY_CTX_SET(uinteger, _CharPtr, @channel2)
+
+ dim channel as ubyte
+ dim strLen as uinteger
+ dim ptr as uinteger
+
+ _PLAY_CTX_FIRST_CHANNEL()
+
+ for channel = 0 to MaxChannel
+ ' We need low-level access to the strings to achieve good performance.
+
+ ' dereference the pointer to the heap
+ ptr = peek(uinteger, _PLAY_CTX_GET(uinteger, _CharPtr))
+
+ ' read the string length
+ strLen = peek(uinteger, ptr)
+
+ ' adjust the pointer so it points to the first char
+ ptr = ptr + 2
+
+ ' store pointers to the string start and end
+ _PLAY_CTX_SET(uinteger, _CharPtr, ptr)
+ _PLAY_CTX_SET(uinteger, _ReturnPtrs + 0, ptr)
+ _PLAY_CTX_SET(uinteger, _StringEndPtr, ptr + strLen)
+
+ SetOctave DefaultOctave
+ _PLAY_CTX_SET(ubyte, _Volume, DefaultVolume)
+ _PLAY_CTX_SET(uinteger, _PrimaryNoteLengthInTicks, _Play_NoteLengthsInTicks(DefaultNoteLength))
+ _PLAY_CTX_SET(uinteger, _ActualNoteLengthInTicks, _Play_NoteLengthsInTicks(DefaultNoteLength))
+ _PLAY_CTX_SET(uinteger, _TickBackCounter, 0)
+ _PLAY_CTX_SET(byte, _SemitoneAdjustment, 0)
+ _PLAY_CTX_SET(ubyte, _ResetNoteLengthBackCount, 0)
+ _PLAY_CTX_SET(ubyte, _FinishedFlag, 0)
+ _PLAY_CTX_SET(ubyte, _NestingLevel, 0)
+
+ _PLAY_CTX_NEXT_CHANNEL()
+ next channel
+
+ EnvelopeShape = DefaultEnvelopeShape
+ Tempo = DefaultTempo
+ UpdateMicroticksPerTick
+ SetChipMixer DefaultMixer
+
+ dim processedChannels as ubyte
+ dim finishedChannels as ubyte
+ dim lengthInTicks as uinteger
+ dim dividerIndex as ubyte
+ dim nestingLevel as ubyte
+ dim returnPtr as uinteger
+ dim returnPtrOffset as uinteger
+ dim halt as ubyte = 0
+ dim volume as ubyte
+ dim mustInitEnvelope as ubyte
+
+ #ifdef _PLAY_BENCHMARK_MODE
+ dim SysFrames as uinteger at $5c78
+ dim startTime as uinteger = SysFrames
+ #endif
+
+ do
+ finishedChannels = 0
+ processedChannels = 0
+ mustInitEnvelope = 0
+
+ _PLAY_CTX_FIRST_CHANNEL()
+
+ for channel = 0 to MaxChannel
+ if _PLAY_CTX_GET(uinteger, _TickBackCounter) = 0 then
+ do
+ ReadChar
+
+ if LastChar = 0 then
+ ' This channel has finished playing.
+ _PLAY_CTX_SET(ubyte, _FinishedFlag, 1)
+ ' While other channels are still playing, this one will do rests.
+ SetChipVolume channel, 0
+ exit do
+
+ else if LastChar = code("&") then
+ SetChipVolume channel, 0
+ exit do
+
+ else if (LastChar >= code("a") and LastChar <= code("g")) _
+ or (LastChar >= code("A") and LastChar <= code("G")) then
+
+ dividerIndex = _PLAY_CTX_GET(ubyte, _BaseDividerIndex)
+
+ if LastChar >= code("a") then
+ ' if lowercase, then transpose down 1 octave and make uppercase
+ dividerIndex = dividerIndex - _Play_NotesPerOctave
+ LastChar = LastChar - 32
+ end if
+
+ dividerIndex = dividerIndex + _
+ _Play_NoteIndexes(LastChar) + _PLAY_CTX_GET(byte, _SemitoneAdjustment)
+
+ _PLAY_CTX_SET(byte, _SemitoneAdjustment, 0)
+
+ SetChipTonePitchDivider channel, _Play_NoteDividers(dividerIndex)
+
+ if channel = 0 then
+ ' Channel A pitch also defines noise generator pitch.
+ ' The formula taken from ROM disassembly.
+ ' Note: this affects all channels which have noise enabled.
+ SetChipNoisePitchDivider ((bnot dividerIndex) band $7f) >> 2
+ end if
+
+ volume = _PLAY_CTX_GET(ubyte, _Volume)
+ SetChipVolume channel, volume
+
+ if volume = VolumeEnvelopeOn then
+ ' If volume envelope is enabled on any channel, we need to re-initialize volume shape
+ ' on sound chip on every note start, for the envelope to start when the note starts.
+ ' Note that this affects all channels at once, but this is the limitation of hardware.
+ mustInitEnvelope = 1
+ end if
+
+ exit do
+
+ else if LastChar = code("$") then
+ _PLAY_CTX_ADD(byte, _SemitoneAdjustment, -1)
+
+ else if LastChar = code("#") then
+ _PLAY_CTX_ADD(byte, _SemitoneAdjustment, 1)
+
+ else if LastChar >= code("0") and LastChar <= code("9")
+ _PLAY_CTX_ADD(uinteger, _CharPtr, -1) ' step back
+ ReadNumber
+ lengthInTicks = _Play_NoteLengthsInTicks(LastNumber)
+
+ _PLAY_CTX_SET(uinteger, _ActualNoteLengthInTicks, lengthInTicks)
+
+ if LastNumber >= 10 then
+ ' triplet mode (temporary length)
+ _PLAY_CTX_SET(ubyte, _ResetNoteLengthBackCount, 3)
+ else
+ _PLAY_CTX_SET(uinteger, _PrimaryNoteLengthInTicks, lengthInTicks)
+ end if
+
+ else if LastChar = code("_") then
+ ' TODO: ties with triplets work differently in Sinclair ROM.
+ ReadNumber
+ lengthInTicks = _Play_NoteLengthsInTicks(LastNumber)
+
+ _PLAY_CTX_SET(ubyte, _ResetNoteLengthBackCount, 1)
+ _PLAY_CTX_ADD(uinteger, _ActualNoteLengthInTicks, lengthInTicks)
+ _PLAY_CTX_SET(uinteger, _PrimaryNoteLengthInTicks, lengthInTicks)
+
+ else if LastChar = code("(") then
+ nestingLevel = _PLAY_CTX_GET(ubyte, _NestingLevel)
+
+ if nestingLevel < MaxNestingLevel then
+ nestingLevel = nestingLevel + 1
+ returnPtrOffset = _ReturnPtrs + nestingLevel * 2
+ _PLAY_CTX_SET(ubyte, _NestingLevel, nestingLevel)
+ _PLAY_CTX_SET(uinteger, returnPtrOffset, _PLAY_CTX_GET(uinteger, _CharPtr))
+ end if
+
+ else if LastChar = code(")") then
+ nestingLevel = _PLAY_CTX_GET(ubyte, _NestingLevel)
+ returnPtrOffset = _ReturnPtrs + nestingLevel * 2
+ returnPtr = _PLAY_CTX_GET(uinteger, returnPtrOffset)
+
+ if returnPtr = 0 then
+ ' already repeated - decrease nesting level
+ nestingLevel = nestingLevel - 1
+ _PLAY_CTX_SET(ubyte, _NestingLevel, nestingLevel)
+ else
+ ' return to the repeat point
+ _PLAY_CTX_SET(uinteger, _CharPtr, returnPtr)
+
+ ' clear return pointer unless it is zero level (in which case we loop infinitely)
+ if nestingLevel > 0 then
+ _PLAY_CTX_SET(uinteger, returnPtrOffset, 0)
+ end if
+ end if
+
+ else if LastChar = code("O") then
+ ReadNumber
+ SetOctave LastNumber
+
+ else if LastChar = code("V") then
+ ReadNumber
+ _PLAY_CTX_SET(ubyte, _Volume, LastNumber)
+
+ else if LastChar = code("U") then
+ _PLAY_CTX_SET(ubyte, _Volume, VolumeEnvelopeOn)
+
+ else if LastChar = code("X") then
+ ReadNumber
+ SetChipEnvelopePeriod LastNumber
+
+ else if LastChar = code("W") then
+ ReadNumber
+ EnvelopeShape = _Play_EnvelopeShapes(LastNumber)
+
+ else if LastChar = code("M") then
+ ReadNumber
+ ' invert the bits and push directly to the chip
+ SetChipMixer bnot LastNumber
+
+ else if LastChar = code("T") then
+ ReadNumber
+
+ if channel = 0 then
+ Tempo = LastNumber
+ UpdateMicroticksPerTick
+ end if
+
+ else if LastChar = code("H") then
+ halt = 1
+ exit for
+
+ ' TODO: process other commands here
+ end if
+ loop
+
+ if _PLAY_CTX_GET(ubyte, _ResetNoteLengthBackCount) > 0 then
+ _PLAY_CTX_ADD(ubyte, _ResetNoteLengthBackCount, -1)
+ else
+ _PLAY_CTX_SET(uinteger, _ActualNoteLengthInTicks, _PLAY_CTX_GET(uinteger, _PrimaryNoteLengthInTicks))
+ end if
+
+ _PLAY_CTX_SET(uinteger, _TickBackCounter, _PLAY_CTX_GET(uinteger, _ActualNoteLengthInTicks))
+
+ processedChannels = processedChannels + 1
+ end if
+
+ _PLAY_CTX_ADD(uinteger, _TickBackCounter, -1)
+
+ finishedChannels = finishedChannels + _PLAY_CTX_GET(ubyte, _FinishedFlag)
+
+ _PLAY_CTX_NEXT_CHANNEL()
+ next channel
+
+ if mustInitEnvelope then
+ SetChipEnvelopeShape EnvelopeShape
+ end if
+
+ if halt then
+ for channel = 0 to MaxChannel
+ SetChipVolume channel, 0
+ next channel
+ exit do
+ end if
+
+ Wait MicroticksPerTick _
+ - TickGeneralOverheadInMicroticks _
+ - TickChannelCommandsOverheadInMicroticks * processedChannels
+ loop until finishedChannels = ChannelCount
+
+ #ifdef _PLAY_BENCHMARK_MODE
+ print "Play duration: "; SysFrames - startTime; " frames."
+ #endif
+
+ asm
+ ei
+ end asm
+end sub
+
+#pragma pop(explicit)
+#pragma pop(strict)
diff --git a/src/lib/arch/zx48k/stdlib/print42.bas b/src/lib/arch/zx48k/stdlib/print42.bas
index bbe8ba44c..6c1d84d3d 100644
--- a/src/lib/arch/zx48k/stdlib/print42.bas
+++ b/src/lib/arch/zx48k/stdlib/print42.bas
@@ -39,9 +39,15 @@ asm
LOCAL examineChar
examineChar:
LD A,(HL) ; Grab the character at our pointer position
- CP 128 ; Too high to print?
+ CP 165 ; Too high to print?
JR NC, nextChar ; Then we go to the next
+ CP 144
+ JR NC, prn ; char is a UDG
+
+ CP 128
+ JR NC, nextChar ; char is a block graphic
+
CP 22 ; Is this an AT?
JR NZ, isNewline ; If not jump over the AT routine to isNewline
@@ -130,6 +136,9 @@ printachar:
PUSH HL ; Store H'L' where we can get it.
EXX
+ CP 144
+ jr nc,printudg
+
ld c, a ; Put a copy of the character in C
ld h, 0
ld l, a ; Put the Character in HL
@@ -148,6 +157,19 @@ printachar:
ld c, l ; Copy our character data address into BC
jr printdata ; We have our data source, so we print it.
+LOCAL printudg
+printudg
+ sub 144 ; get the udg offset
+ ld hl,$5C7B ; examine system variables for udg address
+ ld e,(hl) ; lsb for udg address
+ inc hl
+ ld d,(hl) ; mbs for udg address
+ ld l,a ; character offset for udg
+ call mult8 ; multiplies L by 8 and adds in DE [so HL points at our table entry]
+ ld b, h
+ ld c, l ; Copy our character data address into BC
+ jr printdata ; We have our data source, so we print it.
+
LOCAL calcChar
calcChar: ; this is the calculate from the ROM data option
; a holds the column kill data
diff --git a/src/lib/arch/zx48k/stdlib/scroll.bas b/src/lib/arch/zx48k/stdlib/scroll.bas
index 33cfa9919..06661eb6f 100644
--- a/src/lib/arch/zx48k/stdlib/scroll.bas
+++ b/src/lib/arch/zx48k/stdlib/scroll.bas
@@ -1,8 +1,9 @@
' ----------------------------------------------------------------
' This file is released under the MIT License
'
-' Copyleft (k) 2008
+' Copyleft (k) 2008, 2025
' by Jose Rodriguez-Rosa (a.k.a. Boriel)
+' and Conrado Badenas
'
' Use this file as a template to develop your own library file
' ----------------------------------------------------------------
@@ -25,6 +26,709 @@ sub fastcall ScrollRight(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
asm
push namespace core
+ PROC
+ LOCAL LOOP1, LOOP2, MASK1, MASK2, COLUMNN, NEQ1
+ LOCAL AND1, AND2, AND3, AND4, AND5, AND6, AND7, AND8
+
+; Read parameters, return if they are bad
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+; Compute height and masks
+ inc a
+ ld e, a ; e = y2 - y1 + 1
+
+ ld a,c ;e.g. x1 = 3, mask1 = %11100000, CPL = %00011111
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x1 MOD 8) = 1,2,...,8 for x1 = 0,1,...,7 + 8*n
+ xor a
+MASK1:
+ rra
+ scf ;inject B-1 1s from the left
+ djnz MASK1
+ ld (AND1+1),a ;e.g. %11100000
+ ld (AND5+1),a
+ cpl
+ ld (AND2+1),a ;e.g. %00011111
+ ld (AND7+1),a
+
+ ld a,d ;e.g. x2 = 5, mask2 = %11111100, CPL = %00000011
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x2 MOD 8) = 1,2,...,8 for x2 = 0,1,...,7 + 8*n
+ xor a
+MASK2:
+ scf ;inject B 1s from the left
+ rra
+ djnz MASK2
+ ld (AND4+1),a ;e.g. %11111100
+ ld (AND8+1),a
+ cpl
+ ld (AND3+1),a ;e.g. %00000011
+ ld (AND6+1),a
+
+; Compute Ncols and Display File address, and choose branch
+ ld a,c ;x1
+ and %11111000
+ rrca
+ rrca
+ rrca
+ ld b,a ;col1
+ ld a,d ;x2
+ and %11111000
+ rrca
+ rrca
+ rrca ;col2
+ sub b ;A = Ncols - 1 = col2 - col1
+ ex af,af' ;save Ncols-1 and ZeroFlag
+
+ ld b, h ; BC = y2x1
+ ld a, 191
+ call 22ACh ; 2 bytes after the 'PIXEL ADDRESS' subroutine, because
+; https://skoolkid.github.io/rom/asm/22AA.html starts with LD A,175
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ex af,af' ;load Ncols-1 and ZeroFlag
+ jr z,NEQ1
+
+; N > 1 (there are 2 or more columns)
+ dec a ;A = Ncols - 2
+ ld d,a ;D = N-2
+LOOP1:
+ push hl
+; Scroll column 1
+ ld a,(hl)
+AND1:
+ and %11100000 ;e.g. x1 = 3
+ ld c,a ;get out-window part of byte
+ ld a,(hl)
+AND2:
+ and %00011111 ;e.g. x1 = 3
+ rra ;scroll in-window part of byte
+ rl b ;CF is stored as bit0 of B
+ or c ;put out-window part of byte
+ ld (hl),a
+ inc hl
+; Scroll columns 2,3,...,N-1
+ ld a,d ;N-2
+ and a ;A=0 iff N=2
+ jr z,COLUMNN
+ rr b ;CF is on stage
+ ld b,a
+LOOP2:
+ rr (hl)
+ inc hl
+ djnz LOOP2
+ rl b ;CF is stored as bit0 of B
+; Scroll column N
+COLUMNN:
+ ld a,(hl)
+AND3:
+ and %00000011 ;e.g. x2 = 5
+ ld c,a ;get out-window part of byte
+ ld a,(hl)
+ rr b ;CF is on stage
+ rra ;scroll in-window part of byte
+AND4:
+ and %11111100 ;e.g. x2 = 5
+ or c ;put out-window part of byte
+ ld (hl),a
+; Scroll another line
+ pop hl
+ dec e
+ ret z
+ call SP.PixelDown
+ jp LOOP1
+
+; N = 1 (there is only one column)
+NEQ1:
+ ld b,(hl)
+ ld a,b
+AND5:
+ and %11100000 ;e.g. x1 = 3
+ ld c,a
+ ld a,b
+AND6:
+ and %00000011 ;e.g. x2 = 5
+ or c
+ ld c,a ;get out-window part of byte
+ ld a,b
+AND7:
+ and %00011111 ;e.g. x1 = 3
+ rra ;scroll in-window part of byte
+AND8:
+ and %11111100 ;e.g. x2 = 5
+ or c ;put out-window part of byte
+ ld (hl),a
+; Scroll another line
+ dec e
+ ret z
+ call SP.PixelDown
+ jp NEQ1
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+' ----------------------------------------------------------------
+' sub ScrollLeft
+' pixel by pixel left scroll
+' scrolls 1 pixel left the window defined by (x1, y1, x2, y2)
+' ----------------------------------------------------------------
+sub fastcall ScrollLeft(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1, LOOP2, MASK1, MASK2, COLUMN1, NEQ1
+ LOCAL AND1, AND2, AND3, AND4, AND5, AND6, AND7, AND8
+
+; Read parameters, return if they are bad
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+; Compute height and masks
+ inc a
+ ld e, a ; e = y2 - y1 + 1
+
+ ld a,c ;e.g. x1 = 3, mask1 = %11100000, CPL = %00011111
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x1 MOD 8) = 1,2,...,8 for x1 = 0,1,...,7 + 8*n
+ xor a
+MASK1:
+ rra
+ scf ;inject B-1 1s from the left
+ djnz MASK1
+ ld (AND3+1),a ;e.g. %00000011
+ ld (AND6+1),a
+ cpl
+ ld (AND4+1),a ;e.g. %11111100
+ ld (AND8+1),a
+
+ ld a,d ;e.g. x2 = 5, mask2 = %11111100, CPL = %00000011
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x2 MOD 8) = 1,2,...,8 for x2 = 0,1,...,7 + 8*n
+ xor a
+MASK2:
+ scf ;inject B 1s from the left
+ rra
+ djnz MASK2
+ ld (AND2+1),a ;e.g. %00011111
+ ld (AND7+1),a
+ cpl
+ ld (AND1+1),a ;e.g. %11100000
+ ld (AND5+1),a
+
+; Compute Ncols and Display File address, and choose branch
+ ld a,c ;x1
+ and %11111000
+ rrca
+ rrca
+ rrca
+ ld b,a ;col1
+ ld a,d ;x2
+ and %11111000
+ rrca
+ rrca
+ rrca ;col2
+ sub b ;A = Ncols - 1 = col2 - col1
+ ex af,af' ;save Ncols-1 and ZeroFlag
+
+ ld c, d
+ ld b, h ; BC = y2x2
+ ld a, 191
+ call 22ACh ; 2 bytes after the 'PIXEL ADDRESS' subroutine, because
+; https://skoolkid.github.io/rom/asm/22AA.html starts with LD A,175
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ex af,af' ;load Ncols-1 and ZeroFlag
+ jr z,NEQ1
+
+; N > 1 (there are 2 or more columns)
+ dec a ;A = Ncols - 2
+ ld d,a ;D = N-2
+LOOP1:
+ push hl
+; Scroll column N
+ ld a,(hl)
+AND1:
+ and %00000011 ;e.g. x2 = 5
+ ld c,a ;get out-window part of byte
+ ld a,(hl)
+AND2:
+ and %11111100 ;e.g. x2 = 5
+ rla ;scroll in-window part of byte
+ rl b ;CF is stored as bit0 of B
+ or c ;put out-window part of byte
+ ld (hl),a
+ dec hl
+; Scroll columns N-1,...,3,2
+ ld a,d ;N-2
+ and a ;A=0 iff N=2
+ jr z,COLUMN1
+ rr b ;CF is on stage
+ ld b,a
+LOOP2:
+ rl (hl)
+ dec hl
+ djnz LOOP2
+ rl b ;CF is stored as bit0 of B
+; Scroll column 1
+COLUMN1:
+ ld a,(hl)
+AND3:
+ and %11100000 ;e.g. x1 = 3
+ ld c,a ;get out-window part of byte
+ ld a,(hl)
+ rr b ;CF is on stage
+ rla ;scroll in-window part of byte
+AND4:
+ and %00011111 ;e.g. x1 = 3
+ or c ;put out-window part of byte
+ ld (hl),a
+; Scroll another line
+ pop hl
+ dec e
+ ret z
+ call SP.PixelDown
+ jp LOOP1
+
+; N = 1 (there is only one column)
+NEQ1:
+ ld b,(hl)
+ ld a,b
+AND5:
+ and %00000011 ;e.g. x2 = 5
+ ld c,a
+ ld a,b
+AND6:
+ and %11100000 ;e.g. x1 = 3
+ or c
+ ld c,a ;get out-window part of byte
+ ld a,b
+AND7:
+ and %11111100 ;e.g. x2 = 5
+ rla ;scroll in-window part of byte
+AND8:
+ and %00011111 ;e.g. x1 = 3
+ or c ;put out-window part of byte
+ ld (hl),a
+; Scroll another line
+ dec e
+ ret z
+ call SP.PixelDown
+ jp NEQ1
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+
+' ----------------------------------------------------------------
+' sub ScrollUp
+' pixel by pixel up scroll
+' scrolls 1 pixel up the window defined by (x1, y1, x2, y2)
+' ----------------------------------------------------------------
+sub fastcall ScrollUp(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1, MASK1, MASK2, COLUMNN, NEQ1
+ LOCAL AND1, AND2, AND3, AND4, AND5, AND6, AND7, AND8
+ LOCAL EMPTYLINE
+
+; Read parameters, return if they are bad
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+; Compute height and masks
+ push ix
+ inc a
+ ld ixL,a;ixL = y2 - y1 + 1
+
+ ld a,c ;e.g. x1 = 3, mask1 = %11100000, CPL = %00011111
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x1 MOD 8) = 1,2,...,8 for x1 = 0,1,...,7 + 8*n
+ xor a
+MASK1:
+ rra
+ scf ;inject B-1 1s from the left
+ djnz MASK1
+ ld (AND1+1),a ;e.g. %11100000
+ ld (AND5+1),a
+ cpl
+ ld (AND2+1),a ;e.g. %00011111
+ ld (AND7+1),a
+
+ ld a,d ;e.g. x2 = 5, mask2 = %11111100, CPL = %00000011
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x2 MOD 8) = 1,2,...,8 for x2 = 0,1,...,7 + 8*n
+ xor a
+MASK2:
+ scf ;inject B 1s from the left
+ rra
+ djnz MASK2
+ ld (AND4+1),a ;e.g. %11111100
+ ld (AND8+1),a
+ cpl
+ ld (AND3+1),a ;e.g. %00000011
+ ld (AND6+1),a
+
+; Compute Ncols and Display File address, and choose branch
+ ld a,c ;x1
+ and %11111000
+ rrca
+ rrca
+ rrca
+ ld b,a ;col1
+ ld a,d ;x2
+ and %11111000
+ rrca
+ rrca
+ rrca ;col2
+ sub b ;A = Ncols - 1 = col2 - col1
+ ex af,af' ;save Ncols-1 and ZeroFlag
+
+ ld b, h ; BC = y2x1
+ ld a, 191
+ call 22ACh ; 2 bytes after the 'PIXEL ADDRESS' subroutine, because
+; https://skoolkid.github.io/rom/asm/22AA.html starts with LD A,175
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ex af,af' ;load Ncols-1 and ZeroFlag
+ jr z,NEQ1
+
+; N > 1 (there are 2 or more columns)
+ dec a ;A = Ncols - 2
+ ld ixH,a ;save Ncols-2
+ ld b,0
+LOOP1:
+ dec ixL
+ ld d,h
+ ld e,l
+ call z,EMPTYLINE ;HL at empty line
+ call nz,SP.PixelDown ;HL at line below
+ inc ixL ;restore iterative variable for a second check
+ push hl
+; Scroll column 1
+ ld a,(hl)
+AND2:
+ and %00011111 ;e.g. x1 = 3
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND1:
+ and %11100000 ;e.g. x1 = 3
+ or c ;put in-window part of byte
+ ld (de),a
+ inc hl
+ inc de
+; Scroll columns 2,3,...,N-1
+ ld a,ixH ;load Ncols-2
+ and a
+ jr z,COLUMNN
+ ld c,a
+ ldir
+; Scroll column N
+COLUMNN:
+ ld a,(hl)
+AND4:
+ and %11111100 ;e.g. x2 = 5
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND3:
+ and %00000011 ;e.g. x2 = 5
+ or c ;put in-window part of byte
+ ld (de),a
+; Scroll another line
+ pop hl
+ dec ixL
+ jp nz,LOOP1
+ pop ix
+ ret
+
+; N = 1 (there is only one column)
+NEQ1:
+ dec ixL
+ ld d,h
+ ld e,l
+ call z,EMPTYLINE ;HL at empty line
+ call nz,SP.PixelDown ;HL at line below
+ inc ixL ;restore iterative variable for a second check
+
+ ld a,(hl)
+AND7:
+ and %00011111 ;e.g. x1 = 3
+AND8:
+ and %11111100 ;e.g. x2 = 5
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND5:
+ and %11100000 ;e.g. x1 = 3
+ ld b,a
+ ld a,(de)
+AND6:
+ and %00000011 ;e.g. x2 = 5
+ or b
+ or c ;put in-window part of byte
+ ld (de),a
+; Scroll another line
+ dec ixL
+ jp nz,NEQ1
+ pop ix
+ ret
+
+ defs 32,0 ;empty line with 32 zero-bytes
+EMPTYLINE:
+ ld hl,EMPTYLINE-32
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+
+' ----------------------------------------------------------------
+' sub ScrollDown
+' pixel by pixel down scroll
+' scrolls 1 pixel down the window defined by (x1, y1, x2, y2)
+' ----------------------------------------------------------------
+sub fastcall ScrollDown(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1, MASK1, MASK2, COLUMNN, NEQ1
+ LOCAL AND1, AND2, AND3, AND4, AND5, AND6, AND7, AND8
+ LOCAL EMPTYLINE
+
+; Read parameters, return if they are bad
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+; Compute height and masks
+ push ix
+ inc a
+ ld ixL,a;ixL = y2 - y1 + 1
+ ld h,b ;y1
+
+ ld a,c ;e.g. x1 = 3, mask1 = %11100000, CPL = %00011111
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x1 MOD 8) = 1,2,...,8 for x1 = 0,1,...,7 + 8*n
+ xor a
+MASK1:
+ rra
+ scf ;inject B-1 1s from the left
+ djnz MASK1
+ ld (AND1+1),a ;e.g. %11100000
+ ld (AND5+1),a
+ cpl
+ ld (AND2+1),a ;e.g. %00011111
+ ld (AND7+1),a
+
+ ld a,d ;e.g. x2 = 5, mask2 = %11111100, CPL = %00000011
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x2 MOD 8) = 1,2,...,8 for x2 = 0,1,...,7 + 8*n
+ xor a
+MASK2:
+ scf ;inject B 1s from the left
+ rra
+ djnz MASK2
+ ld (AND4+1),a ;e.g. %11111100
+ ld (AND8+1),a
+ cpl
+ ld (AND3+1),a ;e.g. %00000011
+ ld (AND6+1),a
+
+; Compute Ncols and Display File address, and choose branch
+ ld a,c ;x1
+ and %11111000
+ rrca
+ rrca
+ rrca
+ ld b,a ;col1
+ ld a,d ;x2
+ and %11111000
+ rrca
+ rrca
+ rrca ;col2
+ sub b ;A = Ncols - 1 = col2 - col1
+ ex af,af' ;save Ncols-1 and ZeroFlag
+
+ ld b, h ; BC = y1x1
+ ld a, 191
+ call 22ACh ; 2 bytes after the 'PIXEL ADDRESS' subroutine, because
+; https://skoolkid.github.io/rom/asm/22AA.html starts with LD A,175
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ex af,af' ;load Ncols-1 and ZeroFlag
+ jr z,NEQ1
+
+; N > 1 (there are 2 or more columns)
+ dec a ;A = Ncols - 2
+ ld ixH,a ;save Ncols-2
+ ld b,0
+LOOP1:
+ dec ixL
+ ld d,h
+ ld e,l
+ call z,EMPTYLINE ;HL at empty line
+ call nz,SP.PixelUp ;HL at line above
+ inc ixL ;restore iterative variable for a second check
+ push hl
+; Scroll column 1
+ ld a,(hl)
+AND2:
+ and %00011111 ;e.g. x1 = 3
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND1:
+ and %11100000 ;e.g. x1 = 3
+ or c ;put in-window part of byte
+ ld (de),a
+ inc hl
+ inc de
+; Scroll columns 2,3,...,N-1
+ ld a,ixH ;load Ncols-2
+ and a
+ jr z,COLUMNN
+ ld c,a
+ ldir
+; Scroll column N
+COLUMNN:
+ ld a,(hl)
+AND4:
+ and %11111100 ;e.g. x2 = 5
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND3:
+ and %00000011 ;e.g. x2 = 5
+ or c ;put in-window part of byte
+ ld (de),a
+; Scroll another line
+ pop hl
+ dec ixL
+ jp nz,LOOP1
+ pop ix
+ ret
+
+; N = 1 (there is only one column)
+NEQ1:
+ dec ixL
+ ld d,h
+ ld e,l
+ call z,EMPTYLINE ;HL at empty line
+ call nz,SP.PixelUp ;HL at line below
+ inc ixL ;restore iterative variable for a second check
+
+ ld a,(hl)
+AND7:
+ and %00011111 ;e.g. x1 = 3
+AND8:
+ and %11111100 ;e.g. x2 = 5
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND5:
+ and %11100000 ;e.g. x1 = 3
+ ld b,a
+ ld a,(de)
+AND6:
+ and %00000011 ;e.g. x2 = 5
+ or b
+ or c ;put in-window part of byte
+ ld (de),a
+; Scroll another line
+ dec ixL
+ jp nz,NEQ1
+ pop ix
+ ret
+
+ defs 32,0 ;empty line with 32 zero-bytes
+EMPTYLINE:
+ ld hl,EMPTYLINE-32
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+
+' ----------------------------------------------------------------
+' sub ScrollRightAligned
+' pixel by pixel right scroll.
+' scrolls 1 pixel right the window defined by (x1, y1, x2, y2)
+' This scroll is aligned to columns.
+' x1 and x2 are divided by 8 and rounded down (floor) to get the column number (0..31)
+' ----------------------------------------------------------------
+sub fastcall ScrollRightAligned(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
PROC
LOCAL LOOP1
LOCAL LOOP2
@@ -84,11 +788,13 @@ end sub
' ----------------------------------------------------------------
-' sub ScrolLeft
+' sub ScrolLeftAligned
' pixel by pixel left scroll
' scrolls 1 pixel left the window defined by (x1, y1, x2, y2)
+' This scroll is aligned to columns.
+' x1 and x2 are divided by 8 and rounded down (floor) to get the column number (0..31)
' ----------------------------------------------------------------
-sub fastcall ScrollLeft(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+sub fastcall ScrollLeftAligned(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
asm
push namespace core
@@ -152,11 +858,13 @@ end sub
' ----------------------------------------------------------------
-' sub ScrolUp
+' sub ScrolUpAligned
' pixel by pixel up scroll
' scrolls 1 pixel up the window defined by (x1, y1, x2, y2)
+' This scroll is aligned to columns.
+' x1 and x2 are divided by 8 and rounded down (floor) to get the column number (0..31)
' ----------------------------------------------------------------
-sub fastcall ScrollUp(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+sub fastcall ScrollUpAligned(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
asm
push namespace core
@@ -238,11 +946,13 @@ end sub
' ----------------------------------------------------------------
-' sub ScrolDown
+' sub ScrolDownAligned
' pixel by pixel down scroll
' scrolls 1 pixel down the window defined by (x1, y1, x2, y2)
+' This scroll is aligned to columns.
+' x1 and x2 are divided by 8 and rounded down (floor) to get the column number (0..31)
' ----------------------------------------------------------------
-sub fastcall ScrollDown(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+sub fastcall ScrollDownAligned(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
asm
push namespace core
@@ -322,10 +1032,11 @@ LOOP_START:
end asm
end sub
+
#pragma pop(case_insensitive)
-REM the following is required, because it defines screen start addr
-#require "cls.asm"
+REM the following is required, because it defines SCREEN_ADDR and SCREEN_ATTR_ADDR
+#require "sysvars.asm"
#require "SP/PixelDown.asm"
#require "SP/PixelUp.asm"
diff --git a/src/lib/arch/zx48k/stdlib/winscroll.bas b/src/lib/arch/zx48k/stdlib/winscroll.bas
index 45e5a9a4e..381f02250 100644
--- a/src/lib/arch/zx48k/stdlib/winscroll.bas
+++ b/src/lib/arch/zx48k/stdlib/winscroll.bas
@@ -1,8 +1,9 @@
' ----------------------------------------------------------------
' This file is released under the MIT License
'
-' Copyleft (k) 2008
+' Copyleft (k) 2008, 2025
' by Jose Rodriguez-Rosa (a.k.a. Boriel)
+' and Conrado Badenas
'
' Use this file as a template to develop your own library file
' ----------------------------------------------------------------
@@ -19,75 +20,39 @@ REM Avoid recursive / multiple inclusion
' ---------------------------------------------------------------------
' sub WinScrollRight
' scrolls the window defined by (row, col, width, height) 1 cell right
+' it works with width=1 or height=1 but it is not impressive
+' nothing is done if width=0 or height=0
' ---------------------------------------------------------------------
sub fastcall WinScrollRight(row as uByte, col as uByte, width as Ubyte, height as Ubyte)
asm
push namespace core
- LOCAL BucleChars
- LOCAL BucleScans
- LOCAL BucleAttrs
-
PROC
- ld b, a
- pop hl
- pop de
- ld c, d
+ LOCAL BucleRows, BucleScans, AfterLDDR, AfterLDDR2, AfterLDDR3
+
+; Read parameters, return if they are bad
+ ld b, a ;row
+ pop hl ;return address
pop de
+ ld a, d ;colLeft
+ pop de ;width
+ add a, d
+ dec a
+ ld c, a ;colRight = colLeft + width - 1
ex (sp), hl
- ld e, h
+ ld e, h ;height
ld a, e
or a
- ret z
- or d
- ret z
- push bc
- ld a,b
- and 18h
- ld h,a
- ld a,b
- and 07h
- add a,a
- add a,a
- add a,a
- add a,a
- add a,a
- add a,c
- add a,d
- dec a
- ld l,a ;HL=top-left window address in bitmap coord
- ld bc, (SCREEN_ADDR)
- add hl, bc
- ld b,e
-
-BucleChars:
- push bc
- ld b,8
-
-BucleScans:
- push bc
- push de
- push hl
- ld c,d
- dec c
- ld b,0
- ld d,h
- ld e,l
- dec l
- lddr
- ; clean up latest
- xor a
- ld (de),a
- pop hl
- pop de
- inc h
- pop bc
- djnz BucleScans
- dec h
- call SP.PixelDown
- pop bc
- djnz BucleChars
-
- pop bc
+ ret z ;height=0
+ ld a, d
+ or a
+ ret z ;width=0
+
+; Compute Attributes address
+ sub 2 ;CF=1 if width=1, CF=0 if width>1
+ inc a
+ ex af,af' ;A' = width-1, CF'=1 if width=1, CF'=0 if width>1
+ push ix
+ ld ixL,e ;IXl = height, iterative variable
ld l,b
ld h,0
add hl,hl
@@ -97,33 +62,77 @@ BucleScans:
add hl,hl
ld a,l
add a,c
- add a,d
- dec a
- ld l,a
- ld a,h
- ld h,a ;HL=top-left window address in attr coord
- ld bc, (SCREEN_ATTR_ADDR)
- add hl, bc
- ld b,e
-
-BucleAttrs:
- push bc
- push de
- push hl
- ld c,d
- dec c
- ld b,0
- ld d,h
- ld e,l
- dec l
- lddr
- pop hl
- ld de,32
+ ld l,a ;HL = row*32 + colRight
+ ld de,(SCREEN_ATTR_ADDR)
add hl,de
- pop de
- pop bc
- djnz BucleAttrs
+; For each row, compute Display File address
+BucleRows:
+ push bc ;coords = row,col
+ push hl ;attr address
+ ld a,b ;row
+ and %00011000 ;select third (0,1,2)
+ ld h,a
+ ld a,b
+ and %00000111 ;select Row In Third (0,...,7)
+ rrca
+ rrca
+ rrca ;A = %RIT00000
+ add a,c
+ ld l,a ;HL = %000th000RITcolmn
+ ld de,(SCREEN_ADDR)
+ add hl,de
+ ld b,0
+ ex af,af' ;A = width-1, CF=1 if width=1, CF=0 if width>1
+ ld ixH,7
+
+; For each row, transfer 8 scans
+BucleScans:
+ push hl
+ jr c,AfterLDDR
+ ld d,h
+ ld e,l ;DE = to address
+ dec hl ;HL = from address
+ ld c,a
+ lddr ;1st-7th scan in row
+ ex de,hl
+AfterLDDR:
+ ld (hl),b ;clean up leftmost
+ pop hl
+ inc h ;scan below
+ dec ixH
+ jp nz,BucleScans
+
+ jr c,AfterLDDR2
+ ld d,h
+ ld e,l ;DE = to address
+ dec hl ;HL = from address
+ ld c,a
+ lddr ;8th scan in row
+ ex de,hl
+AfterLDDR2:
+ ld (hl),b ;clean up leftmost
+
+; For each row, transfer a line of attributes
+ pop hl ;attr address
+ jr c,AfterLDDR3
+ ld d,h
+ ld e,l ;DE = to address
+ dec hl ;HL = from address
+ ld c,a
+ push de
+ lddr ;attrs in row
+ pop hl
+AfterLDDR3:
+ ex af,af' ;A' = width-1, CF'=1 if width=1, CF'=0 if width>1
+ ld de,32
+ add hl,de ;row below
+ pop bc
+ inc b ;row below
+ dec ixL
+ jp nz,BucleRows
+
+ pop ix
ENDP
pop namespace
end asm
@@ -133,74 +142,36 @@ end sub
' ---------------------------------------------------------------------
' sub WinScrollLeft
' scrolls the window defined by (row, col, width, height) 1 cell left
+' it works with width=1 or height=1 but it is not impressive
+' nothing is done if width=0 or height=0
' ---------------------------------------------------------------------
sub fastcall WinScrollLeft(row as uByte, col as uByte, width as Ubyte, height as Ubyte)
asm
push namespace core
PROC
- LOCAL BucleChars
- LOCAL BucleScans
- LOCAL BucleAttrs
+ LOCAL BucleRows, BucleScans, AfterLDIR, AfterLDIR2, AfterLDIR3
- ld b, a
- pop hl
- pop de
- ld c, d
+; Read parameters, return if they are bad
+ ld b, a ;row
+ pop hl ;return address
pop de
+ ld c, d ;col
+ pop de ;width
ex (sp), hl
- ld e, h
+ ld e, h ;height
ld a, e
or a
- ret z
- or d
- ret z
- push bc
- ld a,b
- and 18h
- ld h,a
- ld a,b
- and 07h
- add a,a
- add a,a
- add a,a
- add a,a
- add a,a
- add a,c
- ld l,a ;HL=top-left window address in bitmap coord
- ld bc, (SCREEN_ADDR)
- add hl, bc
- ld b,e
-
-BucleChars:
- push bc
- ld b,8
-
-BucleScans:
- push bc
- push de
- push hl
- ld c,d
- dec c
- ld b,0
- ld d,h
- ld e,l
- inc e
- ex de,hl
- ldir
- ; clean up latest
- xor a
- ld (de),a
- pop hl
- pop de
- inc h
- pop bc
- djnz BucleScans
- dec h
- call SP.PixelDown
- pop bc
- djnz BucleChars
-
- pop bc
+ ret z ;height=0
+ ld a, d
+ or a
+ ret z ;width=0
+
+; Compute Attributes address
+ sub 2 ;CF=1 if width=1, CF=0 if width>1
+ inc a
+ ex af,af' ;A' = width-1, CF'=1 if width=1, CF'=0 if width>1
+ push ix
+ ld ixL,e ;IXl = height, iterative variable
ld l,b
ld h,0
add hl,hl
@@ -210,31 +181,77 @@ BucleScans:
add hl,hl
ld a,l
add a,c
- ld l,a
- ld a,h
- ld h,a ;HL=top-left address in attr coords
- ld bc, (SCREEN_ATTR_ADDR)
- add hl, bc
- ld b,e
-
-BucleAttrs:
- push bc
- push de
- push hl
- ld c,d
- dec c
- ld b,0
- ld d,h
- ld e,l
- inc e
- ex de,hl
- ldir
- pop hl
- ld de,32
+ ld l,a ;HL = row*32 + col
+ ld de,(SCREEN_ATTR_ADDR)
add hl,de
- pop de
- pop bc
- djnz BucleAttrs
+
+; For each row, compute Display File address
+BucleRows:
+ push bc ;coords = row,col
+ push hl ;attr address
+ ld a,b ;row
+ and %00011000 ;select third (0,1,2)
+ ld h,a
+ ld a,b
+ and %00000111 ;select Row In Third (0,...,7)
+ rrca
+ rrca
+ rrca ;A = %RIT00000
+ add a,c
+ ld l,a ;HL = %000th000RITcolmn
+ ld de,(SCREEN_ADDR)
+ add hl,de
+ ld b,0
+ ex af,af' ;A = width-1, CF=1 if width=1, CF=0 if width>1
+ ld ixH,7
+
+; For each row, transfer 8 scans
+BucleScans:
+ push hl
+ jr c,AfterLDIR
+ ld d,h
+ ld e,l ;DE = to address
+ inc hl ;HL = from address
+ ld c,a
+ ldir ;1st-7th scan in row
+ ex de,hl
+AfterLDIR:
+ ld (hl),b ;clean up rightmost
+ pop hl
+ inc h ;scan below
+ dec ixH
+ jp nz,BucleScans
+
+ jr c,AfterLDIR2
+ ld d,h
+ ld e,l ;DE = to address
+ inc hl ;HL = from address
+ ld c,a
+ ldir ;8th scan in row
+ ex de,hl
+AfterLDIR2:
+ ld (hl),b ;clean up rightmost
+
+; For each row, transfer a line of attributes
+ pop hl ;attr address
+ jr c,AfterLDIR3
+ ld d,h
+ ld e,l ;DE = to address
+ inc hl ;HL = from address
+ ld c,a
+ push de
+ ldir ;attrs in row
+ pop hl
+AfterLDIR3:
+ ex af,af' ;A' = width-1, CF'=1 if width=1, CF'=0 if width>1
+ ld de,32
+ add hl,de ;row below
+ pop bc
+ inc b ;row below
+ dec ixL
+ jp nz,BucleRows
+
+ pop ix
ENDP
pop namespace
end asm
@@ -244,107 +261,35 @@ end sub
' ---------------------------------------------------------------------
' sub WinScrollUp
' scrolls the window defined by (row, col, width, height) 1 cell up
+' it works with width=1 or height=1 but it is not impressive
+' nothing is done if width=0 or height=0
' ---------------------------------------------------------------------
sub fastcall WinScrollUp(row as uByte, col as uByte, width as Ubyte, height as Ubyte)
asm
push namespace core
PROC
- LOCAL BucleScans, BucleAttrs, ScrollAttrs
- LOCAL CleanLast, CleanLastLoop, EndCleanScan
+ LOCAL BucleRows, BucleScans, AttrAddress
+ LOCAL CleanBottomRow, CleanBottomScans, AfterLDIR
- ld b, a
- pop hl
- pop de
- ld c, d
+; Read parameters, return if they are bad
+ ld b, a ;row
+ pop hl ;return address
pop de
+ ld c, d ;col
+ pop de ;width
ex (sp), hl
- ld e, h
+ ld e, h ;height
ld a, e
or a
- ret z
- or d
- ret z
-
- push bc
- push de
-
- ld a,b
- and 18h
- ld h,a
- ld a,b
- and 07h
- add a,a
- add a,a
- add a,a
- add a,a
- add a,a
- add a,c
- ld l,a ;HL=top-left window address in bitmap coord
- ld bc, (SCREEN_ADDR)
- add hl, bc
- ld a,e
- ld c, d ; c = width
- ld d, h
- ld e, l
- dec a
- jr z, CleanLast
- add a,a
- add a,a
- add a,a
- ld b, a ; b = 8 * (height - 1)
-
- inc h
- inc h
- inc h
- inc h
- inc h
- inc h
- inc h
- call SP.PixelDown
-
-BucleScans:
- push bc
- push de
- push hl
- ld b, 0
- ldir
- pop hl
- pop de
- pop bc
- call SP.PixelDown
- ex de, hl
- call SP.PixelDown
- ex de, hl
- djnz BucleScans
-
-CleanLast:
- ex de,hl
- pop de
- ld b, 8
- ld c, d
- push de
-
-CleanLastLoop:
- push bc
- push hl
- ld (hl), 0
- dec c
- jr z, EndCleanScan
- ld d, h
- ld e, l
- inc de
- ld b, 0
- ldir
-
-EndCleanScan:
- pop hl
- pop bc
- inc h
- djnz CleanLastLoop
+ ret z ;height=0
+ ld a, d
+ or a
+ ret z ;width=0
-ScrollAttrs:
- pop de
- pop bc
+; Compute TopRow Attributes address
+ ex af,af' ;A' = width
+ push ix
+ ld ixL,e ;IXl = height, iterative variable
ld l,b
ld h,0
add hl,hl
@@ -354,147 +299,145 @@ ScrollAttrs:
add hl,hl
ld a,l
add a,c
- ld l,a
- ld a,h
- ld h,a ;HL=top-left address in attr coords
- ld bc, (SCREEN_ATTR_ADDR)
- add hl, bc
- ld b,e
- dec b
- ret z
-
-BucleAttrs:
- push bc
- push de
- push hl
- ld b,0
- ld c,d
- ex de,hl
- ld hl,32
+ ld l,a ;HL = row*32 + col
+ ld de,(SCREEN_ATTR_ADDR)
add hl,de
- ldir
- pop hl
- ld de,32
+ ld (AttrAddress+1),hl
+
+; Compute TopRow Display File address
+ ld a,b ;row
+ and %00011000 ;select third (0,1,2)
+ ld h,a
+ ld a,b
+ and %00000111 ;select Row In Third (0,...,7)
+ rrca
+ rrca
+ rrca ;A = %RIT00000
+ add a,c
+ ld l,a ;HL = %000th000RITcolmn
+ ld de,(SCREEN_ADDR)
add hl,de
- pop de
- pop bc
- djnz BucleAttrs
+ push hl ;HL = from address
+
+BucleRows:
+ dec ixL
+ jr z,CleanBottomRow
+; For each row, compute Display File address
+ inc b ;row below
+ ld a,b ;row
+ and %00011000 ;select third (0,1,2)
+ ld h,a
+ ld a,b
+ and %00000111 ;select Row In Third (0,...,7)
+ rrca
+ rrca
+ rrca ;A = %RIT00000
+ add a,c
+ ld l,a ;HL = %000th000RITcolmn
+ ld de,(SCREEN_ADDR)
+ add hl,de
+
+; For each row, transfer 8 scans
+ pop de ;DE = to address, obtained from last "push hl; from address"
+ push hl ;HL = from address, will be the next "to address"
+ push bc ;coords = row,col
+ ld b,0
+ ex af,af' ;A = width
+ ld ixH,7
+BucleScans:
+ ld c,a
+ push de
+ push hl
+ ldir ;1st-7th scan in row
+ pop hl
+ pop de
+ inc h ;scan below
+ inc d ;scan below
+ dec ixH
+ jp nz,BucleScans
+
+ ld c,a
+ ldir ;8th scan in row
+
+; For each row, transfer a line of attributes
+AttrAddress:
+ ld hl,AttrAddress
+ ld d,h
+ ld e,l ;DE = to address
+ ld c,32
+ add hl,bc ;HL = from address
+ ld (AttrAddress+1),hl
+ ld c,a
+ ldir ;attrs in row
+ ex af,af' ;A' = width
+ pop bc
+ jp BucleRows
+
+; Clean bottom row (Display File, not Attributes)
+CleanBottomRow:
+ ld b,0
+ ex af,af' ;A = width
+ ld ixH,8 ;no need to speed up code by processing scans 1-7 apart of scan 8
+ pop hl
+CleanBottomScans:
+ ld (hl),b
+ ld c,a
+ dec c
+ jr z,AfterLDIR
+ push hl
+ ld d,h
+ ld e,l
+ inc de
+ ldir
+ pop hl
+AfterLDIR:
+ inc h
+ dec ixH
+ jp nz,CleanBottomScans
+
+ pop ix
ENDP
pop namespace
end asm
-
end sub
' ---------------------------------------------------------------------
' sub WinScrollDown
' scrolls the window defined by (row, col, width, height) 1 cell down
+' it works with width=1 or height=1 but it is not impressive
+' nothing is done if width=0 or height=0
' ---------------------------------------------------------------------
sub fastcall WinScrollDown(row as uByte, col as uByte, width as Ubyte, height as Ubyte)
asm
push namespace core
PROC
- LOCAL BucleScans, BucleAttrs, ScrollAttrs
- LOCAL CleanLast, CleanLastLoop, EndCleanScan
+ LOCAL BucleRows, BucleScans, AttrAddress
+ LOCAL CleanTopRow, CleanTopScans, AfterLDIR
- ld b, a
- pop hl
- pop de
- ld c, d
+; Read parameters, return if they are bad
+ ld b, a ;rowTop
+ pop hl ;return address
pop de
+ ld c, d ;col
+ pop de ;width
ex (sp), hl
- ld e, h
- ld a, e
- or a
- ret z
- or d
- ret z
-
+ ld e, h ;height
ld a, b
add a, e
dec a
- ld b, a ; b = row + height - 1 = top most row
-
- push bc
- push de
-
- ld a,b
- and 18h
- ld h,a
- ld a,b
- and 07h
- add a,a
- add a,a
- add a,a
- add a,a
- add a,a
- add a,c
- ld l,a ;HL=bottom-left window address in bitmap coord
- ld bc, (SCREEN_ADDR)
- add hl, bc
- ld a,e
- ld c, d ; c = width
- ld d, h
- ld e, l
- dec a
- jr z, CleanLast
- add a,a
- add a,a
- add a,a
- ld b, a ; b = 8 * (height - 1)
-
- inc d
- inc d
- inc d
- inc d
- inc d
- inc d
- inc d
- call SP.PixelUp
-
-BucleScans:
- push bc
- push de
- push hl
- ld b, 0
- ldir
- pop hl
- pop de
- pop bc
- call SP.PixelUp
- ex de, hl
- call SP.PixelUp
- ex de, hl
- djnz BucleScans
-
-CleanLast:
- ex de,hl
- pop de
- ld b, 8
- ld c, d
- push de
-
-CleanLastLoop:
- push bc
- push hl
- ld (hl), 0
- dec c
- jr z, EndCleanScan
- ld d, h
- ld e, l
- inc de
- ld b, 0
- ldir
-
-EndCleanScan:
- pop hl
- pop bc
- dec h
- djnz CleanLastLoop
+ ld b, a ;rowBottom = rowTop + height - 1
+ ld a, e
+ or a
+ ret z ;height=0
+ ld a, d
+ or a
+ ret z ;width=0
-ScrollAttrs:
- pop de
- pop bc
+; Compute BottomRow Attributes address
+ ex af,af' ;A' = width
+ push ix
+ ld ixL,e ;IXl = height, iterative variable
ld l,b
ld h,0
add hl,hl
@@ -504,43 +447,111 @@ ScrollAttrs:
add hl,hl
ld a,l
add a,c
- ld l,a
- ld a,h
- ld h,a ;HL=top-left address in attr coords
- ld bc, (SCREEN_ATTR_ADDR)
- add hl, bc
- ld b,e
- dec b
- ret z
-
-BucleAttrs:
- push bc
- push de
- push hl
- ld b,0
- ld c,d
- ex de,hl
- ld hl,-32
+ ld l,a ;HL = row*32 + col
+ ld de,(SCREEN_ATTR_ADDR)
add hl,de
- ldir
- pop hl
- ld de,-32
+ ld (AttrAddress+1),hl
+
+; Compute BottomRow Display File address
+ ld a,b ;row
+ and %00011000 ;select third (0,1,2)
+ ld h,a
+ ld a,b
+ and %00000111 ;select Row In Third (0,...,7)
+ rrca
+ rrca
+ rrca ;A = %RIT00000
+ add a,c
+ ld l,a ;HL = %000th000RITcolmn
+ ld de,(SCREEN_ADDR)
add hl,de
- pop de
- pop bc
- djnz BucleAttrs
+ push hl ;HL = from address
+
+BucleRows:
+ dec ixL
+ jr z,CleanTopRow
+; For each row, compute Display File address
+ dec b ;row above
+ ld a,b ;row
+ and %00011000 ;select third (0,1,2)
+ ld h,a
+ ld a,b
+ and %00000111 ;select Row In Third (0,...,7)
+ rrca
+ rrca
+ rrca ;A = %RIT00000
+ add a,c
+ ld l,a ;HL = %000th000RITcolmn
+ ld de,(SCREEN_ADDR)
+ add hl,de
+
+; For each row, transfer 8 scans
+ pop de ;DE = to address, obtained from last "push hl; from address"
+ push hl ;HL = from address, will be the next "to address"
+ push bc ;coords = row,col
+ ld b,0
+ ex af,af' ;A = width
+ ld ixH,7
+BucleScans:
+ ld c,a
+ push de
+ push hl
+ ldir ;1st-7th scan in row
+ pop hl
+ pop de
+ inc h ;scan below
+ inc d ;scan below
+ dec ixH
+ jp nz,BucleScans
+
+ ld c,a
+ ldir ;8th scan in row
+
+; For each row, transfer a line of attributes
+AttrAddress:
+ ld hl,AttrAddress
+ ld d,h
+ ld e,l ;DE = to address
+ ld c,32 ;it is a "mORAcle" that CarryFlag is always 0 here
+ sbc hl,bc ;HL = from address
+ ld (AttrAddress+1),hl
+ ld c,a
+ ldir ;attrs in row
+ ex af,af' ;A' = width
+ pop bc
+ jp BucleRows
+
+; Clean top row (Display File, not Attributes)
+CleanTopRow:
+ ld b,0
+ ex af,af' ;A = width
+ ld ixH,8 ;no need to speed up code by processing scans 1-7 apart of scan 8
+ pop hl
+CleanTopScans:
+ ld (hl),b
+ ld c,a
+ dec c
+ jr z,AfterLDIR
+ push hl
+ ld d,h
+ ld e,l
+ inc de
+ ldir
+ pop hl
+AfterLDIR:
+ inc h
+ dec ixH
+ jp nz,CleanTopScans
+
+ pop ix
ENDP
pop namespace
end asm
-
end sub
#pragma pop(case_insensitive)
-REM the following is required, because it defines screen start addr
-#require "cls.asm"
-#require "SP/PixelDown.asm"
-#require "SP/PixelUp.asm"
+REM the following is required, because it defines SCREEN_ADDR and SCREEN_ATTR_ADDR
#require "sysvars.asm"
diff --git a/src/lib/arch/zx81sd/runtime/arch_config.asm b/src/lib/arch/zx81sd/runtime/arch_config.asm
new file mode 100644
index 000000000..eac1425cf
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/arch_config.asm
@@ -0,0 +1,29 @@
+; arch_config.asm — Constantes de arquitectura ZX81 + SD81 Booster
+;
+; Sobreescribe zx48k/runtime/arch_config.asm para redirigir las
+; llamadas ROM del runtime hacia implementaciones propias en RAM.
+
+; ---------------------------------------------------------------------------
+; Usar scroll por buffer propio (sin ROM Spectrum)
+; ---------------------------------------------------------------------------
+#define __ZXB_ENABLE_BUFFER_SCROLL
+
+; ---------------------------------------------------------------------------
+; Sin soporte de coma flotante en esta versión
+; (el FP calculator de la ROM Spectrum no está disponible)
+; ---------------------------------------------------------------------------
+#define __ZXB_NO_FLOAT
+
+; ---------------------------------------------------------------------------
+; Gráficos — implementaciones propias
+; ---------------------------------------------------------------------------
+; Incluir aquí los ficheros con las rutinas que sustituyen a las ROM calls.
+; El resto del runtime (plot.asm, draw.asm...) incluye arch_config.asm,
+; por lo que estas definiciones quedan disponibles automáticamente.
+
+#include once
+#include once
+
+; Nota: SCROLL_SCR, KEY_SCAN, KEY_TEST, KEY_CODE, LD_BYTES, ROM_SAVE, etc.
+; deberán implementarse cuando se necesiten. Los dejamos sin definir
+; para que el enlazador falle con un error claro si se usan accidentalmente.
diff --git a/src/lib/arch/zx81sd/runtime/arith/divf.asm b/src/lib/arch/zx81sd/runtime/arith/divf.asm
new file mode 100644
index 000000000..42e092006
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/arith/divf.asm
@@ -0,0 +1,74 @@
+#include once
+#include once
+#include once
+
+; -------------------------------------------------------------
+; Floating point library using the FP ROM Calculator (ZX 48K)
+
+; All of them uses C EDHL registers as 1st paramter.
+; For binary operators, the 2n operator must be pushed into the
+; stack, in the order BC DE HL (B not used).
+;
+; Uses CALLEE convention
+; -------------------------------------------------------------
+;
+; zx81sd override: the only change from zx48k's version is where
+; TMP/ERR_SP live. The original uses the Spectrum ROM's DEST (23629)
+; and ERR_SP (23613) system variables to save/restore a stack recovery
+; point around the division (a "longjmp" trick for divide-by-zero) --
+; on zx81sd that mechanism never actually gets used for real recovery
+; (__ERROR does DI+HALT directly, it doesn't restore SP from ERR_SP),
+; but this code writes there on every division regardless of whether
+; an error happens, and those addresses fall inside the program's own
+; compiled code (block 2) instead of real sysvars. DIVF_SCRATCH
+; (sysvars.asm) is dedicated scratch space instead -- same mechanism,
+; just relocated.
+
+ push namespace core
+
+__DIVF: ; Division
+ PROC
+ LOCAL __DIVBYZERO
+ LOCAL TMP, ERR_SP
+
+TMP EQU DIVF_SCRATCH ; zx81sd: dedicated scratch, not DEST
+ERR_SP EQU DIVF_SCRATCH + 2 ; zx81sd: dedicated scratch, not ERR_SP
+
+ call __FPSTACK_PUSH2
+
+ ld hl, (ERR_SP)
+ ld (TMP), hl
+ ld hl, __DIVBYZERO
+ push hl
+ ld (ERR_SP), sp
+
+ ; ------------- ROM DIV
+ rst 28h
+ defb 01h ; EXCHANGE
+ defb 05h ; DIV
+ defb 38h; ; END CALC
+
+ pop hl
+ ld hl, (TMP)
+ ld (ERR_SP), hl
+
+ jp __FPSTACK_POP
+
+__DIVBYZERO:
+ ld hl, (TMP)
+ ld (ERR_SP), hl
+
+ ld a, ERROR_NumberTooBig
+ ld (ERR_NR), a
+
+ ; Returns 0 on DIV BY ZERO error
+ xor a
+ ld b, a
+ ld c, a
+ ld d, a
+ ld e, a
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/array/array.asm b/src/lib/arch/zx81sd/runtime/array/array.asm
new file mode 100644
index 000000000..ec71e353e
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/array/array.asm
@@ -0,0 +1,178 @@
+; vim: ts=4:et:sw=4:
+; Copyleft (K) by Jose M. Rodriguez de la Rosa
+; (a.k.a. Boriel)
+; http://www.boriel.com
+; -------------------------------------------------------------------
+; Simple array Index routine
+; Number of total indexes dimensions - 1 at beginning of memory
+; HL = Start of array memory (First two bytes contains N-1 dimensions)
+; Dimension values on the stack, (top of the stack, highest dimension)
+; E.g. A(2, 4) -> PUSH <4>; PUSH <2>
+
+; For any array of N dimension A(aN-1, ..., a1, a0)
+; and dimensions D[bN-1, ..., b1, b0], the offset is calculated as
+; O = [a0 + b0 * (a1 + b1 * (a2 + ... bN-2(aN-1)))]
+; What I will do here is to calculate the following sequence:
+; ((aN-1 * bN-2) + aN-2) * bN-3 + ...
+;
+; zx81sd override: the only change from zx48k's version is
+; LBOUND_PTR's storage address. The original uses the Spectrum ROM's
+; MEMBOT system variable (23698, safe scratch RAM on real hardware) --
+; on zx81sd that address falls inside the program's own compiled code
+; (block 2, $4000-$5FFF) instead of a real sysvar, silently corrupting
+; it on every multi-dimensional array access. ARRAY_SCRATCH (sysvars.asm)
+; is a dedicated 8-byte scratch area in the zx81sd sysvar block instead.
+
+#include once
+#include once
+
+#ifdef __CHECK_ARRAY_BOUNDARY__
+#include once
+#endif
+
+ push namespace core
+
+__ARRAY_PTR: ;; computes an array offset from a pointer
+ ld c, (hl)
+ inc hl
+ ld h, (hl)
+ ld l, c ;; HL <-- [HL]
+
+__ARRAY:
+ PROC
+
+ LOCAL LOOP
+ LOCAL ARRAY_END
+ LOCAL TMP_ARR_PTR ; Ptr to Array DATA region. Stored temporarily
+ LOCAL LBOUND_PTR, UBOUND_PTR ; LBound and UBound PTR indexes
+ LOCAL RET_ADDR ; Contains the return address popped from the stack
+
+LBOUND_PTR EQU ARRAY_SCRATCH ; zx81sd: dedicated scratch, not MEMBOT
+UBOUND_PTR EQU LBOUND_PTR + 2 ; Next 2 bytes for UBOUND PTR
+RET_ADDR EQU UBOUND_PTR + 2 ; Next 2 bytes for RET_ADDR
+TMP_ARR_PTR EQU RET_ADDR + 2 ; Next 2 bytes for TMP_ARR_PTR
+
+ ld e, (hl)
+ inc hl
+ ld d, (hl)
+ inc hl ; DE <-- PTR to Dim sizes table
+ ld (TMP_ARR_PTR), hl ; HL = Array __DATA__.__PTR__
+ inc hl
+ inc hl
+ ld c, (hl)
+ inc hl
+ ld b, (hl) ; BC <-- Array __LBOUND__ PTR
+ ld (LBOUND_PTR), bc ; Store it for later
+
+#ifdef __CHECK_ARRAY_BOUNDARY__
+ inc hl
+ ld c, (hl)
+ inc hl
+ ld b, (hl) ; BC <-- Array __UBOUND__ PTR
+ ld (UBOUND_PTR), bc
+#endif
+
+ ex de, hl ; HL <-- PTR to Dim sizes table, DE <-- dummy
+ ex (sp), hl ; Return address in HL, PTR Dim sizes table onto Stack
+ ld (RET_ADDR), hl ; Stores it for later
+
+ exx
+ pop hl ; Will use H'L' as the pointer to Dim sizes table
+ ld c, (hl) ; Loads Number of dimensions from (hl)
+ inc hl
+ ld b, (hl)
+ inc hl ; Ready
+ exx
+
+ ld hl, 0 ; HL = Element Offset "accumulator"
+
+LOOP:
+ ex de, hl ; DE = Element Offset
+ ld hl, (LBOUND_PTR)
+ ld a, h
+ or l
+ ld b, h
+ ld c, l
+ jr z, 1f
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ inc hl
+ ld (LBOUND_PTR), hl
+1:
+ pop hl ; Get next index (Ai) from the stack
+ sbc hl, bc ; Subtract LBOUND
+
+#ifdef __CHECK_ARRAY_BOUNDARY__
+ ld a, ERROR_SubscriptWrong
+ jp c, __ERROR
+ push hl ; Saves (Ai) - Lbound(i)
+ add hl, bc ; Recover original (Ai) value
+ push hl
+ ld hl, (UBOUND_PTR)
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ inc hl
+ ld (UBOUND_PTR), hl
+ pop hl ; original (Ai) value
+ scf
+ sbc hl, bc ; HL <- HL - BC - 1 = Ai - UBound(i) - 1 => No Carry if Ai > UBound(i)
+ jp nc, __ERROR
+ pop hl ; Recovers (Ai) - Lbound(Ai)
+#endif
+
+ add hl, de ; Adds current index
+
+ exx ; Checks if B'C' = 0
+ ld a, b ; Which means we must exit (last element is not multiplied by anything)
+ or c
+ jr z, ARRAY_END ; if B'Ci == 0 we are done
+ dec bc ; Decrements loop counter
+
+ ld e, (hl) ; Loads next dimension size into D'E'
+ inc hl
+ ld d, (hl)
+ inc hl
+ push de
+ exx
+
+ pop de ; DE = Max bound Number (i-th dimension)
+
+ call __FMUL16 ; HL <= HL * DE mod 65536
+ jp LOOP
+
+ARRAY_END:
+ ld a, (hl)
+ exx
+
+#ifdef __BIG_ARRAY__
+ ld d, 0
+ ld e, a
+ call __FMUL16
+#else
+ LOCAL ARRAY_SIZE_LOOP
+
+ ex de, hl
+ ld hl, 0
+ ld b, a
+ARRAY_SIZE_LOOP:
+ add hl, de
+ djnz ARRAY_SIZE_LOOP
+
+#endif
+
+ ex de, hl
+ ld hl, (TMP_ARR_PTR)
+ ld a, (hl)
+ inc hl
+ ld h, (hl)
+ ld l, a
+ add hl, de ; Adds element start
+ ld de, (RET_ADDR)
+ push de
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/beep.asm b/src/lib/arch/zx81sd/runtime/beep.asm
new file mode 100644
index 000000000..ac6c61f3a
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/beep.asm
@@ -0,0 +1,41 @@
+; BEEP — Pulso corto de beeper (clic de tecla)
+;
+; SD81 Booster en modo Superfast HiRes Spectrum emula el puerto ULA del
+; Spectrum en $FBh: bits 2-0 = color de borde, bits 4-3 = beeper (ver
+; border.asm y Apendice "Modo Spectrum" del manual del SD81 Booster).
+; Ambas funciones comparten el mismo puerto de solo escritura, asi que
+; se mantiene una copia sombra del ultimo byte escrito para poder
+; pulsar el beeper sin alterar el color de borde actual (y viceversa).
+
+ push namespace core
+
+SD81_ULA_PORT EQU $FB ; Puerto ULA emulado en modo HiRes Spectrum
+SD81_ULA_BEEP_BITS EQU $18 ; bits 4-3
+
+__ZX81SD_ULA_SHADOW:
+ DEFB 0
+
+; ---------------------------------------------------------------------------
+; __ZX81SD_KEYCLICK — Pulso breve de beeper (clic de tecla)
+; No recibe ni devuelve nada. Registros modificados: AF, BC.
+; ---------------------------------------------------------------------------
+__ZX81SD_KEYCLICK:
+ PROC
+ LOCAL CLICK_DELAY
+
+ ld hl, __ZX81SD_ULA_SHADOW
+ ld a, (hl)
+ or SD81_ULA_BEEP_BITS
+ out (SD81_ULA_PORT), a
+
+ ld b, 30
+CLICK_DELAY:
+ djnz CLICK_DELAY
+
+ ld a, (hl) ; restaura el byte previo (beeper apagado, borde intacto)
+ out (SD81_ULA_PORT), a
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/bootstrap.asm b/src/lib/arch/zx81sd/runtime/bootstrap.asm
new file mode 100644
index 000000000..b98741573
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/bootstrap.asm
@@ -0,0 +1,127 @@
+; BOOTSTRAP — Inicialización de las sysvars del runtime (stage 2, parte ASM)
+;
+; El hardware (paginación bloques 1-5, SP, DI) es inicializado directamente
+; por el prólogo del backend Python (emit_prologue), que emite esas
+; instrucciones como constantes de arquitectura conocidas en tiempo de
+; compilación.
+;
+; Esta rutina se ocupa de los valores por defecto de las sysvars en $8000+.
+; Se registra con #init para que el compilador inserte automáticamente
+; CALL SD81_INIT_SYSVARS en el prólogo, justo antes del salto al programa.
+
+#include once
+#include once
+
+; fp_calc.asm se incluye siempre (no solo cuando el programa usa FLOAT):
+; el vector RST $28h (src/arch/zx81sd/backend/main.py, emit_prologue) salta
+; incondicionalmente a FP_CALC_ENTRY, así que esa etiqueta debe existir en
+; todo binario, aunque el programa concreto no acabe generando código que
+; la invoque.
+#include once
+
+#init .core.SD81_INIT_SYSVARS
+
+ push namespace core
+
+; SD81_INIT_SYSVARS — Inicializa el bloque de variables del runtime en $8000
+SD81_INIT_SYSVARS:
+ PROC
+
+ ; CHARS apunta 256 bytes ANTES del inicio del font (convención Spectrum):
+ ; el runtime calcula glifo = CHARS + código*8, sin restar 32.
+ ; Así CHR$(32)=space → CHARS+256 = font[0], CHR$(72)='H' → CHARS+576 = font[40].
+ ld hl, __ZX81SD_CHARSET - 256
+ ld (CHARS), hl
+
+ ; UDG: área dedicada de 21 caracteres definibles (CHR$(144)-CHR$(164),
+ ; como en el Spectrum), reservada en charset.asm DESPUÉS de la fuente.
+ ; (La fuente sólo cubre CHR$(32)-CHR$(127): el antiguo "font+896"
+ ; apuntaba 128 bytes más allá de su final, sobre código del runtime,
+ ; y los POKE USR CHR$ de un programa lo corrompían.)
+ ld hl, __ZX81SD_UDG_AREA
+ ld (UDG), hl
+
+ ; Inicializa los UDGs con copias de las letras A-U (como la ROM del
+ ; Spectrum): glifo de 'A' = font + (65-32)*8 = font + 264.
+ ld hl, __ZX81SD_CHARSET + 264
+ ld de, __ZX81SD_UDG_AREA
+ ld bc, 21 * 8
+ ldir
+
+ ; Cursor al inicio de pantalla (columna=SCR_COLS, fila=SCR_ROWS)
+ ld a, SCR_ROWS
+ ld h, a
+ ld a, SCR_COLS
+ ld l, a
+ ld (S_POSN), hl
+
+ ; SCREEN_ADDR / SCREEN_ATTR_ADDR son variables RAM (no constantes EQU):
+ ; el runtime las lee con LD HL,(SCREEN_ADDR) para obtener la dirección.
+ ld hl, $C000
+ ld (SCREEN_ADDR), hl ; $801E ← $C000
+ ld (DFCC), hl ; cursor bitmap al inicio de pantalla
+
+ ld hl, $D800
+ ld (SCREEN_ATTR_ADDR), hl ; $8020 ← $D800
+ ld (DFCCL), hl ; cursor attrs al inicio de atributos
+
+ ; COORDS: último punto PLOT = (0,0)
+ xor a
+ ld (COORDS), a
+ ld (COORDS + 1), a
+
+ ; Atributos por defecto: tinta negra sobre fondo blanco (INK 0, PAPER 7)
+ ; $38 = 0b00111000 = PAPER 7 + INK 0, igual que el defecto del Spectrum
+ ld a, $38
+ ld (ATTR_P), a
+ xor a
+ ld (MASK_P), a ; $00 = sin transparencia (COPY_ATTR copia ATTR_P íntegro)
+ ld hl, $0038 ; ATTR_T=$38, MASK_T=$00
+ ld (ATTR_T), hl
+
+ ; Borra la pantalla física (bitmap + atributos). El bloque 6 es RAM
+ ; que sobrevive a un reset (no se limpia sola como al encender una
+ ; ROM real), así que sin esto un programa hereda el contenido que
+ ; dejó el anterior hasta que él mismo llame a CLS.
+ ld hl, $C000
+ ld (hl), 0
+ ld d, h
+ ld e, l
+ inc de
+ ld bc, 6143
+ ldir
+
+ ld hl, $D800
+ ld a, (ATTR_P)
+ ld (hl), a
+ ld d, h
+ ld e, l
+ inc de
+ ld bc, 767
+ ldir
+
+ ; Flags a cero
+ xor a
+ ld (FLAGS2), a
+ ld (P_FLAG), a
+ ld (TV_FLAG), a
+ ld (ERR_NR), a
+
+ ; Contadores a cero
+ ld hl, 0
+ ld (FRAMES), hl
+ ld (RANDOM_SEED_LOW), hl
+
+ ; Calculador de coma flotante (fp_calc.asm): pila FP vacía, MEM en su
+ ; posición por defecto (ver sysvars.asm)
+ ld hl, FP_CALC_STACK
+ ld (FP_STKBOT), hl
+ ld (FP_STKEND), hl
+ ld hl, FP_MEM_AREA
+ ld (FP_MEM), hl
+
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/border.asm b/src/lib/arch/zx81sd/runtime/border.asm
new file mode 100644
index 000000000..cb7f3ce6e
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/border.asm
@@ -0,0 +1,26 @@
+; BORDER — Cambia el color del borde
+; SD81 Booster en modo Superfast HiRes Spectrum emula el puerto ULA del
+; Spectrum en $FBh: bits 2-0 = color de borde, bits 4-3 = beeper.
+; El beeper (ver beep.asm) usa el mismo puerto, asi que se preservan
+; los bits 4-3 actuales (guardados en __ZX81SD_ULA_SHADOW) en vez de
+; sobrescribir el byte completo.
+;
+; Entrada: A = color de borde (bits 2-0, igual que en Spectrum)
+; Sustituye a: BORDER EQU $229Bh (ROM Spectrum)
+
+#include once
+
+ push namespace core
+
+BORDER:
+ and $07
+ ld b, a
+ ld hl, __ZX81SD_ULA_SHADOW
+ ld a, (hl)
+ and $18 ; conserva los bits de beeper
+ or b
+ ld (hl), a
+ out (SD81_ULA_PORT), a
+ ret
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/break.asm b/src/lib/arch/zx81sd/runtime/break.asm
new file mode 100644
index 000000000..73f0addc9
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/break.asm
@@ -0,0 +1,62 @@
+; CHECK_BREAK — Detección de la tecla BREAK (SHIFT + SPACE)
+; ZX81 + SD81 Booster
+;
+; Los puertos del teclado del ZX81 son idénticos a los del Spectrum
+; (puerto $FEh con líneas de dirección en el bus A8-A15).
+; TS_BRK ($8020h de la ROM Spectrum) lee las half-rows de CAPS SHIFT y SPACE;
+; aquí se reimplementa directamente sin depender de la ROM.
+;
+; Puerto $FEh:
+; A8 (línea 0, $FEFE) = SHIFT/Z/X/C/V
+; A12 (línea 7, $7FFE) = SPACE/./M/N/B ← SPACE está aquí, bit 0
+;
+; BREAK = CAPS SHIFT (bit 0 de $FEFE) + SPACE (bit 0 de $7FFE)
+
+#include once
+#include once
+
+ push namespace core
+
+CHECK_BREAK:
+ PROC
+ LOCAL TS_BRK, NO_BREAK
+
+ push af
+ call TS_BRK
+ jr c, NO_BREAK
+
+ ld a, ERROR_BreakIntoProgram
+ jp __ERROR
+
+NO_BREAK:
+ pop af
+ pop hl ; ret address
+ ex (sp), hl ; restaura HL original
+ ret
+
+; TS_BRK — Comprueba si BREAK está pulsado
+; Salida: carry set = no pulsado; carry clear = BREAK pulsado
+; Destruye: A
+TS_BRK:
+ ld a, $FE
+ in a, ($FE) ; half-row SPACE/./M/N/B (A12 high = $7FFE... )
+ ; Para leer la half-row de SPACE necesitamos A12=0, resto altos:
+ ; Dirección: $7FFE = 0111 1111 1111 1110b → A12=0, A8=1
+ ld a, $7F
+ in a, ($FE) ; lee half-row 7 (SPACE en bit 0)
+ rra ; bit 0 → carry (0 = pulsado)
+ jr c, NO_SPACE
+
+ ; SPACE pulsado — comprobar CAPS SHIFT
+ ld a, $FE ; A8=0 → half-row 0 (CAPS SHIFT en bit 0)
+ in a, ($FE)
+ rra ; bit 0 → carry (0 = pulsado)
+ ret ; carry clear = ambas pulsadas = BREAK
+
+NO_SPACE:
+ scf ; SPACE no pulsada → no es BREAK
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/charset.asm b/src/lib/arch/zx81sd/runtime/charset.asm
new file mode 100644
index 000000000..9cc665859
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/charset.asm
@@ -0,0 +1,20 @@
+; CHARSET — Fuente de caracteres 8x8 compatible Spectrum
+;
+; 96 caracteres × 8 bytes, desde CHR$(32) hasta CHR$(127).
+; Fichero externo: specfont.bin (debe estar en el mismo directorio).
+
+ push namespace core
+
+__ZX81SD_CHARSET:
+ INCBIN "specfont.bin"
+
+; Area de UDGs (CHR$(144) a CHR$(164), 21 caracteres como en el Spectrum).
+; La fuente solo cubre CHR$(32)-CHR$(127) (768 bytes): sin este bloque,
+; apuntar UDG a "font+896" caia 128 bytes MAS ALLA del final de la fuente,
+; sobre el codigo que tocara despues en el enlazado (los POKE USR CHR$
+; de un programa corrompian el runtime). SD81_INIT_SYSVARS la inicializa
+; con copias de las letras A-U, igual que la ROM del Spectrum.
+__ZX81SD_UDG_AREA:
+ defs 21 * 8
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/chr.asm b/src/lib/arch/zx81sd/runtime/chr.asm
new file mode 100644
index 000000000..a6942465e
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/chr.asm
@@ -0,0 +1,86 @@
+; CHR$(x, y, x) returns the string CHR$(x) + CHR$(y) + CHR$(z)
+;
+; zx81sd override: the only change from zx48k's version is TMP's storage
+; address. The original uses the Spectrum ROM's DEST system variable
+; (23629, safe scratch RAM on real hardware) to stash the return address
+; while it calls __MEM_ALLOC -- on zx81sd that address falls inside the
+; program's own compiled code (block 2, landed inside __DIVU32 in one
+; observed build) instead of a real sysvar. CHR_SCRATCH (sysvars.asm) is
+; a dedicated 2-byte scratch slot instead.
+
+#include once
+#include once
+
+ push namespace core
+
+CHR: ; Returns HL = Pointer to STRING (NULL if no memory)
+ ; Requires alloc.asm for dynamic memory heap.
+ ; Parameters: HL = Number of bytes to insert (already push onto the stack)
+ ; STACK => parameters (16 bit, only the High byte is considered)
+ ; Used registers A, A', BC, DE, HL, H'L'
+
+ PROC
+
+ LOCAL __POPOUT
+ LOCAL TMP
+
+TMP EQU CHR_SCRATCH ; zx81sd: dedicated scratch, not DEST
+
+ ld a, h
+ or l
+ ret z ; If Number of parameters is ZERO, return NULL STRING
+
+ ld b, h
+ ld c, l
+
+ pop hl ; Return address
+ ld (TMP), hl
+
+ push bc
+ inc bc
+ inc bc ; BC = BC + 2 => (2 bytes for the length number)
+ call __MEM_ALLOC
+ pop bc
+
+ ld d, h
+ ld e, l ; Saves HL in DE
+
+ ld a, h
+ or l
+ jr z, __POPOUT ; No Memory, return
+
+ ld (hl), c
+ inc hl
+ ld (hl), b
+ inc hl
+
+__POPOUT: ; Removes out of the stack every byte and return
+ ; If Zero Flag is set, don't store bytes in memory
+ ex af, af' ; Save Zero Flag
+
+ ld a, b
+ or c
+ jr z, __CHR_END
+
+ dec bc
+ pop af ; Next byte
+
+ ex af, af' ; Recovers Zero flag
+ jr z, __POPOUT
+
+ ex af, af' ; Saves Zero flag
+ ld (hl), a
+ inc hl
+ ex af, af' ; Recovers Zero Flag
+
+ jp __POPOUT
+
+__CHR_END:
+ ld hl, (TMP)
+ push hl ; Restores return addr
+ ex de, hl ; Recovers original HL ptr
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/draw.asm b/src/lib/arch/zx81sd/runtime/draw.asm
new file mode 100644
index 000000000..77537ddbc
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/draw.asm
@@ -0,0 +1,335 @@
+; DRAW using bresenhams algorithm and screen positioning — ZX81 + SD81 Booster
+; Copyleft (k) 2010 by J. Rodriguez (a.k.a. Boriel) http://www.boriel.com
+; Adaptado de zx48k/runtime/draw.asm para sustituir las llamadas ROM
+; por implementaciones propias (sin ROM Spectrum disponible).
+;
+; Cambios respecto al original zx48k:
+; - PIXEL_ADDR: llama a nuestra implementación en pixel_addr.asm
+; - COORDS: usa el valor de sysvars.asm ($8000+), no $5C7D
+
+; Y parameter in A
+; X parameter in high byte on top of the stack
+
+#include once
+#include once
+
+#include once
+#include once
+#include once
+
+#include once
+#include once
+#include once
+#include once
+
+;; DRAW PROCEDURE
+ push namespace core
+
+ PROC
+
+ LOCAL __DRAW1
+ LOCAL __DRAW2
+ LOCAL __DRAW3
+ LOCAL __DRAW4, __DRAW4_LOOP
+ LOCAL __DRAW5
+ LOCAL __DRAW6, __DRAW6_LOOP
+ LOCAL __DRAW_ERROR
+ LOCAL DX1, DX2, DY1, DY2
+ LOCAL __INCX, __INCY, __DECX, __DECY
+
+__DRAW_ERROR:
+ jp __OUT_OF_SCREEN_ERR
+
+DRAW:
+ ;; ENTRY POINT
+
+ LOCAL __DRAW_SETUP1, __DRAW_START, __PLOTOVER, __PLOTINVERSE
+
+ ex de, hl ; DE = Y OFFSET
+ pop hl ; return addr
+ ex (sp), hl ; CALLEE => HL = X OFFSET
+ ld bc, (COORDS)
+
+ ld a, c
+ add a, l
+ ld l, a
+ ld a, h
+ adc a, 0 ; HL = HL + C
+ ld h, a
+ jr nz, __DRAW_ERROR ; if a <> 0 => Out of Screen
+
+ ld a, b
+ add a, e
+ ld e, a
+ ld a, d
+ adc a, 0 ; DE = DE + B
+ ld d, a
+ jr nz, __DRAW_ERROR ; if a <> 0 => Out of Screen
+
+ ld a, 191
+ sub e
+ jr c, __DRAW_ERROR ; Out of screen
+
+ ld h, e ; now H,L = y2, x2
+
+__DRAW:
+ ; __FASTCALL__ Entry. Plots from (COORDS) to coord H, L
+ push hl
+ ex de, hl ; D,E = y2, x2;
+
+ ld a, (P_FLAG)
+ ld c, a
+ bit 2, a ; Test for INVERSE1
+ jr z, __DRAW_SETUP1
+ ld a, 2Fh ; CPL
+ ld (__PLOTINVERSE), a
+ ld a, 0A6h ; and (hl)
+ jp __DRAW_START
+
+__DRAW_SETUP1:
+ xor a ; nop
+ ld (__PLOTINVERSE), a
+ ld a, 0B6h ; or (hl)
+ bit 0, c ; Test for OVER
+ jr z, __DRAW_START
+ ld a, 0AEh ; xor (hl)
+
+__DRAW_START:
+ ld (__PLOTOVER), a ; "Pokes" last operation
+ exx
+ ld bc, (COORDS) ; B'C' = y1, x1
+ ld d, b ; Saves B' in D'
+ ld a, 191
+ call PIXEL_ADDR
+ res 6, h ; no-op en SD81 (H siempre $00-$17), mantiene compatibilidad
+
+ ;; Now gets pixel mask in A register
+ ld b, a
+ inc b
+ xor a
+ scf
+ LOCAL __PIXEL_MASK
+__PIXEL_MASK:
+ rra
+ djnz __PIXEL_MASK
+
+ ld b, d ; Restores B' from D'
+ ld de, (SCREEN_ADDR)
+ add hl, de
+ pop de ; D'E' = y2, x2
+ exx ; At this point: D'E' = y2,x2 coords
+ ; B'C' = y1, y1 coords
+ ; H'L' = Screen Address of pixel
+
+ ex af, af' ; Saves A reg for later
+ ; A' = Pixel mask
+
+ ld bc, (COORDS) ; B,C = y1, x1
+
+ ld a, e
+ sub c ; dx = X2 - X1
+ ld c, a ; Saves dx in c
+
+ ld a, 0Ch ; INC C opcode
+ ld hl, __INCX ; xi = 1
+ jr nc, __DRAW1
+
+ ld a, c
+ neg ; dx = X1 - X2
+ ld c, a
+ ld a, 0Dh ; DEC C opcode
+ ld hl, __DECX ; xi = -1
+
+__DRAW1:
+ ld (DX1), a
+ ld (DX1 + 2), hl ; Updates DX1 call address
+ ld (DX2), a
+ ld (DX2 + 2), hl ; Updates DX2 call address
+
+ ld a, d
+ sub b ; dy = Y2 - Y1
+ ld b, a ; Saves dy in b
+
+ ld a, 4 ; INC B opcode
+ ld hl, __INCY ; y1 = 1
+ jr nc, __DRAW2
+
+ ld a, b
+ neg
+ ld b, a ; dy = Y2 - Y1
+ ld a, 5 ; DEC B opcode
+ ld hl, __DECY ; y1 = -1
+
+__DRAW2:
+ ld (DY1), a
+ ld (DY1 + 2), hl ; Updates DX1 call address
+ ld (DY2), a
+ ld (DY2 + 2), hl ; Updates DX2 call address
+
+ ld a, b
+ sub c ; dy - dx
+ jr c, __DRAW_DX_GT_DY ; DX > DY
+
+ ; At this point DY >= DX
+ ; --------------------------
+ ; HL = error = dY / 2
+ ld h, 0
+ ld l, b
+ srl l
+
+ ; DE = -dX
+ xor a
+ sub c
+ ld e, a
+ sbc a, a
+ ld d, a
+
+ ; BC = DY
+ ld c, b
+ ld b, h
+
+ exx
+ scf ; Sets Carry to signal update ATTR
+ ex af, af' ; Brings back pixel mask
+ ld e, a ; Saves it in free E register
+ jp __DRAW4_LOOP
+
+__DRAW3: ; While c != e => while y != y2
+ exx
+ add hl, de ; error -= dX
+ bit 7, h ;
+ exx ; recover coordinates
+ jr z, __DRAW4 ; if error < 0
+
+ exx
+ add hl, bc ; error += dY
+ exx
+
+ ld a, e
+DX1: ; x += xi
+ inc c
+ call __INCX ; This address will be dynamically updated
+ ld e, a
+
+__DRAW4:
+
+DY1: ; y += yi
+ inc b
+ call __INCY ; This address will be dynamically updated
+ ld a, e ; Restores A reg.
+ call __FASTPLOT
+
+__DRAW4_LOOP:
+ ld a, b
+ cp d
+ jp nz, __DRAW3
+ ld (COORDS), bc
+ ret
+
+__DRAW_DX_GT_DY: ; DX > DY
+ ; --------------------------
+ ; HL = error = dX / 2
+ ld h, 0
+ ld l, c
+ srl l ; HL = error = DX / 2
+
+ ; DE = -dY
+ xor a
+ sub b
+ ld e, a
+ sbc a, a
+ ld d, a
+
+ ; BC = dX
+ ld b, h
+
+ exx
+ ld d, e
+ scf ; Sets Carry to signal update ATTR
+ ex af, af' ; Brings back pixel mask
+ ld e, a ; Saves it in free E register
+ jp __DRAW6_LOOP
+
+__DRAW5: ; While loop
+ exx
+ add hl, de ; error -= dY
+ bit 7, h ; if (error < 0)
+ exx ; Restore coords
+ jr z, __DRAW6 ;
+ exx
+ add hl, bc ; error += dX
+ exx
+
+DY2: ; y += yi
+ inc b
+ call __INCY ; This address will be dynamically updated
+
+__DRAW6:
+ ld a, e
+DX2: ; x += xi
+ inc c
+ call __INCX ; This address will be dynamically updated
+ ld e, a
+ call __FASTPLOT
+
+__DRAW6_LOOP:
+ ld a, c ; Current X coord
+ cp d
+ jp nz, __DRAW5
+ ld (COORDS), bc
+ ret
+
+__DRAW_END:
+ exx
+ ret
+
+ ;; Given a A mask and an HL screen position
+ ;; return the next left position
+ ;; Also updates BC coords
+__DECX EQU SP.PixelLeft
+
+ ;; Like the above, but to the RIGHT
+ ;; Also updates BC coords
+__INCX EQU SP.PixelRight
+
+ ;; Given an HL screen position, calculates
+ ;; the above position
+ ;; Also updates BC coords
+__INCY EQU SP.PixelUp
+
+ ;; Given an HL screen position, calculates
+ ;; the above position
+ ;; Also updates BC coords
+__DECY EQU SP.PixelDown
+
+ ;; Puts the A register MASK in (HL)
+__FASTPLOT:
+__PLOTINVERSE:
+ nop ; Replace with CPL if INVERSE 1
+__PLOTOVER:
+ or (hl) ; Replace with XOR (hl) if OVER 1 AND INVERSE 0
+ ; Replace with AND (hl) if INVERSE 1
+
+ ld (hl), a
+ ex af, af' ; Recovers flag. If Carry set => update ATTR
+ ld a, e ; Recovers A reg
+ ret nc
+
+ push hl
+ push de
+ push bc
+ call SET_PIXEL_ADDR_ATTR
+ pop bc
+ pop de
+ pop hl
+
+ LOCAL __FASTPLOTEND
+__FASTPLOTEND:
+ or a ; Resets carry flag
+ ex af, af' ; Recovers A reg
+ ld a, e
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/draw3.asm b/src/lib/arch/zx81sd/runtime/draw3.asm
new file mode 100644
index 000000000..e70bf27ef
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/draw3.asm
@@ -0,0 +1,465 @@
+; draw3.asm (zx81sd) — DRAW en modo arco (DRAW x, y, angulo)
+;
+; Sustituye a zx48k/runtime/draw3.asm, que llama directamente a direcciones
+; fijas de la ROM del Spectrum (STACK-A/STACK-BC en $2D28/$2D2Bh, STK-TO-A en
+; $2314h, STK-TO-BC en $2307h, CD-PRMS1 en $247Dh) para nada de eso hay ROM
+; mapeada en zx81sd. Aqui se usan las mismas rutinas, pero ya portadas como
+; codigo propio en fp_calc.asm (Fases 1, 4 y 5). El resto del algoritmo (que
+; es codigo bytecode del calculador: rst 28h + literales) es identico al
+; original, ya que rst 28h ya apunta a nuestro CALCULATE propio.
+
+#include once
+#include once
+#include once
+#include once
+#include once
+#include once
+
+ push namespace core
+
+DRAW3:
+ PROC
+ LOCAL L2477
+ LOCAL L2420
+ LOCAL L2439
+ LOCAL L245F
+ LOCAL L23C1
+ LOCAL SUM_C, SUM_B
+ LOCAL DR_SIN_NZ
+
+ exx
+ ex af, af' ;; Preserves ARC
+ pop hl
+ pop de
+ ex (sp), hl ;; CALLEE
+ push de
+ call __FPSTACK_I16 ;; X Offset
+ pop hl
+ call __FPSTACK_I16 ;; Y Offset
+ exx
+ ex af, af'
+ call __FPSTACK_PUSH ;; R Arc
+
+; Now enter the calculator and store the complete rotation angle in mem-5
+
+ RST 28H ;; FP-CALC x, y, A.
+ DEFB $C5 ;;st-mem-5 x, y, A.
+
+; Test the angle for the special case of 360 degrees.
+
+ DEFB $A2 ;;stk-half x, y, A, 1/2.
+ DEFB $04 ;;multiply x, y, A/2.
+ DEFB $1F ;;sin x, y, sin(A/2).
+ DEFB $31 ;;duplicate x, y, sin(A/2),sin(A/2)
+ DEFB $30 ;;not x, y, sin(A/2), (0/1).
+ DEFB $30 ;;not x, y, sin(A/2), (1/0).
+ DEFB $00 ;;jump-true x, y, sin(A/2).
+
+ DEFB (DR_SIN_NZ - $) & 0FFh
+ ;;if sin(r/2) is not zero.
+
+; The third parameter is 2*PI (or a multiple of 2*PI) so a 360 degrees turn
+; would just be a straight line. Eliminating this case here prevents
+; division by zero at later stage.
+
+ DEFB $02 ;;delete x, y.
+ DEFB $38 ;;end-calc x, y.
+ JP L2477
+
+; ---
+
+; An arc can be drawn.
+
+DR_SIN_NZ:
+ DEFB $C0 ;;st-mem-0 x, y, sin(A/2). store mem-0
+ DEFB $02 ;;delete x, y.
+
+; The next step calculates (roughly) the diameter of the circle of which the
+; arc will form part.
+
+ DEFB $C1 ;;st-mem-1 x, y. store mem-1
+ DEFB $02 ;;delete x.
+
+ DEFB $31 ;;duplicate x, x.
+ DEFB $2A ;;abs x, x (+ve).
+ DEFB $E1 ;;get-mem-1 x, X, y.
+ DEFB $01 ;;exchange x, y, X.
+ DEFB $E1 ;;get-mem-1 x, y, X, y.
+ DEFB $2A ;;abs x, y, X, Y (+ve).
+ DEFB $0F ;;addition x, y, X+Y.
+ DEFB $E0 ;;get-mem-0 x, y, X+Y, sin(A/2).
+ DEFB $05 ;;division x, y, X+Y/sin(A/2).
+ DEFB $2A ;;abs x, y, X+Y/sin(A/2) = D.
+
+ DEFB $E0 ;;get-mem-0 x, y, D, sin(A/2).
+ DEFB $01 ;;exchange x, y, sin(A/2), D.
+
+ DEFB $38 ;;end-calc x, y, sin(A/2), D.
+
+; The next test avoids drawing 4 straight lines when the start and end pixels
+; are adjacent (or the same).
+
+ LD A,(HL) ; fetch exponent byte of D.
+ CP $81 ; compare to 1
+ JR NC,L23C1 ; forward, if > 1, to DR-PRMS
+
+; else delete the top two stack values and draw a simple straight line.
+
+ RST 28H ;; FP-CALC
+ DEFB $02 ;;delete
+ DEFB $02 ;;delete
+ DEFB $38 ;;end-calc x, y.
+
+ JP L2477 ; to LINE-DRAW
+
+; ---
+
+; The ARC will consist of multiple straight lines so call CD-PRMS1 to
+; pre-calculate sine values from the angle (in mem-5) and determine also
+; the number of straight lines from that value and the 'diameter' which
+; is at the top of the calculator stack.
+
+#ifdef DRAW3_DEBUG
+DRAW3_BP_PRMS: ; breakpoint: justo antes de llamar a CD-PRMS1
+#endif
+L23C1: CALL L247D ; routine CD-PRMS1
+
+ ; mem-0 ; (A)/No. of lines (=a) (step angle)
+ ; mem-1 ; sin(a/2)
+ ; mem-2 ; -
+ ; mem-3 ; cos(a) const
+ ; mem-4 ; sin(a) const
+ ; mem-5 ; Angle of rotation (A) in
+ ; B ; Count of straight lines - max 252.
+
+ PUSH BC ; Save the line count on the machine stack.
+
+; Remove the now redundant diameter value D.
+
+ RST 28H ;; FP-CALC x, y, sin(A/2), D.
+ DEFB $02 ;;delete x, y, sin(A/2).
+
+; Dividing the sine of the step angle by the sine of the total angle gives
+; the length of the initial chord on a unary circle.
+
+ DEFB $E1 ;;get-mem-1 x, y, sin(A/2), sin(a/2)
+ DEFB $01 ;;exchange x, y, sin(a/2), sin(A/2)
+ DEFB $05 ;;division x, y, sin(a/2)/sin(A/2)
+ DEFB $C1 ;;st-mem-1 x, y. f.
+ DEFB $02 ;;delete x, y.
+
+; With the factor stored, scale the x coordinate first.
+
+ DEFB $01 ;;exchange y, x.
+ DEFB $31 ;;duplicate y, x, x.
+ DEFB $E1 ;;get-mem-1 y, x, x, f.
+ DEFB $04 ;;multiply y, x, x*f (=xx)
+ DEFB $C2 ;;st-mem-2 y, x, xx.
+ DEFB $02 ;;delete y. x.
+
+; Now scale the y coordinate.
+
+ DEFB $01 ;;exchange x, y.
+ DEFB $31 ;;duplicate x, y, y.
+ DEFB $E1 ;;get-mem-1 x, y, y, f
+ DEFB $04 ;;multiply x, y, y*f (=yy)
+
+; 'sin' and 'cos' trash locations mem-0 to mem-2 so fetch mem-2 to the
+; calculator stack for safe keeping.
+
+ DEFB $E2 ;;get-mem-2 x, y, yy, xx.
+
+; Rotate the first arc through (A-a)/2 radians.
+;
+; xRotated = y * sin(angle) + x * cos(angle)
+; yRotated = y * cos(angle) - x * sin(angle)
+;
+
+ DEFB $E5 ;;get-mem-5 x, y, yy, xx, A.
+ DEFB $E0 ;;get-mem-0 x, y, yy, xx, A, a.
+ DEFB $03 ;;subtract x, y, yy, xx, A-a.
+ DEFB $A2 ;;stk-half x, y, yy, xx, A-a, 1/2.
+ DEFB $04 ;;multiply x, y, yy, xx, (A-a)/2. (=angle)
+ DEFB $31 ;;duplicate x, y, yy, xx, angle, angle.
+ DEFB $1F ;;sin x, y, yy, xx, angle, sin(angle)
+ DEFB $C5 ;;st-mem-5 x, y, yy, xx, angle, sin(angle)
+ DEFB $02 ;;delete x, y, yy, xx, angle
+
+ DEFB $20 ;;cos x, y, yy, xx, cos(angle).
+
+; mem-0, mem-1 and mem-2 can be used again now...
+
+ DEFB $C0 ;;st-mem-0 x, y, yy, xx, cos(angle).
+ DEFB $02 ;;delete x, y, yy, xx.
+
+ DEFB $C2 ;;st-mem-2 x, y, yy, xx.
+ DEFB $02 ;;delete x, y, yy.
+
+ DEFB $C1 ;;st-mem-1 x, y, yy.
+ DEFB $E5 ;;get-mem-5 x, y, yy, sin(angle)
+ DEFB $04 ;;multiply x, y, yy*sin(angle).
+ DEFB $E0 ;;get-mem-0 x, y, yy*sin(angle), cos(angle)
+ DEFB $E2 ;;get-mem-2 x, y, yy*sin(angle), cos(angle), xx.
+ DEFB $04 ;;multiply x, y, yy*sin(angle), xx*cos(angle).
+ DEFB $0F ;;addition x, y, xRotated.
+ DEFB $E1 ;;get-mem-1 x, y, xRotated, yy.
+ DEFB $01 ;;exchange x, y, yy, xRotated.
+ DEFB $C1 ;;st-mem-1 x, y, yy, xRotated.
+ DEFB $02 ;;delete x, y, yy.
+
+ DEFB $E0 ;;get-mem-0 x, y, yy, cos(angle).
+ DEFB $04 ;;multiply x, y, yy*cos(angle).
+ DEFB $E2 ;;get-mem-2 x, y, yy*cos(angle), xx.
+ DEFB $E5 ;;get-mem-5 x, y, yy*cos(angle), xx, sin(angle).
+ DEFB $04 ;;multiply x, y, yy*cos(angle), xx*sin(angle).
+ DEFB $03 ;;subtract x, y, yRotated.
+ DEFB $C2 ;;st-mem-2 x, y, yRotated.
+
+; Now the initial x and y coordinates are made positive and summed to see
+; if they measure up to anything significant.
+
+ DEFB $2A ;;abs x, y, yRotated'.
+ DEFB $E1 ;;get-mem-1 x, y, yRotated', xRotated.
+ DEFB $2A ;;abs x, y, yRotated', xRotated'.
+ DEFB $0F ;;addition x, y, yRotated+xRotated.
+ DEFB $02 ;;delete x, y.
+
+ DEFB $38 ;;end-calc x, y.
+
+; Although the test value has been deleted it is still above the calculator
+; stack in memory and conveniently DE which points to the first free byte
+; addresses the exponent of the test value.
+
+ LD A,(DE) ; Fetch exponent of the length indicator.
+ CP $81 ; Compare to that for 1
+
+ POP BC ; Balance the machine stack
+
+ JP C,L2477 ; forward, if the coordinates of first line
+ ; don't add up to more than 1, to LINE-DRAW
+
+; Continue when the arc will have a discernable shape.
+
+ PUSH BC ; Restore line counter to the machine stack.
+
+; The parameters of the DRAW command were relative and they are now converted
+; to absolute coordinates by adding to the coordinates of the last point
+; plotted.
+
+ RST 28H ;; FP-CALC x, y.
+ DEFB $01 ;;exchange y, x.
+ DEFB $38 ;;end-calc y, x.
+
+ LD A,(COORDS) ;; Fetch System Variable COORDS-x
+ CALL L2D28 ;; routine STACK-A
+
+ RST 28H ;; FP-CALC y, x, last-x.
+
+; Store the last point plotted to initialize the moving ax value.
+
+ DEFB $C0 ;;st-mem-0 y, x, last-x.
+ DEFB $0F ;;addition y, absolute x.
+ DEFB $01 ;;exchange tx, y.
+ DEFB $38 ;;end-calc tx, y.
+
+ LD A,(COORDS + 1) ; Fetch System Variable COORDS-y
+ CALL L2D28 ; routine STACK-A
+
+ RST 28H ;; FP-CALC tx, y, last-y.
+
+; Store the last point plotted to initialize the moving ay value.
+
+ DEFB $C5 ;;st-mem-5 tx, y, last-y.
+ DEFB $0F ;;addition tx, ty.
+
+; Fetch the moving ax and ay to the calculator stack.
+
+ DEFB $E0 ;;get-mem-0 tx, ty, ax.
+ DEFB $E5 ;;get-mem-5 tx, ty, ax, ay.
+ DEFB $38 ;;end-calc tx, ty, ax, ay.
+
+ POP BC ; Restore the straight line count.
+
+; -----------------------------------
+; THE 'CIRCLE/DRAW CONVERGENCE POINT'
+; -----------------------------------
+
+L2420:
+ DEC B ; decrement the arc count (4,8,12,16...).
+
+ JP L2439 ; forward to ARC-START
+
+; --------------
+; THE 'ARC LOOP'
+; --------------
+
+L2425:
+#ifdef DRAW3_DEBUG
+DRAW3_BP_ARCLOOP: ; breakpoint: inicio de cada vuelta del bucle (rotacion)
+#endif
+ RST 28H ;; FP-CALC
+ DEFB $E1 ;;get-mem-1 rx.
+ DEFB $31 ;;duplicate rx, rx.
+ DEFB $E3 ;;get-mem-3 cos(a)
+ DEFB $04 ;;multiply rx, rx*cos(a).
+ DEFB $E2 ;;get-mem-2 rx, rx*cos(a), ry.
+ DEFB $E4 ;;get-mem-4 rx, rx*cos(a), ry, sin(a).
+ DEFB $04 ;;multiply rx, rx*cos(a), ry*sin(a).
+ DEFB $03 ;;subtract rx, rx*cos(a) - ry*sin(a)
+ DEFB $C1 ;;st-mem-1 rx, new relative x rotated.
+ DEFB $02 ;;delete rx.
+
+ DEFB $E4 ;;get-mem-4 rx, sin(a).
+ DEFB $04 ;;multiply rx*sin(a)
+ DEFB $E2 ;;get-mem-2 rx*sin(a), ry.
+ DEFB $E3 ;;get-mem-3 rx*sin(a), ry, cos(a).
+ DEFB $04 ;;multiply rx*sin(a), ry*cos(a).
+ DEFB $0F ;;addition rx*sin(a) + ry*cos(a).
+ DEFB $C2 ;;st-mem-2 new relative y rotated.
+ DEFB $02 ;;delete .
+ DEFB $38 ;;end-calc .
+
+;; ARC-START
+L2439:
+#ifdef DRAW3_DEBUG
+DRAW3_BP_ARCSTART: ; breakpoint: calculo de Dx/Dy de cada segmento
+#endif
+ PUSH BC ; Preserve the arc counter on the machine stack.
+
+ RST 28H ;; FP-CALC ax, ay.
+ DEFB $C0 ;;st-mem-0 ax, ay.
+ DEFB $02 ;;delete ax.
+
+ DEFB $E1 ;;get-mem-1 ax, xr.
+ DEFB $0F ;;addition ax+xr (= new ax).
+ DEFB $31 ;;duplicate ax, ax.
+ DEFB $38 ;;end-calc ax, ax.
+
+ LD A,(COORDS) ; COORDS-x last x (integer ix 0-255)
+ CALL L2D28 ; routine STACK-A
+
+ RST 28H ;; FP-CALC ax, ax, ix.
+ DEFB $03 ;;subtract ax, ax-ix = relative DRAW Dx.
+
+ DEFB $E0 ;;get-mem-0 ax, Dx, ay.
+ DEFB $E2 ;;get-mem-2 ax, Dx, ay, ry.
+ DEFB $0F ;;addition ax, Dx, ay+ry (= new ay).
+ DEFB $C0 ;;st-mem-0 ax, Dx, ay.
+ DEFB $01 ;;exchange ax, ay, Dx,
+ DEFB $E0 ;;get-mem-0 ax, ay, Dx, ay.
+ DEFB $38 ;;end-calc ax, ay, Dx, ay.
+
+ LD A,(COORDS + 1) ; COORDS-y last y (integer iy 0-175)
+ CALL L2D28 ; routine STACK-A
+
+ RST 28H ;; FP-CALC ax, ay, Dx, ay, iy.
+ DEFB $03 ;;subtract ax, ay, Dx, ay-iy ( = Dy).
+ DEFB $38 ;;end-calc ax, ay, Dx, Dy.
+
+ CALL L2477 ; Routine DRAW-LINE draws (Dx,Dy) relative to
+ ; the last pixel plotted leaving absolute x
+ ; and y on the calculator stack.
+ ; ax, ay.
+
+ POP BC ; Restore the arc counter from the machine stack.
+
+ DJNZ L2425 ; Decrement and loop while > 0 to ARC-LOOP
+
+; -------------
+; THE 'ARC END'
+; -------------
+
+L245F:
+#ifdef DRAW3_DEBUG
+DRAW3_BP_ARCEND: ; breakpoint: ultimo segmento hasta el punto final
+#endif
+ RST 28H ;; FP-CALC tx, ty, ax, ay.
+ DEFB $02 ;;delete tx, ty, ax.
+ DEFB $02 ;;delete tx, ty.
+ DEFB $01 ;;exchange ty, tx.
+ DEFB $38 ;;end-calc ty, tx.
+
+; First calculate the relative x coordinate to the end-point.
+
+ LD A,(COORDS) ; COORDS-x
+ CALL L2D28 ; routine STACK-A
+
+ RST 28H ;; FP-CALC ty, tx, coords_x.
+ DEFB $03 ;;subtract ty, rx.
+
+; Next calculate the relative y coordinate to the end-point.
+
+ DEFB $01 ;;exchange rx, ty.
+ DEFB $38 ;;end-calc rx, ty.
+
+ LD A,(COORDS + 1) ; COORDS-y
+ CALL L2D28 ; routine STACK-A
+
+ RST 28H ;; FP-CALC rx, ty, coords_y
+ DEFB $03 ;;subtract rx, ry.
+ DEFB $38 ;;end-calc rx, ry.
+; Finally draw the last straight line.
+L2477:
+#ifdef DRAW3_DEBUG
+DRAW3_BP_LINE: ; breakpoint: aqui B,C,D,E = linea que se va a dibujar
+#endif
+ call L2307 ;;Pops x, and y, and stores it in B, C
+
+#ifdef DRAW3_DEBUG
+ push af
+ push hl
+ ld a, (DRAW3_DEBUG_N)
+ cp 16
+ jr nc, DRAW3_DEBUG_SKIP2
+ ld hl, (DRAW3_DEBUG_PTR)
+ ld (hl), b ; Y magnitude
+ inc hl
+ ld (hl), c ; X magnitude
+ inc hl
+ ld (hl), d ; Y sign (bit7 via RL later)
+ inc hl
+ ld (hl), e ; X sign (bit7 via RL later)
+ inc hl
+ ld a, (COORDS)
+ ld (hl), a ; COORDS-x before this line
+ inc hl
+ ld a, (COORDS + 1)
+ ld (hl), a ; COORDS-y before this line
+ inc hl
+ ld (DRAW3_DEBUG_PTR), hl
+ ld a, (DRAW3_DEBUG_N)
+ inc a
+ ld (DRAW3_DEBUG_N), a
+DRAW3_DEBUG_SKIP2:
+ pop hl
+ pop af
+#endif
+
+ ld hl, (COORDS) ;;Calculates x2 and y2 in L, H
+
+ rl e ;; Rotate left to carry
+ ld a, c
+ jr nc, SUM_C
+ neg
+SUM_C:
+ add a, l
+ ld l, a ;; X2
+
+ rl d ;; Low sign to carry
+ ld a, b
+ jr nc, SUM_B
+ neg
+SUM_B:
+ add a, h
+ ld h, a
+ jp __DRAW ;;forward to LINE-DRAW (Fastcalled)
+
+ ENDP
+
+#ifdef DRAW3_DEBUG
+DRAW3_DEBUG_N: defb 0
+DRAW3_DEBUG_PTR: defw DRAW3_DEBUG_BUF
+DRAW3_DEBUG_BUF: defs 16 * 6
+#endif
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/error.asm b/src/lib/arch/zx81sd/runtime/error.asm
new file mode 100644
index 000000000..d0dd52c0a
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/error.asm
@@ -0,0 +1,40 @@
+; Simple error control routines — ZX81 + SD81 Booster
+; Sustituye RST 8 (error handler de la ROM Spectrum) por una parada
+; limpia: guarda el código de error en ERR_NR y detiene la CPU.
+; Las interrupciones ya están desactivadas (DI desde el bootstrap).
+
+#include once
+
+ push namespace core
+
+; Códigos de error (compatibles con el manual del ZX Spectrum)
+ERROR_Ok EQU -1
+ERROR_SubscriptWrong EQU 2
+ERROR_OutOfMemory EQU 3
+ERROR_OutOfScreen EQU 4
+ERROR_NumberTooBig EQU 5
+ERROR_InvalidArg EQU 9
+ERROR_IntOutOfRange EQU 10
+ERROR_NonsenseInBasic EQU 11
+ERROR_InvalidFileName EQU 14
+ERROR_InvalidColour EQU 19
+ERROR_BreakIntoProgram EQU 20
+ERROR_TapeLoadingErr EQU 26
+
+; __ERROR — Detiene la ejecución con un código de error en A.
+; Sustituye a: RST 8 (ROM Spectrum)
+__ERROR:
+ ld (__ERROR_CODE), a
+ ld (ERR_NR), a ; guardar en sysvar
+ di ; asegurar interrupciones desactivadas
+ halt ; detener CPU
+__ERROR_CODE:
+ nop ; byte de código de error (para compatibilidad con llamadores)
+ ret ; no se alcanza, pero mantiene la estructura original
+
+; __STOP — Guarda el código de error y continúa (para END del programa).
+__STOP:
+ ld (ERR_NR), a
+ ret
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/fp_calc.asm b/src/lib/arch/zx81sd/runtime/fp_calc.asm
new file mode 100644
index 000000000..adc53f1c7
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/fp_calc.asm
@@ -0,0 +1,1996 @@
+; ===========================================================================
+; fp_calc.asm — Calculador de coma flotante (RST $28h) para ZX81 + SD81 Booster
+;
+; Port del calculador de la ROM del ZX Spectrum 48K (CALCULATE, $335B, y las
+; rutinas de las que depende) a código propio en RAM, ya que en zx81sd no hay
+; ninguna ROM mapeada en tiempo de ejecucion (el bloque 0 lo ocupa entero
+; nuestro binario compilado).
+;
+; Gracias a este port, todo el runtime de coma flotante de zx48k (addf.asm,
+; subf.asm, mulf.asm, divf.asm, negf.asm, cmp/*.asm, bool/*.asm, math/*.asm,
+; str.asm, printf.asm, val.asm...) funciona TAL CUAL, sin modificar ni un
+; solo fichero: todos ellos hacen "rst 28h" seguido de bytes de "literal" de
+; operacion calculadora, y ahora $0028 contiene codigo real (ver
+; src/arch/zx81sd/backend/main.py, emit_prologue) en vez de un DI/HALT.
+;
+; FASE 1 (este fichero): motor CALCULATE + pila de numeros FP + aritmetica
+; (suma/resta/multiplicacion/division) + comparaciones numericas + booleanas
+; (AND/OR/NOT) + funciones unarias basicas (ABS/NEGATE/SGN/INT/truncate).
+; Pendiente en fases posteriores: SIN/COS/TAN/ASN/ACS/ATN/LN/EXP/SQR (usan el
+; "series generator" ya portado aqui, mas la tabla de coeficientes de cada
+; funcion, que aun no esta), STR$ (conversion numero->texto) y VAL (parseo de
+; texto->numero, con semantica simplificada: solo literales decimales, no
+; evaluacion de expresiones completas como hace la ROM real — ver conversacion
+; de diseño).
+;
+; Formato de los numeros (5 bytes), identico al de la ROM Spectrum:
+; Entero pequeño: byte1=$00, byte2=signo($00/$FF), byte3=lo, byte4=hi, byte5=$00
+; Coma flotante: byte1=exponente sesgado(+$80), byte2..5=mantisa de 32 bits
+; con el bit 7 del byte2 usado como signo (bit de mantisa
+; implicito, siempre a 1 salvo cuando se usa para el signo)
+;
+; Referencia: disassembly comentado de la ROM del ZX Spectrum 48K
+; (C:\ClaudeCode\ZXBASIC-SD81\Spectrum48.asm), seccion "FLOATING-POINT
+; CALCULATOR". Las etiquetas Lxxxx conservan la direccion original de la ROM
+; (solo como identificador legible, no como direccion real: aqui son
+; reubicables) para facilitar la referencia cruzada con el disassembly.
+; ===========================================================================
+
+#include once
+#include once
+#include once
+; fp_calc.asm se incluye siempre (no solo cuando el programa usa FLOAT):
+; ver src/arch/zx81sd/backend/main.py. Por eso debe bastarse a si mismo e
+; incluir aqui stackf.asm (de donde vienen __FPSTACK_PUSH/__FPSTACK_POP),
+; en vez de depender de que el fichero que lo incluya lo haga tambien.
+
+ push namespace core
+
+; ---------------------------------------------------------------------------
+; Punto de entrada — sustituye a RST $28h
+; ---------------------------------------------------------------------------
+FP_CALC_ENTRY:
+ jp L335B
+
+; ---------------------------------------------------------------------------
+; TEST-ROOM propio — sustituye a TEST-ROOM ($1F05, que comprobaba espacio
+; libre contra el puntero de pila SP). Aqui la pila de numeros FP es un
+; buffer fijo (FP_CALC_STACK..FP_CALC_STACK_END en sysvars.asm), asi que
+; basta comprobar que FP_STKEND + BC no se sale del buffer.
+; Entrada: BC = bytes requeridos. Sale con BC intacto si hay espacio.
+; ---------------------------------------------------------------------------
+CALC_TEST_ROOM:
+ push hl
+ push de
+ ld hl, (FP_STKEND)
+ add hl, bc
+ ld de, FP_CALC_STACK_END
+ or a
+ sbc hl, de
+ pop de
+ pop hl
+ jr c, CALC_TEST_ROOM_OK
+ ld a, ERROR_OutOfMemory
+ jp __ERROR
+CALC_TEST_ROOM_OK:
+ ret
+
+; ---------------------------------------------------------------------------
+; THE 'TEST FIVE SPACES' SUBROUTINE ($33A9 TEST-5-SP)
+; ---------------------------------------------------------------------------
+L33A9:
+ push de
+ push hl
+ ld bc, 5
+ call CALC_TEST_ROOM
+ pop hl
+ pop de
+ ret
+
+; ---------------------------------------------------------------------------
+; STACK-NUM ($33B4) — apila un numero de 5 bytes apuntado por HL
+; ---------------------------------------------------------------------------
+L33B4:
+ ld de, (FP_STKEND)
+ call L33C0
+ ld (FP_STKEND), de
+ ret
+
+; ---------------------------------------------------------------------------
+; MOVE-FP / duplicate (literal $31, $33C0)
+; ---------------------------------------------------------------------------
+L33C0:
+ call L33A9
+ ldir
+ ret
+
+; ---------------------------------------------------------------------------
+; stk-data (literal $34, $33C6) / STK-CONST ($33C8) / STK-ZEROS ($33F1)
+; ---------------------------------------------------------------------------
+L33C6:
+ ld h, d
+ ld l, e
+
+L33C8:
+ call L33A9
+ exx
+ push hl
+ exx
+ ex (sp), hl
+ push bc
+ ld a, (hl)
+ and $C0
+ rlca
+ rlca
+ ld c, a
+ inc c
+ ld a, (hl)
+ and $3F
+ jr nz, L33DE
+ inc hl
+ ld a, (hl)
+L33DE:
+ add a, $50
+ ld (de), a
+ ld a, 5
+ sub c
+ inc hl
+ inc de
+ ld b, 0
+ ldir
+ pop bc
+ ex (sp), hl
+ exx
+ pop hl
+ exx
+ ld b, a
+ xor a
+L33F1:
+ dec b
+ ret z
+ ld (de), a
+ inc de
+ jr L33F1
+
+; ---------------------------------------------------------------------------
+; SKIP-CONS ($33F7 / $33F8)
+; ---------------------------------------------------------------------------
+L33F7:
+ and a
+L33F8:
+ ret z
+ push af
+ push de
+ ld de, 0
+ call L33C8
+ pop de
+ pop af
+ dec a
+ jr L33F8
+
+; ---------------------------------------------------------------------------
+; LOC-MEM ($3406)
+; ---------------------------------------------------------------------------
+L3406:
+ ld c, a
+ rlca
+ rlca
+ add a, c
+ ld c, a
+ ld b, 0
+ add hl, bc
+ ret
+
+; ---------------------------------------------------------------------------
+; get-mem-xx (literales $E0-$FF, $340F)
+; ---------------------------------------------------------------------------
+L340F:
+ push de
+ ld hl, (FP_MEM)
+ call L3406
+ call L33C0
+ pop hl
+ ret
+
+; ---------------------------------------------------------------------------
+; stk-const-xx (literales $A0-$BF, $341B) + tabla de constantes
+; ---------------------------------------------------------------------------
+L341B:
+ ld h, d
+ ld l, e
+ exx
+ push hl
+ ld hl, L32C5
+ exx
+ call L33F7
+ call L33C8
+ exx
+ pop hl
+ exx
+ ret
+
+; ---------------------------------------------------------------------------
+; st-mem-xx (literales $C0-$DF, $342D)
+; ---------------------------------------------------------------------------
+L342D:
+ push hl
+ ex de, hl
+ ld hl, (FP_MEM)
+ call L3406
+ ex de, hl
+ call L33C0
+ ex de, hl
+ pop hl
+ ret
+
+; ---------------------------------------------------------------------------
+; exchange (literal $01, $343C)
+; ---------------------------------------------------------------------------
+L343C:
+ ld b, 5
+L343E:
+ ld a, (de)
+ ld c, (hl)
+ ex de, hl
+ ld (de), a
+ ld (hl), c
+ inc hl
+ inc de
+ djnz L343E
+ ex de, hl
+ ret
+
+; ---------------------------------------------------------------------------
+; series generator (literales $80-$9F, $3449) — necesario para SIN/COS/EXP/LN
+; (aun sin las tablas de coeficientes; se añadirán en una fase posterior)
+; ---------------------------------------------------------------------------
+L3449:
+ ld b, a
+ call L335E
+ defb $31 ;;duplicate x,x
+ defb $0F ;;addition x+x
+ defb $C0 ;;st-mem-0 x+x
+ defb $02 ;;delete .
+ defb $A0 ;;stk-zero 0
+ defb $C2 ;;st-mem-2 0
+L3453:
+ defb $31 ;;duplicate v,v.
+ defb $E0 ;;get-mem-0 v,v,x+2
+ defb $04 ;;multiply v,v*x+2
+ defb $E2 ;;get-mem-2 v,v*x+2,v
+ defb $C1 ;;st-mem-1
+ defb $03 ;;subtract
+ defb $38 ;;end-calc
+ call L33C6
+ call L3362
+ defb $0F ;;addition
+ defb $01 ;;exchange
+ defb $C2 ;;st-mem-2
+ defb $02 ;;delete
+ defb $35 ;;dec-jr-nz
+ defb (L3453 - $) & 0FFh ;;back to G-LOOP
+ defb $E1 ;;get-mem-1
+ defb $03 ;;subtract
+ defb $38 ;;end-calc
+ ret
+
+; ---------------------------------------------------------------------------
+; abs (literal $2A, $346A) / negate (literal $1B, $346E) / sgn (literal $29,
+; $3492)
+; ---------------------------------------------------------------------------
+L346A:
+ ld b, $FF
+ jr L3474
+
+L346E:
+ call L34E9
+ ret c
+ ld b, 0
+L3474:
+ ld a, (hl)
+ and a
+ jr z, L3483
+ inc hl
+ ld a, b
+ and $80
+ or (hl)
+ rla
+ ccf
+ rra
+ ld (hl), a
+ dec hl
+ ret
+L3483:
+ push de
+ push hl
+ call L2D7F
+ pop hl
+ ld a, b
+ or c
+ cpl
+ ld c, a
+ call L2D8E
+ pop de
+ ret
+
+L3492:
+ call L34E9
+ ret c
+ push de
+ ld de, 1
+ inc hl
+ rl (hl)
+ dec hl
+ sbc a, a
+ ld c, a
+ call L2D8E
+ pop de
+ ret
+
+; ---------------------------------------------------------------------------
+; INT-FETCH ($2D7F) / INT-STORE ($2D8E)
+; ---------------------------------------------------------------------------
+L2D7F:
+ inc hl
+ ld c, (hl)
+ inc hl
+ ld a, (hl)
+ xor c
+ sub c
+ ld e, a
+ inc hl
+ ld a, (hl)
+ adc a, c
+ xor c
+ ld d, a
+ ret
+
+L2D8E:
+ push hl
+ ld (hl), 0
+ inc hl
+ ld (hl), c
+ inc hl
+ ld a, e
+ xor c
+ sub c
+ ld (hl), a
+ inc hl
+ ld a, d
+ adc a, c
+ xor c
+ ld (hl), a
+ inc hl
+ ld (hl), 0
+ pop hl
+ ret
+
+; ---------------------------------------------------------------------------
+; PREP-ADD ($2F9B)
+; ---------------------------------------------------------------------------
+L2F9B:
+ ld a, (hl)
+ ld (hl), 0
+ and a
+ ret z
+ inc hl
+ bit 7, (hl)
+ set 7, (hl)
+ dec hl
+ ret z
+ push bc
+ ld bc, 5
+ add hl, bc
+ ld b, c
+ ld c, a
+ scf
+L2FAF:
+ dec hl
+ ld a, (hl)
+ cpl
+ adc a, 0
+ ld (hl), a
+ djnz L2FAF
+ ld a, c
+ pop bc
+ ret
+
+; ---------------------------------------------------------------------------
+; FETCH-TWO ($2FBA)
+; ---------------------------------------------------------------------------
+L2FBA:
+ push hl
+ push af
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ ld (hl), a
+ inc hl
+ ld a, c
+ ld c, (hl)
+ push bc
+ inc hl
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ ex de, hl
+ ld d, a
+ ld e, (hl)
+ push de
+ inc hl
+ ld d, (hl)
+ inc hl
+ ld e, (hl)
+ push de
+ exx
+ pop de
+ pop hl
+ pop bc
+ exx
+ inc hl
+ ld d, (hl)
+ inc hl
+ ld e, (hl)
+ pop af
+ pop hl
+ ret
+
+; ---------------------------------------------------------------------------
+; SHIFT-FP ($2FDD) / ADD-BACK ($3004)
+; ---------------------------------------------------------------------------
+L2FDD:
+ and a
+ ret z
+ cp $21
+ jr nc, L2FF9
+ push bc
+ ld b, a
+L2FE5:
+ exx
+ sra l
+ rr d
+ rr e
+ exx
+ rr d
+ rr e
+ djnz L2FE5
+ pop bc
+ ret nc
+ call L3004
+ ret nz
+L2FF9:
+ exx
+ xor a
+L2FFB:
+ ld l, 0
+ ld d, a
+ ld e, l
+ exx
+ ld de, 0
+ ret
+
+L3004:
+ inc e
+ ret nz
+ inc d
+ ret nz
+ exx
+ inc e
+ jr nz, L300D
+ inc d
+L300D:
+ exx
+ ret
+
+; ---------------------------------------------------------------------------
+; subtract (literal $03, $300F) / addition (literal $0F, $3014)
+; ---------------------------------------------------------------------------
+L300F:
+ ex de, hl
+ call L346E
+ ex de, hl
+
+L3014:
+ ld a, (de)
+ or (hl)
+ jr nz, L303E
+ push de
+ inc hl
+ push hl
+ inc hl
+ ld e, (hl)
+ inc hl
+ ld d, (hl)
+ inc hl
+ inc hl
+ inc hl
+ ld a, (hl)
+ inc hl
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ pop hl
+ ex de, hl
+ add hl, bc
+ ex de, hl
+ adc a, (hl)
+ rrca
+ adc a, 0
+ jr nz, L303C
+ sbc a, a
+ ld (hl), a
+ inc hl
+ ld (hl), e
+ inc hl
+ ld (hl), d
+ dec hl
+ dec hl
+ dec hl
+ pop de
+ ret
+L303C:
+ dec hl
+ pop de
+L303E:
+ call L3293
+ exx
+ push hl
+ exx
+ push de
+ push hl
+ call L2F9B
+ ld b, a
+ ex de, hl
+ call L2F9B
+ ld c, a
+ cp b
+ jr nc, L3055
+ ld a, b
+ ld b, c
+ ex de, hl
+L3055:
+ push af
+ sub b
+ call L2FBA
+ call L2FDD
+ pop af
+ pop hl
+ ld (hl), a
+ push hl
+ ld l, b
+ ld h, c
+ add hl, de
+ exx
+ ex de, hl
+ adc hl, bc
+ ex de, hl
+ ld a, h
+ adc a, l
+ ld l, a
+ rra
+ xor l
+ exx
+ ex de, hl
+ pop hl
+ rra
+ jr nc, L307C
+ ld a, 1
+ call L2FDD
+ inc (hl)
+ jr z, L309F
+L307C:
+ exx
+ ld a, l
+ and $80
+ exx
+ inc hl
+ ld (hl), a
+ dec hl
+ jr z, L30A5
+ ld a, e
+ neg
+ ccf
+ ld e, a
+ ld a, d
+ cpl
+ adc a, 0
+ ld d, a
+ exx
+ ld a, e
+ cpl
+ adc a, 0
+ ld e, a
+ ld a, d
+ cpl
+ adc a, 0
+ jr nc, L30A3
+ rra
+ exx
+ inc (hl)
+L309F:
+ jp z, L31AD
+ exx
+L30A3:
+ ld d, a
+ exx
+L30A5:
+ xor a
+ jp L3155
+
+; ---------------------------------------------------------------------------
+; HL-HL*DE ($30A9) / PREP-M/D ($30C0)
+; ---------------------------------------------------------------------------
+L30A9:
+ push bc
+ ld b, 16
+ ld a, h
+ ld c, l
+ ld hl, 0
+L30B1:
+ add hl, hl
+ jr c, L30BE
+ rl c
+ rla
+ jr nc, L30BC
+ add hl, de
+ jr c, L30BE
+L30BC:
+ djnz L30B1
+L30BE:
+ pop bc
+ ret
+
+L30C0:
+ call L34E9
+ ret c
+ inc hl
+ xor (hl)
+ set 7, (hl)
+ dec hl
+ ret
+
+; ---------------------------------------------------------------------------
+; multiply (literal $04, $30CA)
+; ---------------------------------------------------------------------------
+L30CA:
+ ld a, (de)
+ or (hl)
+ jr nz, L30F0
+ push de
+ push hl
+ push de
+ call L2D7F
+ ex de, hl
+ ex (sp), hl
+ ld b, c
+ call L2D7F
+ ld a, b
+ xor c
+ ld c, a
+ pop hl
+ call L30A9
+ ex de, hl
+ pop hl
+ jr c, L30EF
+ ld a, d
+ or e
+ jr nz, L30EA
+ ld c, a
+L30EA:
+ call L2D8E
+ pop de
+ ret
+L30EF:
+ pop de
+L30F0:
+ call L3293
+ xor a
+ call L30C0
+ ret c
+ exx
+ push hl
+ exx
+ push de
+ ex de, hl
+ call L30C0
+ ex de, hl
+ jr c, L315D
+ push hl
+ call L2FBA
+ ld a, b
+ and a
+ sbc hl, hl
+ exx
+ push hl
+ sbc hl, hl
+ exx
+ ld b, $21
+ jr L3125
+L3114:
+ jr nc, L311B
+ add hl, de
+ exx
+ adc hl, de
+ exx
+L311B:
+ exx
+ rr h
+ rr l
+ exx
+ rr h
+ rr l
+L3125:
+ exx
+ rr b
+ rr c
+ exx
+ rr c
+ rra
+ djnz L3114
+ ex de, hl
+ exx
+ ex de, hl
+ exx
+ pop bc
+ pop hl
+ ld a, b
+ add a, c
+ jr nz, L313B
+ and a
+L313B:
+ dec a
+ ccf
+L313D:
+ rla
+ ccf
+ rra
+ jp p, L3146
+ jr nc, L31AD
+ and a
+L3146:
+ inc a
+ jr nz, L3151
+ jr c, L3151
+ exx
+ bit 7, d
+ exx
+ jr nz, L31AD
+L3151:
+ ld (hl), a
+ exx
+ ld a, b
+ exx
+L3155:
+ jr nc, L316C
+ ld a, (hl)
+ and a
+L3159:
+ ld a, $80
+ jr z, L315E
+L315D:
+ xor a
+L315E:
+ exx
+ and d
+ call L2FFB
+ rlca
+ ld (hl), a
+ jr c, L3195
+ inc hl
+ ld (hl), a
+ dec hl
+ jr L3195
+L316C:
+ ld b, $20
+L316E:
+ exx
+ bit 7, d
+ exx
+ jr nz, L3186
+ rlca
+ rl e
+ rl d
+ exx
+ rl e
+ rl d
+ exx
+ dec (hl)
+ jr z, L3159
+ djnz L316E
+ jr L315D
+L3186:
+ rla
+ jr nc, L3195
+ call L3004
+ jr nz, L3195
+ exx
+ ld d, $80
+ exx
+ inc (hl)
+ jr z, L31AD
+L3195:
+ push hl
+ inc hl
+ exx
+ push de
+ exx
+ pop bc
+ ld a, b
+ rla
+ rl (hl)
+ rra
+ ld (hl), a
+ inc hl
+ ld (hl), c
+ inc hl
+ ld (hl), d
+ inc hl
+ ld (hl), e
+ pop hl
+ pop de
+ exx
+ pop hl
+ exx
+ ret
+
+L31AD:
+ ld a, ERROR_NumberTooBig
+ jp __ERROR
+
+; ---------------------------------------------------------------------------
+; division (literal $05, $31AF)
+; ---------------------------------------------------------------------------
+L31AF:
+ call L3293
+ ex de, hl
+ xor a
+ call L30C0
+ jr c, L31AD
+ ex de, hl
+ call L30C0
+ ret c
+ exx
+ push hl
+ exx
+ push de
+ push hl
+ call L2FBA
+ exx
+ push hl
+ ld h, b
+ ld l, c
+ exx
+ ld h, c
+ ld l, b
+ xor a
+ ld b, $DF
+ jr L31E2
+L31D2:
+ rla
+ rl c
+ exx
+ rl c
+ rl b
+ exx
+L31DB:
+ add hl, hl
+ exx
+ adc hl, hl
+ exx
+ jr c, L31F2
+L31E2:
+ sbc hl, de
+ exx
+ sbc hl, de
+ exx
+ jr nc, L31F9
+ add hl, de
+ exx
+ adc hl, de
+ exx
+ and a
+ jr L31FA
+L31F2:
+ and a
+ sbc hl, de
+ exx
+ sbc hl, de
+ exx
+L31F9:
+ scf
+L31FA:
+ inc b
+ jp m, L31D2
+ push af
+ jr z, L31E2
+ ld e, a
+ ld d, c
+ exx
+ ld e, c
+ ld d, b
+ pop af
+ rr b
+ pop af
+ rr b
+ exx
+ pop bc
+ pop hl
+ ld a, b
+ sub c
+ jp L313D
+
+; ---------------------------------------------------------------------------
+; Integer truncation towards zero (literal $3A, $3214)
+; ---------------------------------------------------------------------------
+L3214:
+ ld a, (hl)
+ and a
+ ret z
+ cp $81
+ jr nc, L3221
+ ld (hl), 0
+ ld a, $20
+ jr L3272
+L3221:
+ cp $91
+ jr nz, L323F
+ inc hl
+ inc hl
+ inc hl
+ ld a, $80
+ and (hl)
+ dec hl
+ or (hl)
+ dec hl
+ jr nz, L3233
+ ld a, $80
+ xor (hl)
+L3233:
+ dec hl
+ jr nz, L326C
+ ld (hl), a
+ inc hl
+ ld (hl), $FF
+ dec hl
+ ld a, $18
+ jr L3272
+L323F:
+ jr nc, L326D
+ push de
+ cpl
+ add a, $91
+ inc hl
+ ld d, (hl)
+ inc hl
+ ld e, (hl)
+ dec hl
+ dec hl
+ ld c, 0
+ bit 7, d
+ jr z, L3252
+ dec c
+L3252:
+ set 7, d
+ ld b, 8
+ sub b
+ add a, b
+ jr c, L325E
+ ld e, d
+ ld d, 0
+ sub b
+L325E:
+ jr z, L3267
+ ld b, a
+L3261:
+ srl d
+ rr e
+ djnz L3261
+L3267:
+ call L2D8E
+ pop de
+ ret
+L326C:
+ ld a, (hl)
+L326D:
+ sub $A0
+ ret p
+ neg
+L3272:
+ push de
+ ex de, hl
+ dec hl
+ ld b, a
+ srl b
+ srl b
+ srl b
+ jr z, L3283
+L327E:
+ ld (hl), 0
+ dec hl
+ djnz L327E
+L3283:
+ and $07
+ jr z, L3290
+ ld b, a
+ ld a, $FF
+L328A:
+ sla a
+ djnz L328A
+ and (hl)
+ ld (hl), a
+L3290:
+ ex de, hl
+ pop de
+ ret
+
+; ---------------------------------------------------------------------------
+; RE-ST-TWO ($3293) / RESTK-SUB ($3296) / re-stack (literal $3D, $3297)
+; ---------------------------------------------------------------------------
+L3293:
+ call L3296
+L3296:
+ ex de, hl
+L3297:
+ ld a, (hl)
+ and a
+ ret nz
+ push de
+ call L2D7F
+ xor a
+ inc hl
+ ld (hl), a
+ dec hl
+ ld (hl), a
+ ld b, $91
+ ld a, d
+ and a
+ jr nz, L32B1
+ or e
+ ld b, d
+ jr z, L32BD
+ ld d, e
+ ld e, b
+ ld b, $89
+L32B1:
+ ex de, hl
+L32B2:
+ dec b
+ add hl, hl
+ jr nc, L32B2
+ rrc c
+ rr h
+ rr l
+ ex de, hl
+L32BD:
+ dec hl
+ ld (hl), e
+ dec hl
+ ld (hl), d
+ dec hl
+ ld (hl), b
+ pop de
+ ret
+
+; ---------------------------------------------------------------------------
+; THE 'TABLE OF CONSTANTS' ($32C5-$32D6)
+; ---------------------------------------------------------------------------
+L32C5: ;;stk-zero
+ defb $00, $B0, $00
+L32C8: ;;stk-one
+ defb $40, $B0, $00, $01
+L32CC: ;;stk-half
+ defb $30, $00
+L32CE: ;;stk-pi/2
+ defb $F1, $49, $0F, $DA, $A2
+L32D3: ;;stk-ten
+ defb $40, $B0, $00, $0A
+
+; ---------------------------------------------------------------------------
+; THE 'TABLE OF ADDRESSES' ($32D7) — tbl-addrs
+;
+; Las entradas para funciones no soportadas (cadenas, USR, PEEK, IN, CODE,
+; LEN, READ-IN, VAL$) apuntan a CALC_UNSUPPORTED, que detiene la ejecucion
+; con un error claro si alguna vez se generase ese literal (no deberia
+; ocurrir: el compilador de ZX BASIC no emite esos literales para nuestro
+; runtime, ver la lista de literales usados en arith/cmp/bool/math/*.asm).
+; ---------------------------------------------------------------------------
+L32D7:
+ defw L368F ; $00 jump-true
+ defw L343C ; $01 exchange
+ defw L33A1 ; $02 delete
+ defw L300F ; $03 subtract
+ defw L30CA ; $04 multiply
+ defw L31AF ; $05 division
+ defw L3851 ; $06 to-power
+ defw L351B ; $07 or
+ defw L3524 ; $08 no-&-no
+ defw L353B ; $09 no-l-eql
+ defw L353B ; $0A no-gr-eql
+ defw L353B ; $0B nos-neql
+ defw L353B ; $0C no-grtr
+ defw L353B ; $0D no-less
+ defw L353B ; $0E nos-eql
+ defw L3014 ; $0F addition
+ defw CALC_UNSUPPORTED ; $10 str-&-no
+ defw CALC_UNSUPPORTED ; $11 str-l-eql
+ defw CALC_UNSUPPORTED ; $12 str-gr-eql
+ defw CALC_UNSUPPORTED ; $13 strs-neql
+ defw CALC_UNSUPPORTED ; $14 str-grtr
+ defw CALC_UNSUPPORTED ; $15 str-less
+ defw CALC_UNSUPPORTED ; $16 strs-eql
+ defw CALC_UNSUPPORTED ; $17 strs-add
+ defw CALC_UNSUPPORTED ; $18 val$
+ defw CALC_UNSUPPORTED ; $19 usr-$
+ defw CALC_UNSUPPORTED ; $1A read-in
+ defw L346E ; $1B negate
+ defw CALC_UNSUPPORTED ; $1C code
+ defw CALC_UNSUPPORTED ; $1D val (pendiente: parseo numerico propio)
+ defw CALC_UNSUPPORTED ; $1E len
+ defw L37B5 ; $1F sin
+ defw L37AA ; $20 cos
+ defw L37DA ; $21 tan
+ defw L3833 ; $22 asn
+ defw L3843 ; $23 acs
+ defw L37E2 ; $24 atn
+ defw L3713 ; $25 ln
+ defw L36C4 ; $26 exp
+ defw L36AF ; $27 int
+ defw L384A ; $28 sqr
+ defw L3492 ; $29 sgn
+ defw L346A ; $2A abs
+ defw CALC_UNSUPPORTED ; $2B peek
+ defw CALC_UNSUPPORTED ; $2C in
+ defw CALC_UNSUPPORTED ; $2D usr-no
+ defw CALC_UNSUPPORTED ; $2E str$ (pendiente)
+ defw CALC_UNSUPPORTED ; $2F chr$
+ defw L3501 ; $30 not
+ defw L33C0 ; $31 duplicate
+ defw L36A0 ; $32 n-mod-m
+ defw L3686 ; $33 jump
+ defw L33C6 ; $34 stk-data
+ defw L367A ; $35 dec-jr-nz
+ defw L3506 ; $36 less-0
+ defw L34F9 ; $37 greater-0
+ defw L369B ; $38 end-calc
+ defw L3783 ; $39 get-argt
+ defw L3214 ; $3A truncate
+ defw L33A2 ; $3B fp-calc-2
+ defw CALC_UNSUPPORTED ; $3C e-to-fp
+ defw L3297 ; $3D re-stack
+ defw L3449 ; series-xx $80-$9F
+ defw L341B ; stk-const-xx $A0-$BF
+ defw L342D ; st-mem-xx $C0-$DF
+ defw L340F ; get-mem-xx $E0-$FF
+
+CALC_UNSUPPORTED:
+ ld a, ERROR_InvalidArg
+ jp __ERROR
+
+; ---------------------------------------------------------------------------
+; THE 'CALCULATE' SUBROUTINE ($335B) — motor principal
+; ---------------------------------------------------------------------------
+L335B:
+ call L35BF
+L335E:
+ ld a, b
+ ld (FP_BREG), a
+L3362:
+ exx
+ ex (sp), hl
+ exx
+L3365:
+ ld (FP_STKEND), de
+ exx
+ ld a, (hl)
+ inc hl
+L336C:
+ push hl
+ and a
+ jp p, L3380
+ ld d, a
+ and $60
+ rrca
+ rrca
+ rrca
+ rrca
+ add a, $7C
+ ld l, a
+ ld a, d
+ and $1F
+ jr L338E
+L3380:
+ cp $18
+ jr nc, L338C
+ exx
+ ld bc, $FFFB
+ ld d, h
+ ld e, l
+ add hl, bc
+ exx
+L338C:
+ rlca
+ ld l, a
+L338E:
+ ld de, L32D7
+ ld h, 0
+ add hl, de
+ ld e, (hl)
+ inc hl
+ ld d, (hl)
+ ld hl, L3365
+ ex (sp), hl
+ push de
+ exx
+ ld bc, (FP_STKEND + 1) ; C=STKEND_hi, B=FP_BREG (ver nota en sysvars.asm)
+ ret
+
+; ---------------------------------------------------------------------------
+; delete (literal $02) — un simple RET; tambien destino de salto indirecto
+; ---------------------------------------------------------------------------
+L33A1:
+ ret
+
+; ---------------------------------------------------------------------------
+; fp-calc-2 (literal $3B) — reentrada de un solo literal
+; ---------------------------------------------------------------------------
+L33A2:
+ pop af
+ ld a, (FP_BREG)
+ exx
+ jr L336C
+
+; ---------------------------------------------------------------------------
+; STK-PNTRS ($35BF)
+; ---------------------------------------------------------------------------
+L35BF:
+ ld hl, (FP_STKEND)
+ ld de, $FFFB
+ push hl
+ add hl, de
+ pop de
+ ret
+
+; ---------------------------------------------------------------------------
+; jump-true (literal $00) / jump (literal $33) / dec-jr-nz (literal $35)
+; ---------------------------------------------------------------------------
+L368F:
+ inc de
+ inc de
+ ld a, (de)
+ dec de
+ dec de
+ and a
+ jr nz, L3686
+ exx
+ inc hl
+ exx
+ ret
+
+L367A:
+ exx
+ push hl
+ ld hl, FP_BREG
+ dec (hl)
+ pop hl
+ jr nz, L3687
+ inc hl
+ exx
+ ret
+
+L3686:
+ exx
+L3687:
+ ld e, (hl)
+ ld a, e
+ rla
+ sbc a, a
+ ld d, a
+ add hl, de
+ exx
+ ret
+
+; ---------------------------------------------------------------------------
+; end-calc (literal $38)
+; ---------------------------------------------------------------------------
+L369B:
+ pop af
+ exx
+ ex (sp), hl
+ exx
+ ret
+
+; ---------------------------------------------------------------------------
+; n-mod-m (literal $32) — implementado como programa del propio calculador
+; ---------------------------------------------------------------------------
+L36A0:
+ rst 28h
+ defb $C0, $02, $31, $E0, $05, $27, $E0, $01, $C0, $04, $03, $E0, $38
+ ret
+
+; ---------------------------------------------------------------------------
+; int (literal $27) — implementado como programa del propio calculador
+; ---------------------------------------------------------------------------
+L36AF:
+ rst 28h
+ defb $31 ;;duplicate
+ defb $36 ;;less-0
+ defb $00 ;;jump-true
+ defb (L36B7 - $) & 0FFh ;;a X-NEG
+ defb $3A ;;truncate
+ defb $38 ;;end-calc
+ ret
+L36B7:
+ defb $31 ;;duplicate
+ defb $3A ;;truncate
+ defb $C0 ;;st-mem-0
+ defb $03 ;;subtract
+ defb $E0 ;;get-mem-0
+ defb $01 ;;exchange
+ defb $30 ;;not
+ defb $00 ;;jump-true
+ defb (L36C2 - $) & 0FFh ;;a EXIT
+ defb $A1 ;;stk-one
+ defb $03 ;;subtract
+L36C2:
+ defb $38 ;;end-calc
+
+; ---------------------------------------------------------------------------
+; TEST-ZERO ($34E9) / GREATER-0 (literal $37) / NOT (literal $30) /
+; less-0 (literal $36) / SIGN-TO-C / FP-0/1
+; ---------------------------------------------------------------------------
+L34E9:
+ push hl
+ push bc
+ ld b, a
+ ld a, (hl)
+ inc hl
+ or (hl)
+ inc hl
+ or (hl)
+ inc hl
+ or (hl)
+ ld a, b
+ pop bc
+ pop hl
+ ret nz
+ scf
+ ret
+
+L34F9:
+ call L34E9
+ ret c
+ ld a, $FF
+ jr L3507
+
+L3501:
+ call L34E9
+ jr L350B
+
+L3506:
+ xor a
+L3507:
+ inc hl
+ xor (hl)
+ dec hl
+ rlca
+L350B:
+ push hl
+ ld a, 0
+ ld (hl), a
+ inc hl
+ ld (hl), a
+ inc hl
+ rla
+ ld (hl), a
+ rra
+ inc hl
+ ld (hl), a
+ inc hl
+ ld (hl), a
+ pop hl
+ ret
+
+; ---------------------------------------------------------------------------
+; or (literal $07) / no-&-no (literal $08)
+; ---------------------------------------------------------------------------
+L351B:
+ ex de, hl
+ call L34E9
+ ex de, hl
+ ret c
+ scf
+ jr L350B
+
+L3524:
+ ex de, hl
+ call L34E9
+ ex de, hl
+ ret nc
+ and a
+ jr L350B
+
+; ---------------------------------------------------------------------------
+; comparaciones numericas (literales $09-$0E, $353B) — solo la rama numerica;
+; no se soportan comparaciones de cadenas via calculador en este runtime.
+; ---------------------------------------------------------------------------
+L353B:
+ ld a, b
+ sub 8
+ bit 2, a
+ jr nz, L3543
+ dec a
+L3543:
+ rrca
+ jr nc, L354E
+ push af
+ push hl
+ call L343C
+ pop de
+ ex de, hl
+ pop af
+L354E:
+ rrca
+ push af
+ call L300F
+ jr L358C
+
+; ---------------------------------------------------------------------------
+; END-TESTS ($358C)
+; ---------------------------------------------------------------------------
+L358C:
+ pop af
+ push af
+ call c, L3501
+ pop af
+ push af
+ call nc, L34F9
+ pop af
+ rrca
+ call nc, L3501
+ ret
+
+; ===========================================================================
+; FASE 4: SIN/COS/TAN/ASN/ACS/ATN/LN/EXP/SQR
+;
+; Todas estas funciones son, igual que "int" o "n-mod-m" mas arriba,
+; programas del propio calculador (rst 28h + bytes de literal), identicos a
+; los de la ROM, apoyados en el "series generator" (L3449/L3453) ya portado.
+; Solo se han corregido los offsets de jump-true/jump (recalculados con la
+; misma formula ya usada en el resto del fichero: (destino - $) & 0FFh) y se
+; ha sustituido "RST 08h ; DEFB " (ERROR-1 de la ROM) por el
+; mecanismo de error propio (ERROR_xxx + jp __ERROR).
+; ===========================================================================
+
+; ---------------------------------------------------------------------------
+; STACK-A ($2D28) / STACK-BC ($2D2B) — apila A (o BC) como entero pequeño
+; ---------------------------------------------------------------------------
+L2D28:
+ ld c, a
+ ld b, 0
+L2D2B:
+ xor a
+ ld e, a
+ ld d, c
+ ld c, b
+ ld b, a
+ call __FPSTACK_PUSH
+ rst 28h
+ defb $38 ;;end-calc (recalcula HL/DE tras el push)
+ and a
+ ret
+
+; ---------------------------------------------------------------------------
+; FP-TO-BC ($2DA2) — recoge el ultimo valor de la pila FP en BC (redondeando)
+; ---------------------------------------------------------------------------
+L2DA2:
+ rst 28h
+ defb $38 ;;end-calc -> HL apunta al ultimo valor
+ ld a, (hl)
+ and a
+ jr z, L2DAD
+ rst 28h
+ defb $A2 ;;stk-half
+ defb $0F ;;addition
+ defb $27 ;;int
+ defb $38 ;;end-calc
+L2DAD:
+ rst 28h
+ defb $02 ;;delete
+ defb $38 ;;end-calc
+ push hl
+ push de
+ ex de, hl
+ ld b, (hl)
+ call L2D7F
+ xor a
+ sub b
+ bit 7, c
+ ld b, d
+ ld c, e
+ ld a, e
+ pop de
+ pop hl
+ ret
+
+; ---------------------------------------------------------------------------
+; FP-TO-A ($2DD5) — como FP-TO-BC pero devuelve A, con overflow en carry
+; ---------------------------------------------------------------------------
+L2DD5:
+ call L2DA2
+ ret c
+ push af
+ dec b
+ inc b
+ jr z, L2DE1
+ pop af
+ scf
+ ret
+L2DE1:
+ pop af
+ ret
+
+; ---------------------------------------------------------------------------
+; get-argt (literal $39, $3783) — reduce el argumento de sin/cos a -1..+1
+; ---------------------------------------------------------------------------
+L3783:
+ rst 28h
+ defb $3D ;;re-stack
+ defb $34 ;;stk-data
+ defb $EE ;;Exponent: $7E, Bytes: 4
+ defb $22, $F9, $83, $6E
+ defb $04 ;;multiply
+ defb $31 ;;duplicate
+ defb $A2 ;;stk-half
+ defb $0F ;;addition
+ defb $27 ;;int
+ defb $03 ;;subtract
+ defb $31 ;;duplicate
+ defb $0F ;;addition
+ defb $31 ;;duplicate
+ defb $0F ;;addition
+ defb $31 ;;duplicate
+ defb $2A ;;abs
+ defb $A1 ;;stk-one
+ defb $03 ;;subtract
+ defb $31 ;;duplicate
+ defb $37 ;;greater-0
+ defb $C0 ;;st-mem-0
+ defb $00 ;;jump-true
+ defb (L37A1 - $) & 0FFh ;;a ZPLUS
+ defb $02 ;;delete
+ defb $38 ;;end-calc
+ ret
+L37A1:
+ defb $A1 ;;stk-one
+ defb $03 ;;subtract
+ defb $01 ;;exchange
+ defb $36 ;;less-0
+ defb $00 ;;jump-true
+ defb (L37A8 - $) & 0FFh ;;a YNEG
+ defb $1B ;;negate
+L37A8:
+ defb $38 ;;end-calc
+ ret
+
+; ---------------------------------------------------------------------------
+; cos (literal $20, $37AA) — cae en sin/C-ENT (codigo compartido)
+; ---------------------------------------------------------------------------
+L37AA:
+ rst 28h
+ defb $39 ;;get-argt
+ defb $2A ;;abs
+ defb $A1 ;;stk-one
+ defb $03 ;;subtract
+ defb $E0 ;;get-mem-0
+ defb $00 ;;jump-true
+ defb (L37B7 - $) & 0FFh ;;a C-ENT
+ defb $1B ;;negate
+ defb $33 ;;jump
+ defb (L37B7 - $) & 0FFh ;;a C-ENT
+
+; ---------------------------------------------------------------------------
+; sin (literal $1F, $37B5) / C-ENT ($37B7, compartido con cos)
+; ---------------------------------------------------------------------------
+L37B5:
+ rst 28h
+ defb $39 ;;get-argt
+L37B7:
+ defb $31 ;;duplicate
+ defb $31 ;;duplicate
+ defb $04 ;;multiply
+ defb $31 ;;duplicate
+ defb $0F ;;addition
+ defb $A1 ;;stk-one
+ defb $03 ;;subtract
+ defb $86 ;;series-06
+ defb $14, $E6
+ defb $5C, $1F, $0B
+ defb $A3, $8F, $38, $EE
+ defb $E9, $15, $63, $BB, $23
+ defb $EE, $92, $0D, $CD, $ED
+ defb $F1, $23, $5D, $1B, $EA
+ defb $04 ;;multiply
+ defb $38 ;;end-calc
+ ret
+
+; ---------------------------------------------------------------------------
+; tan (literal $21, $37DA) — sin(x) / cos(x)
+; ---------------------------------------------------------------------------
+L37DA:
+ rst 28h
+ defb $31 ;;duplicate
+ defb $1F ;;sin
+ defb $01 ;;exchange
+ defb $20 ;;cos
+ defb $05 ;;division
+ defb $38 ;;end-calc
+ ret
+
+; ---------------------------------------------------------------------------
+; atn (literal $24, $37E2)
+; ---------------------------------------------------------------------------
+L37E2:
+ call L3297 ; re-stack
+ ld a, (hl)
+ cp $81
+ jr c, L37F8 ; SMALL
+ rst 28h
+ defb $A1 ;;stk-one
+ defb $1B ;;negate
+ defb $01 ;;exchange
+ defb $05 ;;division
+ defb $31 ;;duplicate
+ defb $36 ;;less-0
+ defb $A3 ;;stk-pi/2
+ defb $01 ;;exchange
+ defb $00 ;;jump-true
+ defb (L37FA - $) & 0FFh ;;a CASES
+ defb $1B ;;negate
+ defb $33 ;;jump
+ defb (L37FA - $) & 0FFh ;;a CASES
+L37F8:
+ rst 28h
+ defb $A0 ;;stk-zero
+L37FA:
+ defb $01 ;;exchange
+ defb $31 ;;duplicate
+ defb $31 ;;duplicate
+ defb $04 ;;multiply
+ defb $31 ;;duplicate
+ defb $0F ;;addition
+ defb $A1 ;;stk-one
+ defb $03 ;;subtract
+ defb $8C ;;series-0C
+ defb $10, $B2
+ defb $13, $0E
+ defb $55, $E4, $8D
+ defb $58, $39, $BC
+ defb $5B, $98, $FD
+ defb $9E, $00, $36, $75
+ defb $A0, $DB, $E8, $B4
+ defb $63, $42, $C4
+ defb $E6, $B5, $09, $36, $BE
+ defb $E9, $36, $73, $1B, $5D
+ defb $EC, $D8, $DE, $63, $BE
+ defb $F0, $61, $A1, $B3, $0C
+ defb $04 ;;multiply
+ defb $0F ;;addition
+ defb $38 ;;end-calc
+ ret
+
+; ---------------------------------------------------------------------------
+; asn (literal $22, $3833)
+; ---------------------------------------------------------------------------
+L3833:
+ rst 28h
+ defb $31 ;;duplicate
+ defb $31 ;;duplicate
+ defb $04 ;;multiply
+ defb $A1 ;;stk-one
+ defb $03 ;;subtract
+ defb $1B ;;negate
+ defb $28 ;;sqr
+ defb $A1 ;;stk-one
+ defb $0F ;;addition
+ defb $05 ;;division
+ defb $24 ;;atn
+ defb $31 ;;duplicate
+ defb $0F ;;addition
+ defb $38 ;;end-calc
+ ret
+
+; ---------------------------------------------------------------------------
+; acs (literal $23, $3843)
+; ---------------------------------------------------------------------------
+L3843:
+ rst 28h
+ defb $22 ;;asn
+ defb $A3 ;;stk-pi/2
+ defb $03 ;;subtract
+ defb $1B ;;negate
+ defb $38 ;;end-calc
+ ret
+
+; ---------------------------------------------------------------------------
+; ln (literal $25, $3713)
+; ---------------------------------------------------------------------------
+L3713:
+ rst 28h
+ defb $3D ;;re-stack
+ defb $31 ;;duplicate
+ defb $37 ;;greater-0
+ defb $00 ;;jump-true
+ defb (L371C - $) & 0FFh ;;a VALID
+ defb $38 ;;end-calc
+ ld a, ERROR_InvalidArg
+ jp __ERROR
+L371C:
+ defb $A0 ;;stk-zero
+ defb $02 ;;delete
+ defb $38 ;;end-calc
+ ld a, (hl)
+ ld (hl), $80
+ call L2D28
+ rst 28h
+ defb $34 ;;stk-data
+ defb $38 ;;Exponent: $88, Bytes: 1
+ defb $00
+ defb $03 ;;subtract
+ defb $01 ;;exchange
+ defb $31 ;;duplicate
+ defb $34 ;;stk-data
+ defb $F0 ;;Exponent: $80, Bytes: 4
+ defb $4C, $CC, $CC, $CD
+ defb $03 ;;subtract
+ defb $37 ;;greater-0
+ defb $00 ;;jump-true
+ defb (L373D - $) & 0FFh ;;a GRE.8
+ defb $01 ;;exchange
+ defb $A1 ;;stk-one
+ defb $03 ;;subtract
+ defb $01 ;;exchange
+ defb $38 ;;end-calc
+ inc (hl)
+ rst 28h
+L373D:
+ defb $01 ;;exchange
+ defb $34 ;;stk-data
+ defb $F0 ;;Exponent: $80, Bytes: 4
+ defb $31, $72, $17, $F8
+ defb $04 ;;multiply
+ defb $01 ;;exchange
+ defb $A2 ;;stk-half
+ defb $03 ;;subtract
+ defb $A2 ;;stk-half
+ defb $03 ;;subtract
+ defb $31 ;;duplicate
+ defb $34 ;;stk-data
+ defb $32 ;;Exponent: $82, Bytes: 1
+ defb $20
+ defb $04 ;;multiply
+ defb $A2 ;;stk-half
+ defb $03 ;;subtract
+ defb $8C ;;series-0C
+ defb $11, $AC
+ defb $14, $09
+ defb $56, $DA, $A5
+ defb $59, $30, $C5
+ defb $5C, $90, $AA
+ defb $9E, $70, $6F, $61
+ defb $A1, $CB, $DA, $96
+ defb $A4, $31, $9F, $B4
+ defb $E7, $A0, $FE, $5C, $FC
+ defb $EA, $1B, $43, $CA, $36
+ defb $ED, $A7, $9C, $7E, $5E
+ defb $F0, $6E, $23, $80, $93
+ defb $04 ;;multiply
+ defb $0F ;;addition
+ defb $38 ;;end-calc
+ ret
+
+; ---------------------------------------------------------------------------
+; exp (literal $26, $36C4)
+; ---------------------------------------------------------------------------
+L36C4:
+ rst 28h
+ defb $3D ;;re-stack
+ defb $34 ;;stk-data
+ defb $F1 ;;Exponent: $81, Bytes: 4
+ defb $38, $AA, $3B, $29
+ defb $04 ;;multiply
+ defb $31 ;;duplicate
+ defb $27 ;;int
+ defb $C3 ;;st-mem-3
+ defb $03 ;;subtract
+ defb $31 ;;duplicate
+ defb $0F ;;addition
+ defb $A1 ;;stk-one
+ defb $03 ;;subtract
+ defb $88 ;;series-08
+ defb $13, $36
+ defb $58, $65, $66
+ defb $9D, $78, $65, $40
+ defb $A2, $60, $32, $C9
+ defb $E7, $21, $F7, $AF, $24
+ defb $EB, $2F, $B0, $B0, $14
+ defb $EE, $7E, $BB, $94, $58
+ defb $F1, $3A, $7E, $F8, $CF
+ defb $E3 ;;get-mem-3
+ defb $38 ;;end-calc
+ call L2DD5
+ jr nz, L3705 ; N-NEGTV
+ jr c, L3703 ; REPORT-6b
+ add a, (hl)
+ jr nc, L370C ; RESULT-OK
+L3703:
+ ld a, ERROR_NumberTooBig
+ jp __ERROR
+L3705:
+ jr c, L370E ; RSLT-ZERO
+ sub (hl)
+ jr nc, L370E ; RSLT-ZERO
+ neg
+L370C:
+ ld (hl), a
+ ret
+L370E:
+ rst 28h
+ defb $02 ;;delete
+ defb $A0 ;;stk-zero
+ defb $38 ;;end-calc
+ ret
+
+; ---------------------------------------------------------------------------
+; sqr (literal $28, $384A) — cae en to-power (codigo compartido)
+; ---------------------------------------------------------------------------
+L384A:
+ rst 28h
+ defb $31 ;;duplicate
+ defb $30 ;;not
+ defb $00 ;;jump-true
+ defb (L386C - $) & 0FFh ;;a LAST
+ defb $A2 ;;stk-half
+ defb $38 ;;end-calc
+
+; ---------------------------------------------------------------------------
+; to-power (literal $06, $3851)
+; ---------------------------------------------------------------------------
+L3851:
+ rst 28h
+ defb $01 ;;exchange
+ defb $31 ;;duplicate
+ defb $30 ;;not
+ defb $00 ;;jump-true
+ defb (L385D - $) & 0FFh ;;a XIS0
+ defb $25 ;;ln
+ defb $04 ;;multiply
+ defb $38 ;;end-calc
+ jp L36C4
+L385D:
+ defb $02 ;;delete
+ defb $31 ;;duplicate
+ defb $30 ;;not
+ defb $00 ;;jump-true
+ defb (L386A - $) & 0FFh ;;a ONE
+ defb $A0 ;;stk-zero
+ defb $01 ;;exchange
+ defb $37 ;;greater-0
+ defb $00 ;;jump-true
+ defb (L386C - $) & 0FFh ;;a LAST
+ defb $A1 ;;stk-one
+ defb $01 ;;exchange
+ defb $05 ;;division
+L386A:
+ defb $02 ;;delete
+ defb $A1 ;;stk-one
+L386C:
+ defb $38 ;;end-calc
+ ret
+
+; ===========================================================================
+; FASE 5: STK-TO-A / STK-TO-BC / CD-PRMS1
+;
+; Rutinas auxiliares de la ROM usadas por DRAW3 (modo arco) y CIRCLE-DRAW.
+; No son literales del calculador (no se invocan via rst 28h + defb), sino
+; rutinas normales que a su vez usan el calculador ya portado (FP-TO-A,
+; STACK-A, y los literales sqr/sin/stk-data/etc de fp_calc.asm).
+; ===========================================================================
+
+; ---------------------------------------------------------------------------
+; STK-TO-A ($2314) — comprime el ultimo valor de la pila FP en A.
+; C = $01 si es positivo o cero, $FF si es negativo.
+; Error IntOutOfRange (sustituye a REPORT-Bc / RST 08h) si >= 256.
+; ---------------------------------------------------------------------------
+L2314:
+ call L2DD5 ; FP-TO-A: A = valor comprimido, Z si signo positivo
+ jr c, L24F9
+ ld c, $01
+ ret z
+ ld c, $FF
+ ret
+
+L24F9:
+ ld a, ERROR_IntOutOfRange
+ jp __ERROR
+
+; ---------------------------------------------------------------------------
+; STK-TO-BC ($2307) — recoge dos valores de la pila FP: el primero (mas
+; antiguo) en BC, el segundo (mas reciente) en DE (bajo, con signo en E/D).
+; ---------------------------------------------------------------------------
+L2307:
+ call L2314
+ ld b, a
+ push bc
+ call L2314
+ ld e, c
+ pop bc
+ ld d, c
+ ld c, a
+ ret
+
+; ---------------------------------------------------------------------------
+; CD-PRMS1 ($247D) — CIRCLE/DRAW PARAMETERS: a partir del "diametro" z (tope
+; de pila) y el angulo total en mem-5, calcula el numero de lineas rectas
+; (B, multiplo de 4, max 252) y deja en mem-1/mem-3/mem-4 sin(a/2), cos(a) y
+; sin(a) del angulo de paso "a" = ANGULO/lineas.
+; ---------------------------------------------------------------------------
+L247D:
+ rst 28h
+ defb $31 ;;duplicate z, z.
+ defb $28 ;;sqr z, sqr(z).
+ defb $34 ;;stk-data z, sqr(z), 2.
+ defb $32 ;;Exponent: $82, Bytes: 1
+ defb $00 ;;(+00,+00,+00)
+ defb $01 ;;exchange z, 2, sqr(z).
+ defb $05 ;;division z, 2/sqr(z).
+ defb $E5 ;;get-mem-5 z, 2/sqr(z), ANGLE.
+ defb $01 ;;exchange z, ANGLE, 2/sqr(z)
+ defb $05 ;;division z, ANGLE*sqr(z)/2 (=num. lineas)
+ defb $2A ;;abs (solo para arco)
+ defb $38 ;;end-calc z, numero de lineas.
+
+ call L2DD5 ; FP-TO-A
+ jr c, L247D_USE252
+
+ and $FC ; multiplo de 4 (p.ej. 29 -> 28)
+ add a, $04 ; podria dar overflow -> 256
+ jr nc, L247D_SAVE
+
+L247D_USE252:
+ ld a, $FC ; limite de 252 (para arco)
+
+L247D_SAVE:
+ push af ; conserva el contador de lineas
+ call L2D28 ; apila el contador modificado
+
+ rst 28h
+ defb $E5 ;;get-mem-5 z, A, ANGLE.
+ defb $01 ;;exchange z, ANGLE, A.
+ defb $05 ;;division z, ANGLE/A. (angulo de paso = a)
+ defb $31 ;;duplicate z, a, a.
+ defb $1F ;;sin z, a, sin(a)
+ defb $C4 ;;st-mem-4 z, a, sin(a)
+ defb $02 ;;delete z, a.
+ defb $31 ;;duplicate z, a, a.
+ defb $A2 ;;stk-half z, a, a, 1/2.
+ defb $04 ;;multiply z, a, a/2.
+ defb $1F ;;sin z, a, sin(a/2).
+ defb $C1 ;;st-mem-1 z, a, sin(a/2).
+ defb $01 ;;exchange z, sin(a/2), a.
+ defb $C0 ;;st-mem-0 z, sin(a/2), a. (solo para arco)
+ defb $02 ;;delete z, sin(a/2).
+ defb $31 ;;duplicate z, sin(a/2), sin(a/2).
+ defb $04 ;;multiply z, sin(a/2)^2.
+ defb $31 ;;duplicate z, sin(a/2)^2, sin(a/2)^2.
+ defb $0F ;;addition z, 2*sin(a/2)^2.
+ defb $A1 ;;stk-one z, 2*sin(a/2)^2, 1.
+ defb $03 ;;subtract z, 2*sin(a/2)^2-1.
+ defb $1B ;;negate z, 1-2*sin(a/2)^2 = cos(a).
+ defb $C3 ;;st-mem-3 z, cos(a).
+ defb $02 ;;delete z.
+ defb $38 ;;end-calc z.
+
+ pop bc ; restaura el contador de lineas
+ ret
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/fp_tostr.asm b/src/lib/arch/zx81sd/runtime/fp_tostr.asm
new file mode 100644
index 000000000..50d379bc5
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/fp_tostr.asm
@@ -0,0 +1,298 @@
+; fp_tostr.asm (zx81sd) — Conversión de un FLOAT a texto ASCII decimal
+;
+; Usado por printf.asm (PRINT de un FLOAT) y str.asm (STR$) en zx81sd.
+;
+; La ROM del Spectrum resuelve esto con PRINT-FP, una rutina enorme que
+; además soporta notación científica (E-format) y se apoya en mecanismos
+; ajenos a zx81sd (canales CHAN-OPEN, área de "workspace" del editor BASIC
+; que crece con BC-SPACES). Aquí, por acuerdo explícito, se implementa una
+; versión simplificada: signo + parte entera + hasta 5 decimales, sin
+; notación científica, recortando ceros finales (y el punto si todos los
+; decimales resultan cero). Cubre el uso habitual de PRINT/STR$ con FLOAT.
+;
+; Se apoya en el calculador ya portado (fp_calc.asm: duplicate/int/subtract/
+; negate) y en rutinas Z80 puras ya existentes y verificadas en zx81sd:
+; __FTOU32REG/__FTOU8 (conversión FLOAT -> entero, sin ROM) y __DIVU32.
+
+#include once
+#include once
+#include once
+#include once
+
+ push namespace core
+
+; --- Variables de trabajo (no reentrante, uso transitorio) ------------------
+FP_STR_ORIG: defb 0, 0, 0, 0, 0 ; A E D C B del valor (se vuelve positivo)
+FP_STR_INT: defb 0, 0, 0, 0, 0 ; parte entera
+FP_STR_FRAC: defb 0, 0, 0, 0, 0 ; parte fraccionaria (se actualiza cada iteración)
+FP_STR_COUNT: defb 0 ; contador de decimales restantes
+FP_STR_WR: defw 0 ; cursor de escritura en FP_STR_BUF
+FP_STR_BUF: defs 24 ; buffer de salida (signo + entero + '.' + decimales)
+
+; ---------------------------------------------------------------------------
+; STORE5 — Guarda A,E,D,C,B en (HL),(HL+1)..(HL+4)
+; LOAD5 — Carga A,E,D,C,B desde (HL),(HL+1)..(HL+4)
+; (Z80 no tiene LD E,(nn)/LD D,(nn)/etc, solo LD A,(nn) y LD rr,(nn) con pares;
+; por eso se accede siempre indirectamente vía HL.)
+; ---------------------------------------------------------------------------
+STORE5:
+ ld (hl), a
+ inc hl
+ ld (hl), e
+ inc hl
+ ld (hl), d
+ inc hl
+ ld (hl), c
+ inc hl
+ ld (hl), b
+ ret
+
+LOAD5:
+ ld a, (hl)
+ inc hl
+ ld e, (hl)
+ inc hl
+ ld d, (hl)
+ inc hl
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ ret
+
+; ---------------------------------------------------------------------------
+; EMIT_CHAR — Escribe A en el buffer de salida y avanza el cursor
+; ---------------------------------------------------------------------------
+EMIT_CHAR:
+ push hl
+ ld hl, (FP_STR_WR)
+ ld (hl), a
+ inc hl
+ ld (FP_STR_WR), hl
+ pop hl
+ ret
+
+; ---------------------------------------------------------------------------
+; EMIT_U32 — Escribe DEHL (entero sin signo de 32 bits) como digitos decimales
+; en el buffer de salida (sin ceros a la izquierda; "0" si el valor es cero).
+; Mismo algoritmo que __PRINTU32 (printi32.asm/printnum.asm), pero escribiendo
+; en el buffer en vez de en pantalla.
+; ---------------------------------------------------------------------------
+EMIT_U32:
+ PROC
+ LOCAL EMIT_U32_LOOP
+ LOCAL EMIT_U32_START
+ LOCAL EMIT_U32_CONT
+
+ ld b, 0
+
+EMIT_U32_LOOP:
+ ld a, h
+ or l
+ or d
+ or e
+ jp z, EMIT_U32_START
+
+ push bc
+ ld bc, 0
+ push bc
+ ld bc, 10
+ push bc
+ call __DIVU32
+ pop bc
+
+ exx
+ ld a, l
+ or '0'
+ push af
+ exx
+ inc b
+ jp EMIT_U32_LOOP
+
+EMIT_U32_START:
+ ld a, b
+ or a
+ jp nz, EMIT_U32_CONT
+ ld a, '0'
+ call EMIT_CHAR
+ ret
+
+EMIT_U32_CONT:
+ pop af
+ push bc
+ call EMIT_CHAR
+ pop bc
+ djnz EMIT_U32_CONT
+ ret
+
+ ENDP
+
+; ---------------------------------------------------------------------------
+; FP_TO_STR — Convierte un FLOAT a texto ASCII decimal
+; Entrada: A,E,D,C,B = valor FLOAT (convención habitual del runtime)
+; Salida: HL = puntero al texto (sin prefijo de longitud), BC = longitud
+; ---------------------------------------------------------------------------
+FP_TO_STR:
+ PROC
+ LOCAL FP_TO_STR_POS
+ LOCAL FP_TO_STR_POS2
+ LOCAL FP_TO_STR_FRACLOOP
+ LOCAL FP_TO_STR_FRACDONE
+ LOCAL FP_TO_STR_TRIMLOOP
+ LOCAL FP_TO_STR_TRIMDOT
+ LOCAL FP_TO_STR_TRIMKEEP
+ LOCAL FP_TO_STR_DONE
+
+ ld hl, FP_STR_ORIG
+ call STORE5
+
+ ld hl, FP_STR_BUF
+ ld (FP_STR_WR), hl
+
+ ; ¿Es cero? (forma canonica: A=0 y mantisa completa a 0)
+ or e
+ or d
+ or c
+ or b
+ jr nz, FP_TO_STR_POS
+ ld a, '0'
+ call EMIT_CHAR
+ jp FP_TO_STR_DONE
+
+FP_TO_STR_POS:
+ ; El bit 7 de E es el signo tanto en formato entero-pequeno como en
+ ; coma flotante completa (ver fp_calc.asm / __FTOU32REG).
+ ld a, (FP_STR_ORIG + 1)
+ bit 7, a
+ jr z, FP_TO_STR_POS2
+
+ ld a, '-'
+ call EMIT_CHAR
+
+ ; Vuelve positivo el valor original (negate) para trabajar siempre en abs
+ ld hl, FP_STR_ORIG
+ call LOAD5
+ call __FPSTACK_PUSH
+ rst 28h
+ defb $1B ;;negate
+ defb $38 ;;end-calc
+ call __FPSTACK_POP
+ ld hl, FP_STR_ORIG
+ call STORE5
+
+FP_TO_STR_POS2:
+ ; intx = INT(x) (x ya es >= 0 en este punto)
+ ld hl, FP_STR_ORIG
+ call LOAD5
+ call __FPSTACK_PUSH
+ rst 28h
+ defb $31 ;;duplicate
+ defb $27 ;;int
+ defb $38 ;;end-calc
+ call __FPSTACK_POP
+ ld hl, FP_STR_INT
+ call STORE5
+
+ ; frac = x - intx
+ ld hl, FP_STR_INT
+ call LOAD5
+ call __FPSTACK_PUSH
+ rst 28h
+ defb $03 ;;subtract
+ defb $38 ;;end-calc
+ call __FPSTACK_POP
+ ld hl, FP_STR_FRAC
+ call STORE5
+
+ ; imprime la parte entera (ya es >= 0, cabe en 32 bits sin signo)
+ ld hl, FP_STR_INT
+ call LOAD5
+ call __FTOU32REG ; DEHL = parte entera
+ call EMIT_U32
+
+ ld a, '.'
+ call EMIT_CHAR
+ ld a, 5
+ ld (FP_STR_COUNT), a
+
+FP_TO_STR_FRACLOOP:
+ ld hl, FP_STR_FRAC
+ call LOAD5
+ call __FPSTACK_PUSH ; pila: [frac]
+
+ xor a
+ ld d, 10
+ ld e, a
+ ld c, a
+ ld b, a
+ call __FPSTACK_PUSH ; pila: [frac, 10]
+ rst 28h
+ defb $04 ;;multiply
+ defb $38 ;;end-calc
+ ; pila: [frac*10]
+ rst 28h
+ defb $31 ;;duplicate
+ defb $27 ;;int
+ defb $38 ;;end-calc
+ ; pila: [frac*10, digito]
+ call __FPSTACK_POP
+ call __FTOU8 ; A = digito (0-9)
+ push af ; guarda el digito (igual que val.asm con rst 28h)
+
+ pop af
+ push af
+ ld d, a
+ xor a
+ ld e, a
+ ld c, a
+ ld b, a
+ call __FPSTACK_PUSH ; pila: [frac*10, digito]
+ rst 28h
+ defb $03 ;;subtract
+ defb $38 ;;end-calc
+ ; pila: [frac*10 - digito] = nuevo frac
+ call __FPSTACK_POP
+ ld hl, FP_STR_FRAC
+ call STORE5
+
+ pop af ; recupera el digito
+ or '0'
+ call EMIT_CHAR
+
+ ld hl, FP_STR_COUNT
+ dec (hl)
+ jr nz, FP_TO_STR_FRACLOOP
+
+FP_TO_STR_FRACDONE:
+ ; recorta ceros finales (y el punto si todos los decimales eran cero)
+ ld hl, (FP_STR_WR)
+
+FP_TO_STR_TRIMLOOP:
+ dec hl
+ ld a, (hl)
+ cp '.'
+ jr z, FP_TO_STR_TRIMDOT
+ cp '0'
+ jr nz, FP_TO_STR_TRIMKEEP
+ jr FP_TO_STR_TRIMLOOP
+
+FP_TO_STR_TRIMDOT:
+ ld (FP_STR_WR), hl
+ jp FP_TO_STR_DONE
+
+FP_TO_STR_TRIMKEEP:
+ inc hl
+ ld (FP_STR_WR), hl
+
+FP_TO_STR_DONE:
+ ld hl, (FP_STR_WR)
+ ld de, FP_STR_BUF
+ or a
+ sbc hl, de ; HL = longitud del texto
+ ld b, h
+ ld c, l
+ ld hl, FP_STR_BUF
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/io/keyboard/inkey.asm b/src/lib/arch/zx81sd/runtime/io/keyboard/inkey.asm
new file mode 100644
index 000000000..992f0c7c7
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/io/keyboard/inkey.asm
@@ -0,0 +1,53 @@
+; INKEY Function (zx81sd)
+; Returns a string allocated in dynamic memory
+; containing the string.
+; An empty string otherwise.
+;
+; La version de zx48k llama a rutinas de la ROM del Spectrum
+; (KEY_SCAN/KEY_TEST/KEY_CODE) que no existen en tiempo de ejecucion en
+; zx81sd (no hay ROM mapeada). Se sustituye por un escaneo directo de la
+; matriz de teclado del ZX81 y una decodificacion propia a ASCII.
+
+#include once
+#include once
+
+ push namespace core
+
+INKEY:
+ PROC
+ LOCAL __EMPTY_INKEY
+
+ ld bc, 3 ; 1 char length string
+ call __MEM_ALLOC
+
+ ld a, h
+ or l
+ ret z ; Return if NULL (No memory)
+
+ push hl ; Saves memory pointer
+
+ call __ZX81SD_KEYSCAN
+ pop hl
+ or a
+ jr z, __EMPTY_INKEY
+
+ ld (hl), 1
+ inc hl
+ ld (hl), 0
+ inc hl
+ ld (hl), a
+ dec hl
+ dec hl ; HL Points to string result
+ ret
+
+__EMPTY_INKEY:
+ xor a
+ ld (hl), a
+ inc hl
+ ld (hl), a
+ dec hl
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/io/keyboard/keyscan.asm b/src/lib/arch/zx81sd/runtime/io/keyboard/keyscan.asm
new file mode 100644
index 000000000..8bf0a78cd
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/io/keyboard/keyscan.asm
@@ -0,0 +1,397 @@
+; ---------------------------------------------------------------------------
+; keyscan.asm — Escaneo directo de la matriz de teclado del ZX81
+;
+; No hay ROM mapeada en tiempo de ejecucion (el bloque 0 lo ocupa por
+; completo nuestro binario compilado), asi que no se puede llamar a la
+; rutina KEYBOARD/DECODE de la ROM original. El hardware del teclado, en
+; cambio, es el mismo ZX81 de siempre (el SD81 Booster no lo toca), asi
+; que se reimplementa aqui el escaneo fisico (puerto $FEFE, 8 filas
+; seleccionadas rotando el registro alto del puerto) y una decodificacion
+; propia a ASCII.
+;
+; ESQUEMA DE TECLAS (2026-07-04, revisado tras probar en el emulador)
+; --------------------------------------------------------------------
+; El teclado fisico del ZX81 no tiene mayuscula/minuscula por tecla (SHIFT
+; da simbolos, no la version en mayuscula de la letra) — a diferencia del
+; Spectrum, donde SHIFT+letra si da la mayuscula de esa letra via la ROM
+; (K-DECODE en modo L). Para poder imprimir el juego de caracteres
+; completo sin depender de teclado externo:
+;
+; Sin modificador -> minuscula (a, s, d... como hasta ahora)
+; SHIFT + letra -> MAYUSCULA de esa letra
+; SHIFT + "2" -> conmuta CAPS LOCK (persistente; no imprime nada)
+; CAPS LOCK activo -> minuscula pasa a mayuscula (SHIFT sigue dando
+; mayuscula igual, no hay interaccion: es un OR)
+; "." (con o sin SHIFT) -> "." o "," exactamente igual que en el ZX81
+; real (SHIFT+"." = ",")
+;
+; SHIFT es comodo de mantener pulsado con una mano mientras se pulsa la
+; letra con la otra (aqui __ZX81SD_KEYSCAN SI detecta la simultaneidad
+; real, con dos lecturas de puerto dedicadas para SHIFT y una tercera
+; tecla). Pero un primer diseno que anadia un segundo modificador de
+; simbolo con "." (pulsar "."+tecla a la vez para sacar el simbolo
+; impreso en el teclado del ZX81) resulto impracticable de teclear: al
+; usarse desde INPUT, la rutina de lectura ya compromete la tecla "."
+; en cuanto se detecta pulsada sola, sin dar tiempo a que la segunda
+; tecla llegue a la vez (no son dedos que se muevan en paralelo como
+; SHIFT+letra, sino una pulsacion despues de otra). Por eso ese segundo
+; modificador se ha movido a stdlib/input.bas, como una "tecla muerta"
+; a nivel de composicion de caracteres (se pulsa "." y LUEGO la tecla
+; del simbolo, secuencialmente, sin necesidad de mantenerlas a la vez):
+; ver __ZX81SD_SYMBOL_FOR mas abajo, que da el mapeo tecla->simbolo que
+; usa esa logica.
+;
+; IMPORTANTE: la ROM del ZX81 decodifica las teclas a SU PROPIO charset
+; (con codigos de token para palabras clave de BASIC), no a ASCII. El
+; runtime de zx81sd usa el charset Spectrum/ASCII para imprimir (ver
+; charset.asm, specfont.bin), asi que las tablas de decodificacion de
+; abajo traducen directamente cada tecla a su codigo ASCII, ignorando las
+; teclas que en el ZX81 producen tokens de BASIC sin equivalente ASCII
+; simple (STOP, AND, OR, THEN, TO, STEP, <=, >=, <>, **, EDIT, GRAPHICS,
+; FUNCTION, cursores, LPRINT, LLIST, SLOW, FAST). La tecla RUBOUT
+; (SHIFT+0) se traduce a ASCII 12, y ENTER (NEWLINE) a ASCII 13, para
+; mantener compatibilidad con la convencion ya usada por stdlib/input.bas.
+; ---------------------------------------------------------------------------
+
+ push namespace core
+
+; --- Tablas de decodificacion, en ambito de fichero (no LOCAL a ningun
+; PROC): las usa __ZX81SD_KEYSCAN y tambien __ZX81SD_SYMBOL_FOR. ---
+;
+; Orden de filas/columnas identico al de las tablas K-UNSHIFT/K-SHIFT de
+; la ROM original del ZX81 (fila 0: SHIFT,Z,X,C,V ... fila 7: ENTER,L,K,
+; J,H / SPACE,.,M,N,B), verificado contra el disassembly.
+__ZX81SD_UNSHIFT_TABLE:
+ DEFB 'z', 'x', 'c', 'v'
+ DEFB 'a', 's', 'd', 'f', 'g'
+ DEFB 'q', 'w', 'e', 'r', 't'
+ DEFB '1', '2', '3', '4', '5'
+ DEFB '0', '9', '8', '7', '6'
+ DEFB 'p', 'o', 'i', 'u', 'y'
+ DEFB 13, 'l', 'k', 'j', 'h' ; NEWLINE -> ENTER (ASCII 13)
+ DEFB ' ', '.', 'm', 'n', 'b'
+
+; __ZX81SD_SYMBOL_TABLE — simbolos impresos en el teclado del ZX81 bajo
+; SHIFT. Alineada posicion a posicion con __ZX81SD_UNSHIFT_TABLE (mismo
+; indice = misma tecla fisica); la usa __ZX81SD_SYMBOL_FOR para el modo
+; de composicion "." + tecla de stdlib/input.bas.
+__ZX81SD_SYMBOL_TABLE:
+ DEFB ':', ';', '?', '/'
+ DEFB 0, 0, 0, 0, 0 ; STOP, LPRINT, SLOW, FAST, LLIST
+ DEFB '"', 0, 0, 0, 0 ; "" (par de comillas), OR, STEP, <=, <>
+ DEFB 0, 0, 0, 0, 0 ; EDIT, [CAPS LOCK], THEN, TO, cursor-izq
+ DEFB 12, 0, 0, 0, 0 ; RUBOUT (DEL=12), GRAPHICS, cursor der/arr/abj
+ DEFB '"', ')', '(', '$', 0 ; ", ), (, $, >=
+ DEFB 0, '=', '+', '-', 0 ; FUNCTION, =, +, -, **
+ DEFB 0, ',', '>', '<', '*' ; £, ',', >, <, *
+
+; __ZX81SD_CAPS_TABLE — igual que __ZX81SD_SYMBOL_TABLE, pero con la
+; MAYUSCULA en cada posicion que en UNSHIFT_TABLE es una letra (para
+; SHIFT+letra, y para el modo CAPS LOCK persistente). Las posiciones que
+; no son letra se dejan igual que en SYMBOL_TABLE (digitos, ENTER,
+; SPACE, RUBOUT...).
+__ZX81SD_CAPS_TABLE:
+ DEFB 'Z', 'X', 'C', 'V'
+ DEFB 'A', 'S', 'D', 'F', 'G'
+ DEFB 'Q', 'W', 'E', 'R', 'T'
+ DEFB 0, 0, 0, 0, 0 ; digitos 1-5 (el "2" se intercepta antes)
+ DEFB 12, 0, 0, 0, 0 ; digitos 0,9,8,7,6 (RUBOUT en el "0")
+ DEFB 'P', 'O', 'I', 'U', 'Y'
+ DEFB 0, 'L', 'K', 'J', 'H' ; ENTER sin cambio, L/K/J/H en mayuscula
+ DEFB 0, ',', 'M', 'N', 'B' ; SPACE sin cambio, "." no se alcanza aqui
+
+; --- Estado persistente del teclado (sobrevive entre llamadas) ---
+__ZX81SD_KBD_CAPSLOCK: DEFB 0 ; 1 = CAPS LOCK activo
+__ZX81SD_KBD_CAPS_EDGE: DEFB 0 ; deteccion de flanco para el combo SHIFT+"2"
+
+; --- Estado transitorio (se recalcula en cada llamada a __ZX81SD_KEYSCAN) ---
+__ZX81SD_KBD_SHIFT: DEFB 0
+__ZX81SD_KBD_OTHER_VALID: DEFB 0
+__ZX81SD_KBD_OTHER_IDX: DEFB 0
+
+; ---------------------------------------------------------------------------
+; __ZX81SD_KEYSCAN
+;
+; Escanea el teclado y decodifica la tecla pulsada segun el esquema de
+; arriba (SHIFT como modificador de mayuscula, CAPS LOCK persistente).
+;
+; Devuelve:
+; A = codigo ASCII de la tecla pulsada, o 0 si no hay ninguna pulsada,
+; si la combinacion no tiene equivalente ASCII, si solo esta
+; pulsado SHIFT solo, o si se acaba de conmutar el CAPS LOCK
+; (SHIFT+"2").
+; Flag Z activo si A = 0
+;
+; Registros modificados: AF, BC, DE, HL
+; ---------------------------------------------------------------------------
+__ZX81SD_KEYSCAN:
+ PROC
+ LOCAL FIND_OTHER
+ LOCAL ROW_LOOP
+ LOCAL NEXT_ROW
+ LOCAL FIND_COL
+ LOCAL GOT_COL0
+ LOCAL GOT_COL1
+ LOCAL GOT_COL2
+ LOCAL GOT_COL3
+ LOCAL GOT_COL4
+ LOCAL GOT_KEY
+ LOCAL OTHER_FOUND
+ LOCAL OTHER_NONE
+ LOCAL NOT_ROW0
+
+ LOCAL DECIDE
+ LOCAL HAVE_SHIFT
+ LOCAL SHIFT_ALONE
+ LOCAL CHECK_CAPS_KEY
+ LOCAL DO_CAPS_TOGGLE
+ LOCAL CAPS_EDGE_DONE
+ LOCAL NO_SHIFT
+ LOCAL NO_KEY
+ LOCAL USE_UNSHIFT
+ LOCAL USE_CAPS_FOR_PLAIN
+ LOCAL LOOKUP_OTHER_IN_HL
+ LOCAL RESET_CAPS_EDGE
+
+ LOCAL SHIFT_BIT_SET
+
+ ; --- 1. Leer SHIFT (fila 0, columna 0) con una lectura dedicada ---
+ ld bc, $FEFE
+ in a, (c)
+ and $01
+ ld a, 1
+ jr z, SHIFT_BIT_SET
+ xor a
+SHIFT_BIT_SET:
+ ld (__ZX81SD_KBD_SHIFT), a
+
+ ; --- 2. Buscar otra tecla pulsada (excluyendo SHIFT) ---
+ xor a
+ ld (__ZX81SD_KBD_OTHER_VALID), a
+ call FIND_OTHER ; carry activo = encontrada (indice en A)
+ jr nc, DECIDE
+ ld (__ZX81SD_KBD_OTHER_IDX), a
+ ld a, 1
+ ld (__ZX81SD_KBD_OTHER_VALID), a
+
+DECIDE:
+ ld a, (__ZX81SD_KBD_SHIFT)
+ or a
+ jr nz, HAVE_SHIFT
+ jr NO_SHIFT
+
+HAVE_SHIFT:
+ ld a, (__ZX81SD_KBD_OTHER_VALID)
+ or a
+ jr nz, CHECK_CAPS_KEY
+
+SHIFT_ALONE:
+ ; Solo SHIFT: sin caracter, y se reinicia la deteccion de flanco del
+ ; combo SHIFT+"2" (no puede haber toggle sin la tecla "2" presente).
+ call RESET_CAPS_EDGE
+ xor a
+ ret
+
+CHECK_CAPS_KEY:
+ ld a, (__ZX81SD_KBD_OTHER_IDX)
+ cp 15 ; indice de la tecla "2" (fila 3, columna 1)
+ jr z, DO_CAPS_TOGGLE
+
+ ; SHIFT + letra (o cualquier otra tecla, salvo "2"): __ZX81SD_CAPS_TABLE
+ ; (mayuscula en las posiciones de letra; en el resto, igual que
+ ; __ZX81SD_SYMBOL_TABLE — p.ej. SHIFT+"." sigue dando ",").
+ call RESET_CAPS_EDGE
+ ld hl, __ZX81SD_CAPS_TABLE
+ jr LOOKUP_OTHER_IN_HL
+
+DO_CAPS_TOGGLE:
+ ; Conmuta CAPS LOCK solo en el flanco de subida del combo (para no
+ ; conmutarlo en cada llamada mientras se mantiene pulsado).
+ ld a, (__ZX81SD_KBD_CAPS_EDGE)
+ or a
+ jr nz, CAPS_EDGE_DONE
+ ld a, 1
+ ld (__ZX81SD_KBD_CAPS_EDGE), a
+ ld a, (__ZX81SD_KBD_CAPSLOCK)
+ xor 1
+ ld (__ZX81SD_KBD_CAPSLOCK), a
+CAPS_EDGE_DONE:
+ xor a ; SHIFT+"2" es mudo: nunca devuelve caracter
+ ret
+
+NO_SHIFT:
+ call RESET_CAPS_EDGE
+
+ ld a, (__ZX81SD_KBD_OTHER_VALID)
+ or a
+ jr z, NO_KEY
+
+ ; Sin SHIFT: __ZX81SD_UNSHIFT_TABLE (minuscula), salvo que CAPS LOCK
+ ; este activo, en cuyo caso se usa __ZX81SD_CAPS_TABLE (mayuscula en
+ ; las letras). "." cae aqui igual que cualquier otra tecla: siempre
+ ; devuelve "." (o "," si SHIFT, ya cubierto arriba).
+ ld a, (__ZX81SD_KBD_CAPSLOCK)
+ or a
+ jr z, USE_UNSHIFT
+ jr USE_CAPS_FOR_PLAIN
+
+USE_UNSHIFT:
+ ld hl, __ZX81SD_UNSHIFT_TABLE
+ jr LOOKUP_OTHER_IN_HL
+
+USE_CAPS_FOR_PLAIN:
+ ld hl, __ZX81SD_CAPS_TABLE
+
+LOOKUP_OTHER_IN_HL:
+ ld a, (__ZX81SD_KBD_OTHER_IDX)
+ ld e, a
+ ld d, 0
+ add hl, de
+ ld a, (hl)
+ or a
+ ret
+
+NO_KEY:
+ xor a
+ ret
+
+RESET_CAPS_EDGE:
+ xor a
+ ld (__ZX81SD_KBD_CAPS_EDGE), a
+ ret
+
+; ---------------------------------------------------------------------------
+; FIND_OTHER — busca una tecla pulsada distinta de SHIFT (fila 0, col 0).
+; Devuelve acarreo INACTIVO si no encuentra ninguna; si encuentra una,
+; acarreo ACTIVO y A = indice (0-38, formula fila*5+columna-1). Se usa el
+; acarreo (no Z) porque el indice 0 (tecla Z) es un resultado valido con
+; A=0, que haria Z activo por error si se usara ese flag para
+; "encontrada/no encontrada".
+; Registros modificados: AF, BC, DE, HL.
+; ---------------------------------------------------------------------------
+FIND_OTHER:
+ ld d, 0 ; D = indice de fila actual (0-7)
+ ld b, $FE ; B = mitad alta del puerto ($FEFE...$7FFE)
+
+ROW_LOOP:
+ ld c, $FE
+ in a, (c) ; lee la fila; bits 0-4 = columnas (0 = pulsada)
+ and $1F
+
+ ld l, a ; L = mapa de bits de columnas pulsadas
+
+ ld a, d
+ or a
+ jr nz, NOT_ROW0
+ set 0, l ; fila 0: descartar SHIFT (columna 0)
+NOT_ROW0:
+ ld a, l
+ cp $1F
+ jr z, NEXT_ROW ; nada mas pulsado en esta fila
+
+FIND_COL:
+ ; Bucle desenrollado a proposito: usar CP para comparar el contador
+ ; de columna sobreescribia A (el mapa de bits que se iba rotando)
+ ; antes de que la siguiente vuelta pudiera comprobarlo, con lo que
+ ; solo la columna 0 se detectaba bien. Sin bucle no hay ese riesgo.
+ ld a, l
+ rrca
+ jr nc, GOT_COL0
+ rrca
+ jr nc, GOT_COL1
+ rrca
+ jr nc, GOT_COL2
+ rrca
+ jr nc, GOT_COL3
+ rrca
+ jr nc, GOT_COL4
+ jr NEXT_ROW ; no deberia ocurrir (ya se comprobo cp $1F antes)
+
+GOT_COL0:
+ ld c, 0
+ jr GOT_KEY
+GOT_COL1:
+ ld c, 1
+ jr GOT_KEY
+GOT_COL2:
+ ld c, 2
+ jr GOT_KEY
+GOT_COL3:
+ ld c, 3
+ jr GOT_KEY
+GOT_COL4:
+ ld c, 4
+
+GOT_KEY:
+ ; indice de tabla = fila*5 + columna - 1
+ ; (la fila 0 solo aporta 4 teclas: columnas 1-4, de ahi el -1)
+ ld a, d
+ add a, a
+ add a, a
+ add a, d ; A = fila*5
+ add a, c ; A = fila*5 + columna
+ dec a ; A = indice (0-38)
+ jr OTHER_FOUND
+
+NEXT_ROW:
+ rlc b
+ inc d
+ ld a, d
+ cp 8
+ jr nz, ROW_LOOP
+
+OTHER_NONE:
+ xor a
+ ret ; carry inactivo (xor siempre lo limpia)
+
+OTHER_FOUND:
+ scf ; carry activo; A ya tiene el indice (GOT_KEY)
+ ret
+
+ ENDP
+
+; ---------------------------------------------------------------------------
+; __ZX81SD_SYMBOL_FOR — dado un caracter ASCII (el que devolveria una
+; pulsacion normal sin modificadores, es decir uno de los que aparecen en
+; __ZX81SD_UNSHIFT_TABLE), devuelve el simbolo del ZX81 que le
+; corresponde bajo el antiguo modificador SHIFT del ZX81 (o 0 si esa
+; tecla no tiene simbolo). La usa stdlib/input.bas para componer simbolos
+; como una "tecla muerta": el usuario pulsa "." y LUEGO esta tecla, sin
+; necesitar mantenerlas pulsadas a la vez (ver cabecera del fichero).
+;
+; Entrada: A = caracter ASCII de la segunda tecla
+; Salida: A = simbolo del ZX81, o 0 si esa tecla no tiene simbolo
+; Registros modificados: AF, BC, DE, HL
+; ---------------------------------------------------------------------------
+__ZX81SD_SYMBOL_FOR:
+ PROC
+ LOCAL SEARCH_LOOP
+ LOCAL FOUND
+ LOCAL NOT_FOUND
+
+ ld c, a ; C = caracter buscado
+ ld hl, __ZX81SD_UNSHIFT_TABLE
+ ld de, __ZX81SD_SYMBOL_TABLE
+ ld b, 39 ; numero de entradas de ambas tablas
+
+SEARCH_LOOP:
+ ld a, (hl)
+ cp c
+ jr z, FOUND
+ inc hl
+ inc de
+ djnz SEARCH_LOOP
+ jr NOT_FOUND
+
+FOUND:
+ ld a, (de)
+ or a
+ ret
+
+NOT_FOUND:
+ xor a
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/io/sd81_mcu.asm b/src/lib/arch/zx81sd/runtime/io/sd81_mcu.asm
new file mode 100644
index 000000000..1e473e009
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/io/sd81_mcu.asm
@@ -0,0 +1,274 @@
+; sd81_mcu.asm (zx81sd) — Primitivas del protocolo del MCU del SD81 Booster
+; para uso desde el RUNTIME en ensamblador (load.asm / save.asm).
+;
+; (La stdlib mcu.bas lleva sus propias copias de estas rutinas dentro de
+; SUBs fastcall; aqui viven las de los statements LOAD/SAVE del compilador.)
+;
+; Protocolo: puerto $A7 = datos, puerto $AF = reloj (bit 7). El MCU
+; invierte el bit de reloj cuando procesa cada lectura/escritura de $A7;
+; tras cada operacion hay que esperar ese cambio. NUNCA escribir en $AF
+; (provoca un reset del MCU).
+
+ push namespace core
+
+SD81_MCU_DATA EQU $A7
+SD81_MCU_CLOCK EQU $AF
+
+; ---------------------------------------------------------------------------
+; SD81_MCU_SEND — Envia el byte A al MCU y espera a que lo procese.
+; Preserva BC, DE, HL. Modifica AF.
+; ---------------------------------------------------------------------------
+SD81_MCU_SEND:
+ PROC
+ LOCAL WAIT
+ push bc
+ ld b, a ; salva el dato
+ in a, (SD81_MCU_CLOCK)
+ ld c, a ; C = reloj previo
+ ld a, b
+ out (SD81_MCU_DATA), a
+WAIT:
+ in a, (SD81_MCU_CLOCK)
+ xor c
+ jp p, WAIT
+ pop bc
+ ret
+ ENDP
+
+; ---------------------------------------------------------------------------
+; SD81_MCU_RECV — Lee un byte del MCU en A y espera el toggle posterior.
+; Preserva BC, DE, HL. Modifica AF.
+; ---------------------------------------------------------------------------
+SD81_MCU_RECV:
+ PROC
+ LOCAL WAIT
+ push bc
+ in a, (SD81_MCU_CLOCK)
+ ld c, a ; reloj previo
+ in a, (SD81_MCU_DATA) ; lee el dato (dispara el toggle del MCU)
+ ld b, a
+WAIT:
+ in a, (SD81_MCU_CLOCK)
+ xor c
+ jp p, WAIT
+ ld a, b
+ pop bc
+ ret
+ ENDP
+
+; ---------------------------------------------------------------------------
+; SD81_MCU_SEND_BLOCK — Envia BC bytes desde (HL) al MCU.
+; Modifica AF, BC, E, HL. Devuelve HL apuntando tras el bloque.
+; ---------------------------------------------------------------------------
+SD81_MCU_SEND_BLOCK:
+ PROC
+ LOCAL LOOP, WAIT
+ ld a, b
+ or c
+ ret z
+LOOP:
+ in a, (SD81_MCU_CLOCK)
+ ld e, a ; reloj previo
+ ld a, (hl)
+ out (SD81_MCU_DATA), a
+WAIT:
+ in a, (SD81_MCU_CLOCK)
+ xor e
+ jp p, WAIT
+ inc hl
+ dec bc
+ ld a, b
+ or c
+ jr nz, LOOP
+ ret
+ ENDP
+
+; ---------------------------------------------------------------------------
+; SD81_MCU_RECV_BLOCK — Recibe BC bytes del MCU en (HL).
+; Modifica AF, BC, E, HL.
+; ---------------------------------------------------------------------------
+SD81_MCU_RECV_BLOCK:
+ PROC
+ LOCAL LOOP, WAIT
+ ld a, b
+ or c
+ ret z
+LOOP:
+ in a, (SD81_MCU_CLOCK)
+ ld e, a
+ in a, (SD81_MCU_DATA)
+ ld (hl), a
+WAIT:
+ in a, (SD81_MCU_CLOCK)
+ xor e
+ jp p, WAIT
+ inc hl
+ dec bc
+ ld a, b
+ or c
+ jr nz, LOOP
+ ret
+ ENDP
+
+; ---------------------------------------------------------------------------
+; SD81_MCU_RECV_VERIFY — Recibe BC bytes y los compara con (HL).
+; Devuelve A = 0 si todo coincide, A = 1 si hubo diferencias.
+; (Consume siempre los BC bytes para no desincronizar el protocolo.)
+; Modifica AF, BC, DE, HL.
+; ---------------------------------------------------------------------------
+SD81_MCU_RECV_VERIFY:
+ PROC
+ LOCAL LOOP, WAIT, SAME
+ ld d, 0 ; flag de diferencias
+ ld a, b
+ or c
+ jr z, SAME
+LOOP:
+ in a, (SD81_MCU_CLOCK)
+ ld e, a
+ in a, (SD81_MCU_DATA)
+ cp (hl)
+ jr z, WAIT
+ ld d, 1 ; difiere
+WAIT:
+ in a, (SD81_MCU_CLOCK)
+ xor e
+ jp p, WAIT
+ inc hl
+ dec bc
+ ld a, b
+ or c
+ jr nz, LOOP
+SAME:
+ ld a, d
+ ret
+ ENDP
+
+; ---------------------------------------------------------------------------
+; SD81_MCU_RECV_SINK — Recibe y descarta BC bytes (mantiene el protocolo
+; sincronizado cuando el fichero es mayor que el buffer del llamador).
+; Modifica AF, BC, E.
+; ---------------------------------------------------------------------------
+SD81_MCU_RECV_SINK:
+ PROC
+ LOCAL LOOP, WAIT
+ ld a, b
+ or c
+ ret z
+LOOP:
+ in a, (SD81_MCU_CLOCK)
+ ld e, a
+ in a, (SD81_MCU_DATA)
+WAIT:
+ in a, (SD81_MCU_CLOCK)
+ xor e
+ jp p, WAIT
+ dec bc
+ ld a, b
+ or c
+ jr nz, LOOP
+ ret
+ ENDP
+
+; ---------------------------------------------------------------------------
+; SD81_ASC2ZX — Convierte el caracter ASCII en A a codigo de caracter
+; ZX81 (los nombres de fichero de los comandos del MCU viajan en ZX81).
+; Sin equivalente -> '?' ($0F). Preserva BC, DE, HL.
+; ---------------------------------------------------------------------------
+SD81_ASC2ZX:
+ PROC
+ LOCAL NOT_LOWER, NOT_UPPER, NOT_DIGIT, TABLE, TLOOP, TDONE
+ cp 'a'
+ jr c, NOT_LOWER
+ cp 'z' + 1
+ jr nc, NOT_LOWER
+ sub 32 ; a mayuscula
+NOT_LOWER:
+ cp 'A'
+ jr c, NOT_UPPER
+ cp 'Z' + 1
+ jr nc, NOT_UPPER
+ sub 'A' - 38 ; letras: codigos 38-63
+ ret
+NOT_UPPER:
+ cp '0'
+ jr c, NOT_DIGIT
+ cp '9' + 1
+ jr nc, NOT_DIGIT
+ sub '0' - 28 ; digitos: codigos 28-37
+ ret
+NOT_DIGIT:
+ push hl
+ push bc
+ ld hl, TABLE
+ ld b, (TDONE - TABLE) / 2
+TLOOP:
+ cp (hl)
+ inc hl
+ jr z, TDONE ; encontrado: (HL) = codigo ZX81
+ inc hl
+ djnz TLOOP
+ ld a, $0F ; sin equivalente: '?'
+ pop bc
+ pop hl
+ ret
+TDONE:
+ ld a, (hl)
+ pop bc
+ pop hl
+ ret
+
+TABLE: ; pares ASCII, codigo ZX81
+ defb ' ', $00
+ defb '.', $1B
+ defb '/', $18
+ defb '-', $16
+ defb '*', $17
+ defb '<', $13
+ defb '>', $12
+ defb '(', $10
+ defb ')', $11
+ defb '$', $0D
+ defb ':', $0E
+ defb ',', $1A
+ defb ';', $19
+ defb '+', $15
+ defb '=', $14
+ defb '"', $0B
+ ENDP
+
+; ---------------------------------------------------------------------------
+; SD81_MCU_SEND_NAME — Envia un string de zxbasic (prefijo de longitud de
+; 16 bits + datos) como cadena Pascal para el MCU: byte de longitud
+; (recortada a 255) + caracteres convertidos a codigos ZX81.
+; Entrada: HL = puntero al string (al prefijo de longitud). HL != 0.
+; Modifica AF, BC, HL.
+; ---------------------------------------------------------------------------
+SD81_MCU_SEND_NAME:
+ PROC
+ LOCAL LOOP, LENOK
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ inc hl ; BC = longitud, HL -> caracteres
+ ld a, b
+ or a
+ jr z, LENOK
+ ld c, 255 ; recorta a 255 (limite del protocolo)
+LENOK:
+ ld a, c
+ call SD81_MCU_SEND ; longitud
+ ld a, c
+ or a
+ ret z
+ ld b, c
+LOOP:
+ ld a, (hl)
+ call SD81_ASC2ZX
+ call SD81_MCU_SEND
+ inc hl
+ djnz LOOP
+ ret
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/io/sound/beep.asm b/src/lib/arch/zx81sd/runtime/io/sound/beep.asm
new file mode 100644
index 000000000..f2562fcee
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/io/sound/beep.asm
@@ -0,0 +1,208 @@
+; beep.asm (zx81sd) — Comando BEEP duracion, tono (version con expresiones)
+;
+; Sustituye a zx48k/runtime/io/sound/beep.asm, que llama a BEEP ($03F8) de
+; la ROM Spectrum. Aqui se porta la rutina original usando el calculador FP
+; propio (fp_calc.asm) y el bucle de beeper propio (io/sound/beeper.asm).
+;
+; BEEP dur, pitch
+; dur = duracion en segundos (0 a 10)
+; pitch = semitonos sobre/bajo el DO central (-60 a 127)
+;
+; Diferencias con la ROM:
+; - MEM-0 esta en FP_MEM_AREA (sysvars propias), no en $5C92.
+; - FIND-INT1/FIND-INT2 se sustituyen por __FPSTACK_POP + __FTOU32REG
+; (ya portados y verificados en zx81sd).
+; - La constante 437500 (= 3.5MHz/8 del Spectrum) se sustituye por
+; 406250 (= 3.25MHz/8 del ZX81). El resto del bytecode es identico.
+;
+; Entrada (convencion del compilador, igual que en zx48k):
+; Duracion en A,E,D,C,B (float); tono en la pila de maquina (float).
+
+#include once
+#include once
+#include once
+#include once
+#include once
+
+ push namespace core
+
+BEEP:
+ PROC
+ LOCAL BEEP_I_OK, BEEP_OCTAVE, BEEP_ERROR, BEEP_SEMITONES
+
+ call __FPSTACK_PUSH ; duracion -> pila FP
+
+ pop hl ; direccion de retorno
+ pop af
+ pop de
+ pop bc ; tono (float) desde la pila de maquina
+ push hl ; CALLEE
+
+ call __FPSTACK_PUSH ; tono -> pila FP
+
+; Igual que la ROM ($03F8): separa el tono en parte entera (mem-0) y
+; fraccionaria, y deja en la pila 1 + 0.05776226 * frac(tono).
+
+ rst 28h ;; FP-CALC dur, tono.
+ defb $31 ;;duplicate dur, tono, tono.
+ defb $27 ;;int dur, tono, int(tono).
+ defb $C0 ;;st-mem-0 (tono entero a mem-0)
+ defb $03 ;;subtract dur, frac(tono).
+ defb $34 ;;stk-data constante 0.05776226
+ defb $EC ;;Exponent: $7C, Bytes: 4
+ defb $6C, $98, $1F, $F5
+ defb $04 ;;multiply
+ defb $A1 ;;stk-one
+ defb $0F ;;addition dur, 1 + 0.0577*frac.
+ defb $38 ;;end-calc
+
+; mem-0 contiene el tono entero en formato entero-pequeno:
+; 0, signo (0/FF), LSB, MSB, 0. Comprueba -128 <= tono <= 127.
+
+ ld hl, FP_MEM_AREA
+ ld a, (hl) ; el primer byte debe ser 0 (forma entera)
+ and a
+ jp nz, BEEP_ERROR
+
+ inc hl
+ ld c, (hl) ; C = byte de signo (0/FF)
+ inc hl
+ ld b, (hl) ; B = LSB (complemento a dos)
+ ld a, b
+ rla
+ sbc a, a ; A = 0/FF segun el bit 7 de B
+ cp c ; debe coincidir con el signo
+ jp nz, BEEP_ERROR
+
+ inc hl
+ cp (hl) ; y el MSB debe ser 0/FF igualmente
+ jp nz, BEEP_ERROR
+
+ ld a, b ; A = tono + 60
+ add a, $3C
+ jp p, BEEP_I_OK ; si -60 <= tono, sigue
+
+ jp po, BEEP_ERROR ; fuera de rango por abajo
+
+BEEP_I_OK: ; aqui -60 <= tono <= 127, A = tono+60 (0-187)
+ ld b, $FA ; B = -6 octavas bajo el DO central
+
+BEEP_OCTAVE:
+ inc b ; octava siguiente
+ sub $0C ; 12 semitonos = 1 octava
+ jr nc, BEEP_OCTAVE
+
+ add a, $0C ; A = semitonos sobre DO (0-11)
+ push bc ; B = desplazamiento de octava (-5..10)
+
+ ; HL = BEEP_SEMITONES + A*5 (LOC-MEM de la ROM)
+ ld c, a
+ add a, a
+ add a, a
+ add a, c ; A = A*5 (max 55, sin acarreo)
+ ld c, a
+ ld b, 0
+ ld hl, BEEP_SEMITONES
+ add hl, bc
+
+ ; STACK-NUM: apila el float de la tabla (frecuencia del semitono)
+ ld a, (hl)
+ inc hl
+ ld e, (hl)
+ inc hl
+ ld d, (hl)
+ inc hl
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ call __FPSTACK_PUSH
+
+ rst 28h ;; FP-CALC dur, factor, freq.
+ defb $04 ;;multiply dur, freq ajustada a frac(tono).
+ defb $38 ;;end-calc (HL -> exponente del resultado)
+
+ pop af ; A = desplazamiento de octava
+ add a, (hl) ; freq *= 2^octava (suma al exponente)
+ ld (hl), a
+
+ rst 28h ;; FP-CALC dur, freq.
+ defb $C0 ;;st-mem-0 (frecuencia a mem-0)
+ defb $02 ;;delete dur.
+ defb $31 ;;duplicate dur, dur.
+ defb $38 ;;end-calc
+
+ ; comprueba 0 <= duracion <= 10 (como FIND-INT1 + CP 11 de la ROM)
+ call __FPSTACK_POP
+ bit 7, e ; negativa -> error
+ jp nz, BEEP_ERROR
+ call __FTOU32REG ; DEHL = int(duracion)
+ ld a, d
+ or e
+ or h
+ jp nz, BEEP_ERROR
+ ld a, l
+ cp $0B
+ jp nc, BEEP_ERROR
+
+; Calcula los parametros del bucle del beeper:
+; ciclos = duracion * frecuencia
+; periodo = 406250 / frecuencia - 30.125 (406250 = 3.25MHz / 8)
+
+ rst 28h ;; FP-CALC dur.
+ defb $E0 ;;get-mem-0 dur, freq.
+ defb $04 ;;multiply ciclos.
+ defb $E0 ;;get-mem-0 ciclos, freq.
+ defb $34 ;;stk-data constante 406250
+ defb $80 ;;Exponent: $93, Bytes: 3
+ defb $43
+ defb $46, $5D, $40
+ defb $01 ;;exchange ciclos, 406250, freq.
+ defb $05 ;;division ciclos, 406250/freq.
+ defb $34 ;;stk-data constante 30.125
+ defb $35 ;;Exponent: $85, Bytes: 1
+ defb $71
+ defb $03 ;;subtract ciclos, periodo.
+ defb $38 ;;end-calc
+
+ call __FPSTACK_POP
+ call __FTOU32REG ; HL = periodo
+ push hl
+ call __FPSTACK_POP
+ call __FTOU32REG ; HL = ciclos
+ ex de, hl ; DE = ciclos
+ pop hl ; HL = periodo
+
+ ld a, d
+ or e
+ ret z ; duracion 0: nada que hacer (evita 65536 ciclos)
+ dec de ; DE = ciclos - 1
+
+ push ix ; el beeper usa IX (frame pointer del compilador)
+ call __ZX81SD_BEEPER
+ pop ix
+ ret
+
+BEEP_ERROR:
+ ld a, ERROR_IntOutOfRange
+ jp __ERROR
+
+; Tabla de semitonos de la ROM ($046E): frecuencias de la octava central.
+; Octavas arriba/abajo = multiplicar por 2^n (se suma n al exponente).
+
+BEEP_SEMITONES:
+ defb $89, $02, $D0, $12, $86 ; 261.625565290 DO
+ defb $89, $0A, $97, $60, $75 ; 277.182631135 DO#
+ defb $89, $12, $D5, $17, $1F ; 293.664768100 RE
+ defb $89, $1B, $90, $41, $02 ; 311.126983881 RE#
+ defb $89, $24, $D0, $53, $CA ; 329.627557039 MI
+ defb $89, $2E, $9D, $36, $B1 ; 349.228231549 FA
+ defb $89, $38, $FF, $49, $3E ; 369.994422674 FA#
+ defb $89, $43, $FF, $6A, $73 ; 391.995436072 SOL
+ defb $89, $4F, $A7, $00, $54 ; 415.304697513 SOL#
+ defb $89, $5C, $00, $00, $00 ; 440.000000000 LA
+ defb $89, $69, $14, $F6, $24 ; 466.163761616 LA#
+ defb $89, $76, $F1, $10, $05 ; 493.883301378 SI
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/io/sound/beeper.asm b/src/lib/arch/zx81sd/runtime/io/sound/beeper.asm
new file mode 100644
index 000000000..1f99f7c98
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/io/sound/beeper.asm
@@ -0,0 +1,142 @@
+; beeper.asm (zx81sd) — Generador de onda cuadrada por el beeper del SD81
+;
+; Sustituye a zx48k/runtime/io/sound/beeper.asm, que llama a BEEPER ($03B5)
+; de la ROM Spectrum. Aqui se porta el bucle original de la ROM adaptando:
+;
+; - Puerto: $FB (ULA emulada del SD81 en modo HiRes Spectrum) en vez de
+; $FE. Mismo formato: bits 2-0 = borde, bit 3 = MIC, bit 4 = altavoz.
+; El borde se toma de la copia sombra __ZX81SD_ULA_SHADOW (beep.asm)
+; en vez de BORDCR ($5C48), y se restaura al terminar.
+; - Reloj: el bucle es identico en T-states (no depende del reloj), pero
+; el periodo HL que llega del COMPILADOR (BEEP const,const se precalcula
+; en el frontend con la formula del Spectrum: HL = 437500/f - 30.125,
+; 437500 = 3.5MHz/8) hay que corregirlo para los 3.25 MHz del ZX81:
+; HL' = 406250/f - 30.125 = HL*13/14 - 2.15 (406250/437500 = 13/14)
+; La entrada __BEEPER (la que usa el compilador) aplica esa correccion.
+; La entrada __ZX81SD_BEEPER (interna) recibe el periodo ya en unidades
+; de 3.25 MHz y no corrige nada (la usa nuestro BEEP de beep.asm, que
+; calcula directamente con 406250).
+; - Interrupciones: la ROM hace DI...EI. El runtime zx81sd se ejecuta con
+; las interrupciones deshabilitadas (la FPGA genera el video), asi que
+; se mantiene el DI (inocuo) y NO se hace EI al final.
+;
+; __ZX81SD_BEEPER — entrada directa (formato ROM $03B5):
+; DE = numero de ciclos - 1
+; HL = periodo del tono: T-states = 236 + 8*HL (a 3.25 MHz)
+; Modifica: AF, BC, DE, HL, IX (el llamador debe preservar IX si lo usa)
+
+#include once ; __ZX81SD_ULA_SHADOW (sombra borde/beeper)
+
+ push namespace core
+
+__ZX81SD_BEEPER:
+ PROC
+ LOCAL BE_IX3, BE_IX2, BE_IX1, BE_IX0
+ LOCAL BE_HL_LP, BE_AGAIN, BE_END
+
+ di ; timing exacto (ya suelen estar deshabilitadas)
+ ld a, l
+ srl l
+ srl l ; L = parte media del periodo
+ cpl
+ and $03 ; A = 3 - parte fina del periodo
+ ld c, a
+ ld b, $00
+ ld ix, BE_IX3
+ add ix, bc ; IX = entrada al bucle con 0-3 NOPs (parte fina)
+
+ ld a, (__ZX81SD_ULA_SHADOW)
+ and $07 ; bits 2-0 = borde actual
+ or $08 ; bit 3 (MIC) a 1, como la ROM
+
+BE_IX3:
+ nop ;(4) NOPs opcionales: ajuste fino del periodo
+BE_IX2:
+ nop ;(4)
+BE_IX1:
+ nop ;(4)
+BE_IX0:
+ inc b ;(4)
+ inc c ;(4)
+
+BE_HL_LP:
+ dec c ;(4) bucle de duracion del semiciclo
+ jr nz, BE_HL_LP ;(12/7)
+
+ ld c, $3F ;(7)
+ dec b ;(4)
+ jp nz, BE_HL_LP ;(10)
+
+ xor $10 ;(7) conmuta el bit del altavoz
+ out (SD81_ULA_PORT), a ;(11)
+ ld b, h ;(4) B = parte gruesa del periodo
+ ld c, a ;(4) salva el byte del puerto
+ bit 4, a ;(8) si la salida quedo alta,
+ jr nz, BE_AGAIN ;(12/7) hace el semiciclo alto
+
+ ld a, d ;(4) ciclo completo (bajo->bajo):
+ or e ;(4) ¿quedan ciclos?
+ jr z, BE_END ;(12/7)
+
+ ld a, c ;(4) restaura el byte del puerto
+ ld c, l ;(4) C = parte media del periodo
+ dec de ;(6)
+ jp (ix) ;(8) siguiente ciclo
+
+BE_AGAIN: ; a mitad de ciclo
+ ld c, l ;(4)
+ inc c ;(4) +16 T para igualar semiciclo alto y bajo
+ jp (ix) ;(8)
+
+BE_END:
+ ld a, (__ZX81SD_ULA_SHADOW)
+ out (SD81_ULA_PORT), a ; deja borde/beeper como estaban (sin EI: ver cabecera)
+ ret
+
+ ENDP
+
+; ---------------------------------------------------------------------------
+; __BEEPER — Entrada del compilador para BEEP ,
+; HL (fastcall) = numero de ciclos - 1
+; (SP+2) en la pila = periodo calculado por el frontend PARA 3.5 MHz
+; Corrige el periodo a 3.25 MHz (HL' = HL - HL/14 - 2) y llama al bucle.
+; ---------------------------------------------------------------------------
+__BEEPER:
+ PROC
+ LOCAL DIV14, DIV14_SKIP
+
+ ex de, hl ; DE = ciclos - 1
+ pop hl ; direccion de retorno
+ ex (sp), hl ; HL = periodo (unidades Spectrum) — CALLEE
+
+ push de ; salva ciclos
+ push hl ; salva periodo original
+ ex de, hl ; DE = dividendo (periodo)
+ xor a ; A = resto
+ ld c, 14
+ ld b, 16
+DIV14:
+ sla e ; desplaza el dividendo (bit 0 entra a 0;
+ rl d ; NO usar rl e: arrastraria el acarreo residual
+ rla ; del cp/sub de la vuelta anterior)
+ cp c
+ jr c, DIV14_SKIP
+ sub c
+ inc e
+DIV14_SKIP:
+ djnz DIV14 ; DE = periodo / 14
+ pop hl ; periodo original
+ or a
+ sbc hl, de ; HL = periodo - periodo/14 (= periodo*13/14)
+ dec hl
+ dec hl ; -2 (~ -30.125*(1-13/14), redondeado)
+ pop de ; ciclos - 1
+
+ push ix ; el bucle usa IX; el compilador usa IX como frame
+ call __ZX81SD_BEEPER
+ pop ix
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/load.asm b/src/lib/arch/zx81sd/runtime/load.asm
new file mode 100644
index 000000000..21f858551
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/load.asm
@@ -0,0 +1,160 @@
+; load.asm (zx81sd) — LOAD/VERIFY ... CODE desde la tarjeta SD
+;
+; Sustituye a zx48k/runtime/load.asm (que usa LD-BYTES de cinta, puerto
+; $FE y cabeceras de cassette del Spectrum). Aqui el statement LOAD de
+; zxbasic carga el fichero desde la SD del SD81 Booster con el comando 9
+; del MCU (ver io/sd81_mcu.asm para el protocolo).
+;
+; Interfaz del compilador (identica a zx48k):
+; pila: A = 1 LOAD / 0 VERIFY; BC = longitud; DE = direccion destino;
+; HL = string de zxbasic con el nombre (hay que liberarlo).
+;
+; Semantica:
+; - BC = 0: carga el fichero completo en DE.
+; - BC > 0: carga como maximo BC bytes en DE; si el fichero es mayor
+; se descarta el resto (el protocolo obliga a consumirlo); si es
+; menor se marca error de carga.
+; - VERIFY: compara en vez de escribir; diferencia = error de carga.
+; - Error (fichero no existe, etc.): ERR_NR = ERROR_TapeLoadingErr y
+; se retorna (mismo comportamiento "blando" que la version de cinta).
+; - Ojo con extensiones especiales del MCU: un .ROM resetea la CPU
+; (no retorna) y un .WAV solo se reproduce (carga 0 bytes).
+
+#include once
+#include once
+#include once
+
+ push namespace core
+
+LOAD_CODE:
+ PROC
+ LOCAL LC_FLAG, LC_REQ, LC_DEST, LC_SIZE, LC_N
+ LOCAL LC_HAVE_SIZE, LC_USE_REQ, LC_DO_RECV, LC_DRAIN
+ LOCAL LC_ERROR, LC_END, LC_ERR_NOFREE
+ LOCAL LC_VERIFY
+
+ pop hl ; direccion de retorno
+ pop af ; A = 1 LOAD / 0 VERIFY
+ pop bc ; longitud solicitada
+ pop de ; direccion destino
+ ex (sp), hl ; CALLEE: HL = string con el nombre
+
+ ld (LC_FLAG), a
+ ld (LC_REQ), bc
+ ld (LC_DEST), de
+
+ ld a, h
+ or l
+ jp z, LC_ERR_NOFREE ; string nulo
+
+ push hl ; salva el puntero para MEM_FREE
+
+ ; longitud del nombre 0 -> error (tras liberar)
+ ld e, (hl)
+ inc hl
+ ld d, (hl)
+ dec hl
+ ld a, d
+ or e
+ jr nz, LC_HAVE_SIZE
+
+ pop hl
+ call MEM_FREE
+ jp LC_ERR_NOFREE
+
+LC_HAVE_SIZE:
+ ; comando 9 (LOAD) + nombre
+ ld a, 9
+ call SD81_MCU_SEND
+ call SD81_MCU_SEND_NAME
+ pop hl
+ call MEM_FREE ; el nombre ya viajo: libera el string
+
+ ; tamano real del fichero (little-endian)
+ call SD81_MCU_RECV
+ ld l, a
+ call SD81_MCU_RECV
+ ld h, a
+ ld (LC_SIZE), hl
+
+ ; n = (LC_REQ = 0) ? size : min(LC_REQ, size)
+ ld bc, (LC_REQ)
+ ld a, b
+ or c
+ jr z, LC_DO_RECV ; BC=0: n = size (ya en HL)
+ or a
+ sbc hl, bc ; size - req
+ jr nc, LC_USE_REQ ; size >= req: n = req
+ ld hl, (LC_SIZE) ; size < req: n = size (y sera error corto)
+ jr LC_DO_RECV
+LC_USE_REQ:
+ ld hl, (LC_REQ)
+
+LC_DO_RECV:
+ ld (LC_N), hl
+ ld b, h
+ ld c, l ; BC = n
+ ld hl, (LC_DEST)
+ ld a, (LC_FLAG)
+ or a
+ jr z, LC_VERIFY
+
+ call SD81_MCU_RECV_BLOCK
+ xor a ; sin diferencias
+ jr LC_DRAIN
+
+LC_VERIFY:
+ call SD81_MCU_RECV_VERIFY ; A = 1 si hubo diferencias
+
+LC_DRAIN:
+ push af ; salva el resultado de la verificacion
+ ; descarta lo que sobre: size - n
+ ld hl, (LC_SIZE)
+ ld bc, (LC_N)
+ or a
+ sbc hl, bc
+ ld b, h
+ ld c, l
+ call SD81_MCU_RECV_SINK
+
+ call SD81_MCU_RECV ; byte de estado del MCU
+ ld e, a
+ pop af
+ or e ; error si status != 0 o verificacion fallida
+ jp nz, LC_ERROR
+
+ ; carga corta: se pidieron LC_REQ (>0) bytes y el fichero era menor
+ ld bc, (LC_REQ)
+ ld a, b
+ or c
+ jr z, LC_END
+ ld hl, (LC_N)
+ or a
+ sbc hl, bc
+ jr z, LC_END ; n = req: completo
+
+LC_ERROR:
+ ld a, ERROR_TapeLoadingErr
+ ld (ERR_NR), a
+LC_END:
+ ret
+
+LC_ERR_NOFREE:
+ ld a, ERROR_InvalidFileName
+ ld (ERR_NR), a
+ ret
+
+LC_FLAG:
+ defb 0
+LC_REQ:
+ defw 0
+LC_DEST:
+ defw 0
+LC_SIZE:
+ defw 0
+LC_N:
+ defw 0
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/paging.asm b/src/lib/arch/zx81sd/runtime/paging.asm
new file mode 100644
index 000000000..5433ff188
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/paging.asm
@@ -0,0 +1,54 @@
+; PAGING — Rutinas de paginación de memoria del SD81 Booster
+; Puerto $E7h: mapeador de memoria en bloques de 8KB
+;
+; Modo simple (hasta 256KB, 32 páginas):
+; OUT ($E7h), A con A = (página << 3) | bloque
+;
+; Modo completo (hasta 512KB, 64 páginas):
+; OUT (C), A con A = bloque (0-7), B = página (0-63), C = $E7h
+;
+; Los bloques 0-3 ($0000-$7FFF) son ejecutables.
+; Los bloques 4-7 ($8000-$FFFF) son solo datos (sin MC45).
+; Los bloques 6-7 deben ser consistentes con el HFILE en modo normal del ZX81;
+; en modo Superfast HiRes Spectrum la FPGA ignora esa restricción.
+
+ push namespace core
+
+SD81_PAGE_PORT EQU $E7 ; Puerto del mapeador de memoria
+
+; PAGE_SET_SIMPLE — Asigna página a bloque (modo simple, hasta 32 páginas)
+; Entrada: A = bloque (0-7), B = página (0-31)
+; Destruye: A, C
+PAGE_SET_SIMPLE:
+ ld c, a ; C = bloque
+ ld a, b
+ sla a
+ sla a
+ sla a ; A = página << 3
+ or c ; A = (página << 3) | bloque
+ out (SD81_PAGE_PORT), a
+ ret
+
+; PAGE_SET_FULL — Asigna página a bloque (modo completo, hasta 64 páginas)
+; Entrada: A = bloque (0-7), B = página (0-63)
+; Destruye: C
+PAGE_SET_FULL:
+ ld c, SD81_PAGE_PORT
+ out (c), a ; A = bloque, B = página
+ ret
+
+; PAGE_SET_SCREEN — Mapea la página de pantalla al bloque 6 ($C000)
+; Entrada: B = número de página RAM dedicada a la pantalla
+; Destruye: A, C
+PAGE_SET_SCREEN:
+ ld a, 6 ; bloque 6 = $C000-$DFFF
+ jp PAGE_SET_FULL
+
+; PAGE_SET_DATA — Mapea una página de datos al bloque 7 ($E000)
+; Entrada: B = número de página
+; Destruye: A, C
+PAGE_SET_DATA:
+ ld a, 7 ; bloque 7 = $E000-$FFFF
+ jp PAGE_SET_FULL
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/pause.asm b/src/lib/arch/zx81sd/runtime/pause.asm
new file mode 100644
index 000000000..610268c32
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/pause.asm
@@ -0,0 +1,29 @@
+; -----------------------------------------------------------------------
+; PAUSE — Espera N frames usando VSYNC del SD81 Booster
+; Sustituye a CALL $1F3Dh (PAUSE_1 de la ROM Spectrum) que usa HALT.
+;
+; Entrada: HL = número de frames a esperar
+; -----------------------------------------------------------------------
+
+#include once
+
+ push namespace core
+
+__PAUSE:
+ PROC
+ LOCAL PAUSE_LOOP
+
+ ld b, h
+ ld c, l ; BC = contador de frames
+
+PAUSE_LOOP:
+ ld a, b
+ or c
+ ret z ; BC=0 → fin
+ call VSYNC_TICK ; espera un frame e incrementa FRAMES
+ dec bc
+ jr PAUSE_LOOP
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/pixel_addr.asm b/src/lib/arch/zx81sd/runtime/pixel_addr.asm
new file mode 100644
index 000000000..93a0156f8
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/pixel_addr.asm
@@ -0,0 +1,62 @@
+; PIXEL_ADDR — Calcula la dirección (offset) del byte de pantalla para (B=Y, C=X)
+; Sustituye a PIXEL_ADDR EQU $22ACh (ROM Spectrum)
+;
+; Interfaz idéntica a la ROM Spectrum para compatibilidad con plot.asm / draw.asm:
+; Entrada: A = 191 (límite superior Y), B = Y (0=abajo, 191=arriba), C = X (0-255)
+; Salida: HL = offset dentro del bitmap desde $0000 (sin base de pantalla)
+; A = X AND 7 (posición del bit, 0=izquierda/bit7, 7=derecha/bit0)
+; Destruye: B (como la ROM). DEBE preservar D y E: draw.asm salva B' en D'
+; alrededor de esta llamada (ld d,b / call PIXEL_ADDR / ld b,d), igual que
+; hacía con PIXEL-ADD ($22AC) de la ROM Spectrum, que tampoco tocaba DE.
+;
+; El llamador (plot.asm, draw.asm) añade la base de pantalla:
+; res 6, h ; no-op en nuestro caso (H siempre en $00-$17)
+; add hl, (SCREEN_ADDR)
+;
+; Organización bitmap Spectrum interleaved:
+; V = 191 - Y (Y desde abajo → V desde arriba)
+; H = (V AND $C0)>>3 | (V AND $07) → tercio + línea en tercio
+; L = (V AND $38)<<2 | (C>>3) → fila de char*32 + columna byte
+
+ push namespace core
+
+PIXEL_ADDR:
+ PROC
+
+ sub b ; A = 191 - Y (convierte coord Spectrum a offset desde arriba)
+ ld b, a ; B = V = 191-Y (B ya no se necesita: era la Y de entrada)
+
+ ; -- H: tercio (bits 12-11) + línea en tercio (bits 10-8) --
+ and $C0 ; A = (V AND $C0) = tercio * 64
+ rrca
+ rrca
+ rrca ; A = tercio * 8
+ ld h, a
+ ld a, b
+ and $07 ; A = línea en tercio (0-7)
+ or h
+ ld h, a ; H = (tercio<<3) | línea_en_tercio
+
+ ; -- L: fila de carácter (bits 7-5) + columna de byte (bits 4-0) --
+ ld a, b
+ and $38 ; A = fila_de_char * 8
+ rlca
+ rlca ; A = fila_de_char * 32 (tras AND $38 no hay acarreo al rotar)
+ ld b, a
+ ld a, c
+ rrca
+ rrca
+ rrca
+ and $1F ; A = X / 8 (columna de byte, 0-31)
+ add a, b
+ ld l, a ; L = fila_de_char*32 + col_byte
+
+ ; -- Retornar A = X AND 7 (posición del bit dentro del byte) --
+ ld a, c
+ and $07 ; A = X AND 7
+
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/plot.asm b/src/lib/arch/zx81sd/runtime/plot.asm
new file mode 100644
index 000000000..f32e3581c
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/plot.asm
@@ -0,0 +1,90 @@
+; MIXED __FASTCALL__ / __CALLEE__ PLOT Function — ZX81 + SD81 Booster
+; Adaptado de zx48k/runtime/plot.asm para sustituir las llamadas ROM
+; por implementaciones propias (sin ROM Spectrum disponible).
+;
+; Cambios respecto al original zx48k:
+; - PIXEL_ADDR: llama a nuestra implementación en pixel_addr.asm
+; - COORDS: usa el valor de sysvars.asm ($8000+), no $5C7D
+; - PLOT_SUB: eliminado (el código propio no lo llama)
+
+; Y in A (accumulator)
+; X in top of the stack
+
+#include once
+#include once
+#include once
+#include once
+#include once
+
+ push namespace core
+
+PLOT:
+ PROC
+
+ LOCAL __PLOT_ERR
+ LOCAL __PLOT_OVER1
+
+ pop hl
+ ex (sp), hl ; Callee
+
+ ld b, a
+ ld c, h
+
+#ifdef SCREEN_Y_OFFSET
+ ld a, SCREEN_Y_OFFSET
+ add a, b
+ ld b, a
+#endif
+
+#ifdef SCREEN_X_OFFSET
+ ld a, SCREEN_X_OFFSET
+ add a, c
+ ld c, a
+#endif
+
+ ld a, 191
+ cp b
+ jr c, __PLOT_ERR ; jr is faster here (#1)
+
+__PLOT: ; __FASTCALL__ entry (b, c) = pixel coords (y, x)
+ ld (COORDS), bc ; Saves current point
+ ld a, 191 ; Max y coord
+ call PIXEL_ADDR
+ res 6, h ; no-op en SD81 (H siempre en $00-$17), mantiene compatibilidad
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ld b, a
+ inc b
+ ld a, 0FEh
+LOCAL __PLOT_LOOP
+__PLOT_LOOP:
+ rrca
+ djnz __PLOT_LOOP
+
+ ld b, a
+ ld a, (P_FLAG)
+ ld c, a
+ ld a, (hl)
+ bit 0, c ; is it OVER 1
+ jr nz, __PLOT_OVER1
+ and b
+
+__PLOT_OVER1:
+ bit 2, c ; is it inverse 1
+ jr nz, __PLOT_END
+
+ xor b
+ cpl
+
+LOCAL __PLOT_END
+__PLOT_END:
+ ld (hl), a
+ jp SET_PIXEL_ADDR_ATTR
+
+__PLOT_ERR:
+ jp __OUT_OF_SCREEN_ERR ; Spent 3 bytes, but saves 3 T-States at (#1)
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/po_gr_1.asm b/src/lib/arch/zx81sd/runtime/po_gr_1.asm
new file mode 100644
index 000000000..c1435cdd5
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/po_gr_1.asm
@@ -0,0 +1,57 @@
+; PO_GR_1 — Genera el patrón de bits para caracteres gráficos 128-143
+; Sustituye a PO-GR-1 ($0B38, ROM Spectrum). Mismo algoritmo que la ROM.
+;
+; Los caracteres gráficos Spectrum (CHR$ 128 a CHR$ 143) son bloques
+; de 2×2 cuadrantes. El nibble bajo del código determina qué cuadrantes
+; están encendidos, con el mapeo EXACTO de la ROM (PO-GR-2, $0B3E):
+;
+; bit 0 = cuadrante superior DERECHO → $0F en filas 0-3
+; bit 1 = cuadrante superior IZQUIERDO → $F0 en filas 0-3
+; bit 2 = cuadrante inferior DERECHO → $0F en filas 4-7
+; bit 3 = cuadrante inferior IZQUIERDO → $F0 en filas 4-7
+;
+; (CHR$(129) = ▝, CHR$(130) = ▘, CHR$(143) = bloque macizo.)
+;
+; Entrada: B = código de carácter (128-143), solo se usan bits 3-0
+; Salida: MEM0 (8 bytes) = patrón del carácter; HL = MEM0
+; Destruye: A, B, C
+
+#include once
+
+ push namespace core
+
+PO_GR_1:
+ PROC
+ LOCAL PO_GR_HALF, PO_GR_FILL
+
+ ld a, b
+ and $0F
+ ld b, a ; B = bits de cuadrante
+ ld hl, MEM0
+ call PO_GR_HALF ; filas 0-3 (bits 0-1)
+ call PO_GR_HALF ; filas 4-7 (bits 2-3)
+ ld hl, MEM0 ; devuelve el puntero al patrón
+ ret
+
+; Construye media celda (4 filas iguales) a partir de los dos bits
+; bajos de B, desplazándolos fuera. Igual que PO-GR-2 de la ROM.
+PO_GR_HALF:
+ rr b ; bit par → carry
+ sbc a, a ; $00 o $FF
+ and $0F ; mitad derecha
+ ld c, a
+ rr b ; bit impar → carry
+ sbc a, a
+ and $F0 ; mitad izquierda
+ or c ; byte de la fila completo
+ ld c, 4
+PO_GR_FILL:
+ ld (hl), a
+ inc hl
+ dec c
+ jr nz, PO_GR_FILL
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/print.asm b/src/lib/arch/zx81sd/runtime/print.asm
new file mode 100644
index 000000000..1bb7a0285
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/print.asm
@@ -0,0 +1,588 @@
+; vim:ts=4:sw=4:et:
+; PRINT command routine
+; Does not print attribute. Use PRINT_STR or PRINT_NUM for that
+
+#include once
+#include once
+#include once
+#include once
+#include once
+#include once
+#include once
+#include once
+#include once
+#include once
+#include once
+#include once
+#include once
+
+; Putting a comment starting with @INIT
+; will make the compiler to add a CALL to
+; It is useful for initialization routines.
+#init .core.__PRINT_INIT
+
+ push namespace core
+
+__PRINT_INIT: ; To be called before program starts (initializes library)
+ PROC
+
+ ld hl, __PRINT_START
+ ld (PRINT_JUMP_STATE), hl
+
+ ;; Clears ATTR2 flags (OVER 2, etc)
+ xor a
+ ld (FLAGS2), a
+ ld hl, TV_FLAG
+ res 0, (hl)
+
+ LOCAL SET_SCR_ADDR
+ call __LOAD_S_POSN
+ jp __SET_SCR_PTR
+
+ ;; Receives HL = future value of S_POSN
+ ;; Stores it at (S_POSN) and refresh screen pointers (ATTR, SCR)
+SET_SCR_ADDR:
+ ld (S_POSN), hl
+ ex de, hl
+ ld hl, SCR_SIZE
+ or a
+ sbc hl, de
+ ex de, hl
+ dec e
+ jp __SET_SCR_PTR
+
+__PRINTCHAR: ; Print character store in accumulator (A register)
+ ; Modifies H'L', B'C', A'F', D'E', A
+
+ LOCAL PO_GR_1
+
+ LOCAL __PRCHAR
+ LOCAL __PRINT_JUMP
+ LOCAL __SRCADDR
+ LOCAL __PRINT_UDG
+ LOCAL __PRGRAPH
+ LOCAL __PRINT_START
+
+PRINT_JUMP_STATE EQU __PRINT_JUMP + 2
+
+__PRINT_JUMP:
+ exx ; Switch to alternative registers
+ jp __PRINT_START ; Where to jump. If we print 22 (AT), next two calls jumps to AT1 and AT2 respectively
+
+__PRINT_START:
+
+__PRINT_CHR:
+ cp ' '
+ jp c, __PRINT_SPECIAL ; Characters below ' ' are special ones (> 127 bytes away)
+ ex af, af' ; Saves a value (char to print) for later
+
+ ld hl, (S_POSN)
+ dec l
+ jr nz, 1f
+ ld l, SCR_COLS - 1
+ dec h
+ jr nz, 2f
+
+#ifndef __ZXB_DISABLE_SCROLL
+ inc h
+ push hl
+ call __SCROLL_SCR
+ pop hl
+#else
+ ld h, SCR_ROWS - 1
+#endif
+2:
+ call SET_SCR_ADDR
+ jr 4f
+1:
+ ld (S_POSN), hl
+4:
+ ex af, af'
+
+ cp 80h ; Is it a "normal" (printable) char
+ jr c, __SRCADDR
+
+ cp 90h ; Is it an UDG?
+ jr nc, __PRINT_UDG
+
+ ; Print an 8 bit pattern (80h to 8Fh)
+
+ ld b, a
+ call PO_GR_1 ; This ROM routine will generate the bit pattern at MEM0
+ ld hl, MEM0
+ jp __PRGRAPH
+
+; PO_GR_1 se incluye como rutina propia (po_gr_1.asm), no desde ROM Spectrum
+#include once
+
+__PRINT_UDG:
+ sub 90h ; Sub ASC code
+ ld bc, (UDG)
+ jr __PRGRAPH0
+
+__SOURCEADDR EQU (__SRCADDR + 1) ; Address of the pointer to chars source
+__SRCADDR:
+ ld bc, (CHARS)
+
+__PRGRAPH0:
+ add a, a ; A = a * 2 (since a < 80h) ; Thanks to Metalbrain at http://foro.speccy.org
+ ld l, a
+ ld h, 0 ; HL = a * 2 (accumulator)
+ add hl, hl
+ add hl, hl ; HL = a * 8
+ add hl, bc ; HL = CHARS address
+
+__PRGRAPH:
+ ex de, hl ; HL = Write Address, DE = CHARS address
+
+#ifndef __ZXB_DISABLE_BOLD
+ ld a, (FLAGS2)
+ bit 2, a
+ call nz, __BOLD
+#endif
+
+#ifndef __ZXB_DISABLE_ITALIC
+ ld a, (FLAGS2)
+ bit 4, a
+ call nz, __ITALIC
+#endif
+
+ ld hl, (DFCC)
+ push hl
+
+ ld b, 8 ; 8 bytes per char
+
+__PRCHAR:
+ ld a, (de) ; DE *must* be source, and HL destiny
+
+PRINT_MODE: ; Which operation is used to write on the screen
+ ; Set it with:
+ ; LD A,
+ ; LD (PRINT_MODE), A
+ ;
+ ; Available operations:
+ ; NORMAL : 0h --> NOP ; OVER 0
+ ; XOR : AEh --> XOR (HL) ; OVER 1
+ ; OR : B6h --> OR (HL) ; PUTSPRITE
+ ; AND : A6h --> AND (HL) ; PUTMASK
+ nop ; Set to one of the values above
+
+INVERSE_MODE: ; 00 -> NOP -> INVERSE 0
+ nop ; 2F -> CPL -> INVERSE 1
+
+ ld (hl), a
+
+ inc de
+ inc h ; Next line
+ djnz __PRCHAR
+
+ pop hl
+ inc hl
+ ld (DFCC), hl
+
+ ld hl, (DFCCL) ; current ATTR Pos
+ inc hl
+ ld (DFCCL), hl
+ dec hl
+ call __SET_ATTR
+ exx
+ ret
+
+; ------------- SPECIAL CHARS (< 32) -----------------
+
+__PRINT_SPECIAL: ; Jumps here if it is a special char
+ ld hl, __PRINT_TABLE
+ jp JUMP_HL_PLUS_2A
+
+PRINT_EOL: ; Called WHENEVER there is no ";" at end of PRINT sentence
+ exx
+
+__PRINT_0Dh: ; Called WHEN printing CHR$(13)
+ ld hl, (S_POSN)
+ dec l
+ jr nz, 1f
+ dec h
+ jr nz, 1f
+#ifndef __ZXB_DISABLE_SCROLL
+ inc h
+ push hl
+ call __SCROLL_SCR
+ pop hl
+#else
+ ld h, SCR_ROWS - 1
+#endif
+1:
+ ld l, 1
+
+__PRINT_EOL_END:
+ call SET_SCR_ADDR
+ exx
+ ret
+
+__PRINT_COM:
+ exx
+ push hl
+ push de
+ push bc
+ call PRINT_COMMA
+ pop bc
+ pop de
+ pop hl
+ ret
+
+__PRINT_TAB:
+ ld hl, __PRINT_TAB1
+ jr __PRINT_SET_STATE
+
+__PRINT_TAB1:
+ ld (MEM0), a
+ ld hl, __PRINT_TAB2
+ jr __PRINT_SET_STATE
+
+__PRINT_TAB2:
+ ld a, (MEM0) ; Load tab code (ignore the current one)
+ ld hl, __PRINT_START
+ ld (PRINT_JUMP_STATE), hl
+ exx
+ push hl
+ push bc
+ push de
+ call PRINT_TAB
+ pop de
+ pop bc
+ pop hl
+ ret
+
+__PRINT_AT:
+ ld hl, __PRINT_AT1
+ jr __PRINT_SET_STATE
+
+__PRINT_NOP:
+__PRINT_RESTART:
+ ld hl, __PRINT_START
+
+__PRINT_SET_STATE:
+ ld (PRINT_JUMP_STATE), hl ; Saves next entry call
+ exx
+ ret
+
+__PRINT_AT1: ; Jumps here if waiting for 1st parameter
+ ld hl, (S_POSN)
+ ld h, a
+ ld a, SCR_ROWS
+ sub h
+ ld (S_POSN + 1), a
+
+ ld hl, __PRINT_AT2
+ jr __PRINT_SET_STATE
+
+__PRINT_AT2:
+ call __LOAD_S_POSN
+ ld e, a
+ call __SAVE_S_POSN
+ jp __PRINT_RESTART
+
+__PRINT_DEL:
+ call __LOAD_S_POSN ; Gets current screen position
+ dec e
+ ld a, -1
+ cp e
+ jr nz, 3f
+ ld e, SCR_COLS - 2
+ dec d
+ cp d
+ jr nz, 3f
+ ld d, SCR_ROWS - 1
+3:
+ call __SAVE_S_POSN
+ exx
+ ret
+
+__PRINT_INK:
+ ld hl, __PRINT_INK2
+ jr __PRINT_SET_STATE
+
+__PRINT_INK2:
+ call INK_TMP
+ jp __PRINT_RESTART
+
+__PRINT_PAP:
+ ld hl, __PRINT_PAP2
+ jr __PRINT_SET_STATE
+
+__PRINT_PAP2:
+ call PAPER_TMP
+ jp __PRINT_RESTART
+
+__PRINT_FLA:
+ ld hl, __PRINT_FLA2
+ jr __PRINT_SET_STATE
+
+__PRINT_FLA2:
+ call FLASH_TMP
+ jp __PRINT_RESTART
+
+__PRINT_BRI:
+ ld hl, __PRINT_BRI2
+ jr __PRINT_SET_STATE
+
+__PRINT_BRI2:
+ call BRIGHT_TMP
+ jp __PRINT_RESTART
+
+__PRINT_INV:
+ ld hl, __PRINT_INV2
+ jr __PRINT_SET_STATE
+
+__PRINT_INV2:
+ call INVERSE_TMP
+ jp __PRINT_RESTART
+
+__PRINT_OVR:
+ ld hl, __PRINT_OVR2
+ jr __PRINT_SET_STATE
+
+__PRINT_OVR2:
+ call OVER_TMP
+ jp __PRINT_RESTART
+
+#ifndef __ZXB_DISABLE_BOLD
+__PRINT_BOLD:
+ ld hl, __PRINT_BOLD2
+ jp __PRINT_SET_STATE
+
+__PRINT_BOLD2:
+ call BOLD_TMP
+ jp __PRINT_RESTART
+#endif
+
+#ifndef __ZXB_DISABLE_ITALIC
+__PRINT_ITA:
+ ld hl, __PRINT_ITA2
+ jp __PRINT_SET_STATE
+
+__PRINT_ITA2:
+ call ITALIC_TMP
+ jp __PRINT_RESTART
+#endif
+
+#ifndef __ZXB_DISABLE_BOLD
+ LOCAL __BOLD
+
+__BOLD:
+ push hl
+ ld hl, MEM0
+ ld b, 8
+1:
+ ld a, (de)
+ ld c, a
+ rlca
+ or c
+ ld (hl), a
+ inc hl
+ inc de
+ djnz 1b
+ pop hl
+ ld de, MEM0
+ ret
+#endif
+
+#ifndef __ZXB_DISABLE_ITALIC
+ LOCAL __ITALIC
+
+__ITALIC:
+ push hl
+ ld hl, MEM0
+ ex de, hl
+ ld bc, 8
+ ldir
+ ld hl, MEM0
+ srl (hl)
+ inc hl
+ srl (hl)
+ inc hl
+ srl (hl)
+ inc hl
+ inc hl
+ inc hl
+ sla (hl)
+ inc hl
+ sla (hl)
+ inc hl
+ sla (hl)
+ pop hl
+ ld de, MEM0
+ ret
+#endif
+
+#ifndef __ZXB_DISABLE_SCROLL
+ LOCAL __SCROLL_SCR
+
+; En zx81sd el scroll se hace SIEMPRE con la implementacion por buffer:
+; la variante del zx48k ("__SCROLL_SCR EQU 0DFEh") delega en la rutina
+; CL-SC-ALL de la ROM del Spectrum, que aqui no esta mapeada — ese CALL
+; ejecutaria codigo arbitrario del programa (bug cazado con flights.bas:
+; el primer PRINT que desbordaba la pantalla saltaba a la linea BASIC
+; que casualmente ocupara $0DFE).
+__SCROLL_SCR: ;; Scrolls screen and attrs 1 row up
+ ld de, (SCREEN_ADDR)
+ ld b, 3
+3:
+ push bc
+ ld a, 8
+1:
+ ld hl, 32
+ add hl, de
+ ld bc, 32 * 7
+ push de
+ ldir
+ pop de
+ inc d
+ dec a
+ jr nz, 1b
+ push hl
+ ld bc, -32 - 256 * 7
+ add hl, bc
+ ex de, hl
+ ld a, 8
+2:
+ ld bc, 32
+ push hl
+ push de
+ ldir
+ pop de
+ pop hl
+ inc d
+ inc h
+ dec a
+ jr nz, 2b
+ pop de
+ pop bc
+ djnz 3b
+
+ dec de
+ ld h, d
+ ld l, e
+ ld a, 8
+3:
+ push hl
+ push de
+ ld (hl), b
+ dec de
+ ld bc, 31
+ lddr
+ pop de
+ pop hl
+ dec d
+ dec h
+ dec a
+ jr nz, 3b
+
+ ld de, (SCREEN_ATTR_ADDR)
+ ld hl, 32
+ add hl, de
+ ld bc, 32 * 23
+ ldir
+
+ ld h, d
+ ld l, e
+ ld a, (ATTR_P)
+ ld (hl), a
+ inc de
+ ld bc, 31
+ ldir
+ ret
+#endif
+
+
+PRINT_COMMA:
+ call __LOAD_S_POSN
+ ld a, e
+ and 16
+ add a, 16
+
+PRINT_TAB:
+ ; Tabulates the number of spaces in A register
+ ; If the current cursor position is already A, does nothing
+ PROC
+ LOCAL LOOP
+
+ call __LOAD_S_POSN ; e = current row
+ sub e
+ and 31
+ ret z
+
+ ld b, a
+LOOP:
+ ld a, ' '
+ call __PRINTCHAR
+ djnz LOOP
+ ret
+ ENDP
+
+PRINT_AT: ; Changes cursor to ROW, COL
+ ; COL in A register
+ ; ROW in stack
+
+ pop hl ; Ret address
+ ex (sp), hl ; callee H = ROW
+ ld l, a
+ ex de, hl
+
+ call __IN_SCREEN
+ ret nc ; Return if out of screen
+ jp __SAVE_S_POSN
+
+ LOCAL __PRINT_COM
+ LOCAL __PRINT_AT1
+ LOCAL __PRINT_AT2
+ LOCAL __PRINT_BOLD
+ LOCAL __PRINT_ITA
+ LOCAL __PRINT_INK
+ LOCAL __PRINT_PAP
+ LOCAL __PRINT_SET_STATE
+ LOCAL __PRINT_TABLE
+ LOCAL __PRINT_TAB, __PRINT_TAB1, __PRINT_TAB2
+
+#ifndef __ZXB_DISABLE_ITALIC
+ LOCAL __PRINT_ITA2
+#else
+ __PRINT_ITA EQU __PRINT_NOP
+#endif
+
+#ifndef __ZXB_DISABLE_BOLD
+ LOCAL __PRINT_BOLD2
+#else
+ __PRINT_BOLD EQU __PRINT_NOP
+#endif
+
+__PRINT_TABLE: ; Jump table for 0 .. 22 codes
+
+ DW __PRINT_NOP ; 0
+ DW __PRINT_NOP ; 1
+ DW __PRINT_NOP ; 2
+ DW __PRINT_NOP ; 3
+ DW __PRINT_NOP ; 4
+ DW __PRINT_NOP ; 5
+ DW __PRINT_COM ; 6 COMMA
+ DW __PRINT_NOP ; 7
+ DW __PRINT_DEL ; 8 DEL
+ DW __PRINT_NOP ; 9
+ DW __PRINT_NOP ; 10
+ DW __PRINT_NOP ; 11
+ DW __PRINT_NOP ; 12
+ DW __PRINT_0Dh ; 13
+ DW __PRINT_BOLD ; 14
+ DW __PRINT_ITA ; 15
+ DW __PRINT_INK ; 16
+ DW __PRINT_PAP ; 17
+ DW __PRINT_FLA ; 18
+ DW __PRINT_BRI ; 19
+ DW __PRINT_INV ; 20
+ DW __PRINT_OVR ; 21
+ DW __PRINT_AT ; 22 AT
+ DW __PRINT_TAB ; 23 TAB
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/printf.asm b/src/lib/arch/zx81sd/runtime/printf.asm
new file mode 100644
index 000000000..5c88cbb48
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/printf.asm
@@ -0,0 +1,37 @@
+; printf.asm (zx81sd) — PRINT de un numero FLOAT
+;
+; Sustituye a zx48k/runtime/printf.asm, que usa el literal 'str$' ($2Eh) del
+; calculador de la ROM (STR$ + STK-STO-$ + heap temporal) para despues
+; imprimir la cadena resultante. Aqui se usa directamente fp_tostr.asm
+; (conversion simplificada ya portada) e imprime sus caracteres uno a uno,
+; sin pasar por el heap.
+
+#include once
+#include once
+
+ push namespace core
+
+__PRINTF:
+ ; Entrada: A,E,D,C,B = valor FLOAT
+ call FP_TO_STR ; HL = puntero al texto, BC = longitud
+ PROC
+ LOCAL __PRINTF_LOOP
+
+__PRINTF_LOOP:
+ ld a, b
+ or c
+ ret z
+
+ ld a, (hl)
+ push hl
+ push bc
+ call __PRINTCHAR
+ pop bc
+ pop hl
+ inc hl
+ dec bc
+ jr __PRINTF_LOOP
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/random.asm b/src/lib/arch/zx81sd/runtime/random.asm
new file mode 100644
index 000000000..0aa7e620a
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/random.asm
@@ -0,0 +1,102 @@
+; RANDOM functions — ZX81 + SD81 Booster
+; Adaptación de zx48k/runtime/random.asm:
+; - FRAMES usa el contador VSYNC software en SYSVAR_BASE+$19
+; - RANDOM_SEED_LOW en SYSVAR_BASE+$1B (en vez de sysvar Spectrum 23670)
+; - RANDOM_SEED_HIGH sigue siendo RAND+1 (operando inline de ld de,imm16)
+; - Sin cambios en el algoritmo RNG (xorshift)
+
+#include once
+
+ push namespace core
+
+RANDOMIZE:
+ ; Randomize with 32 bit seed in DE HL
+ ; if SEED = 0, uses FRAMES counter as seed
+ PROC
+
+ LOCAL TAKE_FRAMES
+
+ ld a, h
+ or l
+ or d
+ or e
+ jr z, TAKE_FRAMES
+
+ ld (RANDOM_SEED_LOW), hl
+ ld (RANDOM_SEED_HIGH), de
+ ret
+
+TAKE_FRAMES:
+ ; Toma la semilla del contador VSYNC (sustituye al contador de frames de la ROM)
+ ld hl, (FRAMES)
+ ld (RANDOM_SEED_LOW), hl
+ ld hl, 0
+ ld (RANDOM_SEED_HIGH), hl
+ ret
+
+ ENDP
+
+RANDOM_SEED_HIGH EQU RAND + 1 ; Operando inline de ld de,imm16 (self-modifying seed)
+
+RAND:
+ PROC
+ ld de, 0C0DEh ; yw → zt (DE = semilla alta, se sobreescribe cada vez)
+ ld hl, (RANDOM_SEED_LOW) ; xz → yw
+ ld (RANDOM_SEED_LOW), de ; x = y, z = w
+ ld a, e ; w = w ^ (w << 3)
+ add a, a
+ add a, a
+ add a, a
+ xor e
+ ld e, a
+ ld a, h ; t = x ^ (x << 1)
+ add a, a
+ xor h
+ ld d, a
+ rra ; t = t ^ (t >> 1) ^ w
+ xor d
+ xor e
+ ld d, l ; y = z
+ ld e, a ; w = t
+ ld (RANDOM_SEED_HIGH), de
+ ret
+ ENDP
+
+RND:
+ ; Returns a FLOATING point integer using RAND as mantissa
+ PROC
+ LOCAL RND_LOOP
+
+ call RAND
+ ld b, h
+ ld c, l
+
+ ld a, e
+ or d
+ or c
+ or b
+ ret z
+
+ ld l, 81h
+ ld a, e
+RND_LOOP:
+ dec l
+ sla b
+ rl c
+ rl d
+ rla
+ jp nc, RND_LOOP
+
+ ccf
+ rra
+ rr d
+ rr c
+ rr b
+
+ ld e, a
+ ld a, l
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/save.asm b/src/lib/arch/zx81sd/runtime/save.asm
new file mode 100644
index 000000000..0b9457960
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/save.asm
@@ -0,0 +1,100 @@
+; save.asm (zx81sd) — SAVE ... CODE a la tarjeta SD
+;
+; Sustituye a zx48k/runtime/save.asm (que usa SA-BYTES de cinta de la
+; ROM Spectrum). El statement SAVE de zxbasic guarda el bloque en la SD
+; del SD81 Booster con el comando 10 del MCU (ver io/sd81_mcu.asm).
+;
+; Interfaz del compilador (identica a zx48k):
+; pila: BC = longitud; DE = direccion origen;
+; HL = string de zxbasic con el nombre (hay que liberarlo).
+;
+; Errores ("blandos", como la version de cinta): nombre nulo/vacio ->
+; ERROR_InvalidFileName; longitud 0 -> ERROR_InvalidArg; fallo del MCU
+; (SD llena, etc.) -> ERROR_TapeLoadingErr. En todos se retorna con
+; ERR_NR puesto.
+
+#include once
+#include once
+#include once
+
+ push namespace core
+
+SAVE_CODE:
+ PROC
+ LOCAL SC_LEN, SC_SRC
+ LOCAL SC_HAVE_NAME, SC_ERR_NAME_NOFREE, SC_DO_SAVE
+
+ pop hl ; direccion de retorno
+ pop bc ; longitud
+ pop de ; direccion origen
+ ex (sp), hl ; CALLEE: HL = string con el nombre
+
+ ld (SC_LEN), bc
+ ld (SC_SRC), de
+
+ ld a, h
+ or l
+ jp z, SC_ERR_NAME_NOFREE ; string nulo
+
+ ; longitud del bloque 0 -> error (liberando el string)
+ ld a, b
+ or c
+ jr nz, SC_HAVE_NAME
+ call MEM_FREE
+ ld a, ERROR_InvalidArg
+ ld (ERR_NR), a
+ ret
+
+SC_HAVE_NAME:
+ ; nombre vacio -> error (tras liberar)
+ ld e, (hl)
+ inc hl
+ ld d, (hl)
+ dec hl
+ ld a, d
+ or e
+ jr nz, SC_DO_SAVE
+
+ call MEM_FREE
+ jp SC_ERR_NAME_NOFREE
+
+SC_DO_SAVE:
+ ; comando 10 (SAVE) + nombre + longitud + datos + status
+ push hl
+ ld a, 10
+ call SD81_MCU_SEND
+ pop hl
+ push hl
+ call SD81_MCU_SEND_NAME
+ pop hl
+ call MEM_FREE
+
+ ld bc, (SC_LEN)
+ ld a, c
+ call SD81_MCU_SEND ; longitud, byte bajo
+ ld a, b
+ call SD81_MCU_SEND ; longitud, byte alto
+
+ ld hl, (SC_SRC)
+ call SD81_MCU_SEND_BLOCK
+
+ call SD81_MCU_RECV ; byte de estado
+ or a
+ ret z
+ ld a, ERROR_TapeLoadingErr
+ ld (ERR_NR), a
+ ret
+
+SC_ERR_NAME_NOFREE:
+ ld a, ERROR_InvalidFileName
+ ld (ERR_NR), a
+ ret
+
+SC_LEN:
+ defw 0
+SC_SRC:
+ defw 0
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/specfont.bin b/src/lib/arch/zx81sd/runtime/specfont.bin
new file mode 100644
index 000000000..458e0a57a
Binary files /dev/null and b/src/lib/arch/zx81sd/runtime/specfont.bin differ
diff --git a/src/lib/arch/zx81sd/runtime/stackf.asm b/src/lib/arch/zx81sd/runtime/stackf.asm
new file mode 100644
index 000000000..9473ddda2
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/stackf.asm
@@ -0,0 +1,97 @@
+; stackf.asm (zx81sd) — Gestión de la pila del calculador FP
+;
+; Sustituye a zx48k/runtime/stackf.asm, que define __FPSTACK_PUSH/POP como
+; direcciones FIJAS de la ROM del Spectrum ($2AB6h STK-STORE, $2BF1h
+; STK-FETCH). En zx81sd esas direcciones son parte de nuestro propio
+; binario compilado (varían de un programa a otro), así que no se pueden
+; usar como constantes — hay que reimplementar ambas rutinas como código
+; reubicable normal, usando fp_calc.asm (mismo formato de pila y de número
+; de 5 bytes que el motor CALCULATE ya portado).
+
+#include once
+
+ push namespace core
+
+; ---------------------------------------------------------------------------
+; __FPSTACK_PUSH — Apila los registros A,E,D,C,B (5 bytes) en la pila FP
+; Sustituye a STK-STORE ($2AB6h ROM Spectrum)
+; ---------------------------------------------------------------------------
+__FPSTACK_PUSH:
+ push bc
+ push af
+ ld bc, 5
+ call CALC_TEST_ROOM
+ pop af
+ pop bc
+ ld hl, (FP_STKEND)
+ ld (hl), a
+ inc hl
+ ld (hl), e
+ inc hl
+ ld (hl), d
+ inc hl
+ ld (hl), c
+ inc hl
+ ld (hl), b
+ inc hl
+ ld (FP_STKEND), hl
+ ret
+
+__FPSTACK_PUSH2: ; Pushes Current A ED CB registers and top of the stack on (SP + 4)
+ ; Second argument to push into the stack calculator is popped out of the stack
+ ; Since the caller routine also receives the parameters into the top of the stack
+ ; four bytes must be removed from SP before pop them out
+
+ call __FPSTACK_PUSH ; Pushes A ED CB into the FP-STACK
+ exx
+ pop hl ; Caller-Caller return addr
+ exx
+ pop hl ; Caller return addr
+
+ pop af
+ pop de
+ pop bc
+
+ push hl ; Caller return addr
+ exx
+ push hl ; Caller-Caller return addr
+ exx
+
+ jp __FPSTACK_PUSH
+
+
+__FPSTACK_I16: ; Pushes 16 bits integer in HL into the FP ROM STACK
+ ; This format is specified in the ZX 48K Manual
+ ; You can push a 16 bit signed integer as
+ ; 0 SS LL HH 0, being SS the sign and LL HH the low
+ ; and High byte respectively
+ ld a, h
+ rla ; sign to Carry
+ sbc a, a ; 0 if positive, FF if negative
+ ld e, a
+ ld d, l
+ ld c, h
+ xor a
+ ld b, a
+ jp __FPSTACK_PUSH
+
+; ---------------------------------------------------------------------------
+; __FPSTACK_POP — Extrae los últimos 5 bytes de la pila FP a A,E,D,C,B
+; Sustituye a STK-FETCH ($2BF1h ROM Spectrum)
+; ---------------------------------------------------------------------------
+__FPSTACK_POP:
+ ld hl, (FP_STKEND)
+ dec hl
+ ld b, (hl)
+ dec hl
+ ld c, (hl)
+ dec hl
+ ld d, (hl)
+ dec hl
+ ld e, (hl)
+ dec hl
+ ld a, (hl)
+ ld (FP_STKEND), hl
+ ret
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/str.asm b/src/lib/arch/zx81sd/runtime/str.asm
new file mode 100644
index 000000000..e603f8dd5
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/str.asm
@@ -0,0 +1,54 @@
+; str.asm (zx81sd) — La funcion STR$( )
+;
+; Sustituye a zx48k/runtime/str.asm, que usa el literal 'str$' ($2Eh) del
+; calculador de la ROM junto con STK-STO-$ y RECLAIM2 (ROM $19E8h) para
+; construir la cadena en el area de trabajo de la ROM. Aqui se usa
+; directamente fp_tostr.asm (conversion simplificada ya portada) y se copia
+; el resultado a un bloque nuevo del heap propio (mem/alloc.asm).
+
+#include once
+#include once
+
+ push namespace core
+
+__STR:
+__STR_FAST:
+ ; Entrada: A,E,D,C,B = valor FLOAT
+ ; Salida: HL = puntero a la cadena (heap), formato [longitud(2B)][texto]
+ call FP_TO_STR ; HL = puntero al texto, BC = longitud
+ PROC
+ LOCAL __STR_END
+
+ push hl ; guarda puntero al texto (FP_STR_BUF)
+ push bc ; guarda longitud
+
+ ld hl, 2
+ add hl, bc
+ ld b, h
+ ld c, l
+ call __MEM_ALLOC ; HL = nuevo bloque de (longitud+2) bytes (o NULL)
+
+ pop bc ; longitud del texto
+ pop de ; puntero al texto (FP_STR_BUF)
+
+ ld a, h
+ or l
+ jr z, __STR_END ; sin memoria -> devuelve NULL
+
+ push hl
+ ld (hl), c
+ inc hl
+ ld (hl), b
+ inc hl ; HL = destino del texto
+
+ ex de, hl ; HL = origen (texto), DE = destino
+ ldir
+
+ pop hl ; HL = puntero a la cadena resultante
+
+__STR_END:
+ ret
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/sysvars.asm b/src/lib/arch/zx81sd/runtime/sysvars.asm
new file mode 100644
index 000000000..3cfeb609e
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/sysvars.asm
@@ -0,0 +1,108 @@
+; -----------------------------------------------------------------------
+; ZX81 + SD81 Booster System Variables
+;
+; SCREEN_ADDR / SCREEN_ATTR_ADDR: bloque 6 ($C000), pantalla Spectrum
+; emulada por la FPGA del SD81 Booster en modo Superfast HiRes Spectrum.
+;
+; Las variables dinámicas del runtime se sitúan en $8000+ (bloques 4-5),
+; fuera de la zona de código ejecutable ($0000-$7FFF), para no partir
+; el espacio de ejecución del usuario.
+; -----------------------------------------------------------------------
+
+; Estos ficheros se incluyen siempre a través de sysvars.asm (primer fichero
+; en incluirse en cualquier programa zx81sd) para que sus #init se registren
+; en la primera pasada del preprocesador, antes de que emit_prologue() genere
+; las llamadas CALL a las rutinas de inicialización.
+#include once
+
+ push namespace core
+
+; --- Variables dinámicas del runtime ($8000+) ---------------------------
+; Zona de datos (bloques 4-5), no ejecutable sin MC45.
+;
+; SCREEN_ADDR y SCREEN_ATTR_ADDR son variables RAM (no constantes EQU)
+; porque el runtime de zx48k las lee con direccionamiento indirecto:
+; LD HL, (SCREEN_ADDR) → carga el CONTENIDO de esa posición de memoria.
+; SD81_INIT_SYSVARS las inicializa con $C000 y $D800 respectivamente.
+
+SYSVAR_BASE EQU $8000
+
+CHARS EQU SYSVAR_BASE + $00 ; DW — puntero a charset (8x8)
+UDG EQU SYSVAR_BASE + $02 ; DW — puntero a UDGs
+COORDS EQU SYSVAR_BASE + $04 ; DW — última coordenada PLOT (X,Y)
+FLAGS2 EQU SYSVAR_BASE + $06 ; DB — flags de pantalla (OVER/INVERSE/etc.)
+ECHO_E EQU SYSVAR_BASE + $07 ; DB — (reservado)
+DFCC EQU SYSVAR_BASE + $08 ; DW — siguiente dirección bitmap para PRINT
+DFCCL EQU SYSVAR_BASE + $0A ; DW — siguiente dirección attrs para PRINT
+S_POSN EQU SYSVAR_BASE + $0C ; DW — posición cursor (H=fila, L=columna)
+ATTR_P EQU SYSVAR_BASE + $0E ; DB — atributo permanente (INK/PAPER/etc.)
+MASK_P EQU SYSVAR_BASE + $0F ; DB — máscara permanente ($00 = sin transparencia)
+ATTR_T EQU SYSVAR_BASE + $10 ; DB — atributo temporal
+; MASK_T se accede implícitamente como ATTR_T+1 ($8011) via LD HL,(ATTR_T)
+P_FLAG EQU SYSVAR_BASE + $12 ; DB — flags de impresión (OVER/INVERSE perm.)
+MEM0 EQU SYSVAR_BASE + $13 ; 5B — buffer temporal para rutinas gráficas
+TV_FLAG EQU SYSVAR_BASE + $18 ; DB — flags de control de salida a pantalla
+ERR_NR EQU SYSVAR_BASE + $19 ; DB — código de error (-1 = sin error)
+FRAMES EQU SYSVAR_BASE + $1A ; DW — contador de frames VSYNC (software)
+RANDOM_SEED_LOW EQU SYSVAR_BASE + $1C ; DW — semilla RNG (16 bits bajos)
+SCREEN_ADDR EQU SYSVAR_BASE + $1E ; DW — puntero al framebuffer (init: $C000)
+SCREEN_ATTR_ADDR EQU SYSVAR_BASE + $20 ; DW — puntero a atributos (init: $D800)
+
+; --- Sysvars del calculador de coma flotante (fp_calc.asm) --------------
+; Equivalentes a STKBOT/STKEND/BREG/MEM de la ROM Spectrum ($5C63/$5C65/
+; $5C67/$5C68), pero apuntando a un buffer fijo propio en vez de al área
+; de trabajo dinámica de la ROM (aquí no existe "memoria libre creciente"
+; entre el programa y la pila de máquina).
+;
+; IMPORTANTE: FP_BREG debe estar INMEDIATAMENTE DESPUÉS de FP_STKEND — el
+; motor CALCULATE (L338E, ENT-TABLE) explota la contigüidad de memoria de
+; la ROM original (STKEND_hi seguido de BREG) para cargar ambos con un
+; único LD BC,(FP_STKEND+1): C=STKEND_hi, B=BREG. No reordenar.
+FP_STKBOT EQU SYSVAR_BASE + $22 ; DW — base de la pila de números FP
+FP_STKEND EQU SYSVAR_BASE + $24 ; DW — siguiente posición libre en la pila FP
+FP_BREG EQU SYSVAR_BASE + $26 ; DB — literal en curso (para fp-calc-2/dec-jr-nz)
+FP_MEM EQU SYSVAR_BASE + $27 ; DW — puntero al área MEM (6 celdas de 5B)
+FP_CALC_STACK EQU SYSVAR_BASE + $29 ; 60B — pila de números FP (12 números máx.)
+FP_CALC_STACK_END EQU FP_CALC_STACK + 60
+FP_MEM_AREA EQU SYSVAR_BASE + $65 ; 30B — área MEM (6 celdas de 5 bytes)
+
+; --- Scratch del indexador de arrays (runtime/array/array.asm) ---------
+; El array.asm compartido de zx48k usa la sysvar MEMBOT de la ROM Spectrum
+; (dirección fija 23698 = $5C92) como almacenamiento temporal para sus
+; punteros (LBOUND_PTR/UBOUND_PTR/RET_ADDR/TMP_ARR_PTR, 2 bytes cada uno).
+; En zx81sd esa dirección cae dentro del propio código compilado del
+; programa (bloque 2, $4000-$5FFF): cualquier array multidimensional lo
+; corrompía en cada acceso. El override zx81sd de array.asm usa esta
+; zona en su lugar.
+ARRAY_SCRATCH EQU SYSVAR_BASE + $83 ; 8B — LBOUND/UBOUND/RET/TMP_ARR_PTR
+
+; --- Scratch de CHR$() (runtime/chr.asm) --------------------------------
+; El chr.asm compartido de zx48k usa la sysvar DEST de la ROM Spectrum
+; (dirección fija 23629 = $5C4D) para guardar temporalmente la dirección
+; de retorno mientras reserva memoria para la cadena. Esa dirección cae
+; dentro del código compilado en zx81sd (bloque 2, en concreto dentro de
+; __DIVU32). El override zx81sd de chr.asm usa esta zona en su lugar.
+CHR_SCRATCH EQU SYSVAR_BASE + $8B ; 2B — dirección de retorno
+
+; --- Scratch de la división FP (runtime/arith/divf.asm) -----------------
+; El divf.asm compartido de zx48k usa DEST (23629, igual que chr.asm) y
+; ERR_SP (23613) de la ROM Spectrum para guardar/restaurar un punto de
+; recuperación de pila alrededor de la división en coma flotante (truco
+; de "longjmp" para división por cero). En zx81sd ese mecanismo de
+; recuperación no llega a usarse de verdad (__ERROR hace DI+HALT
+; directamente, no restaura SP desde ERR_SP), pero el código escribe ahí
+; en TODAS las divisiones igualmente, no solo cuando hay error — y esas
+; direcciones caen dentro del código compilado (bloque 2). El override
+; zx81sd usa esta zona en su lugar (mismo mecanismo, solo cambia dónde
+; vive el scratch).
+DIVF_SCRATCH EQU SYSVAR_BASE + $8D ; 4B — TMP (2B) + ERR_SP (2B)
+
+; Tamaño total del bloque de sysvars: $91 bytes
+
+; --- Constantes de pantalla ---------------------------------------------
+
+SCR_COLS EQU 33 ; Columnas + 1 (32 columnas visibles)
+SCR_ROWS EQU 24 ; Filas (24 filas visibles)
+SCR_SIZE EQU (SCR_ROWS << 8) + SCR_COLS
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/val.asm b/src/lib/arch/zx81sd/runtime/val.asm
new file mode 100644
index 000000000..df642dbb0
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/val.asm
@@ -0,0 +1,210 @@
+; val.asm (zx81sd) — VAL(a$): convierte texto a numero en coma flotante
+;
+; Sustituye a zx48k/runtime/val.asm, que usa VAL de la ROM del Spectrum:
+; ademas de convertir el texto a numero, la ROM real vuelve a meter la
+; cadena en el interprete de BASIC y la evalua como una expresion completa
+; (por eso en un Spectrum real VAL("2+2") funciona y da 4). Esa parte vive
+; en el escaner de lineas de BASIC de la ROM, un subsistema aparte del
+; calculador (no portado aqui).
+;
+; Esta version soporta solo un LITERAL DECIMAL simple: signo opcional,
+; digitos, y un punto decimal opcional seguido de mas digitos. NO evalua
+; expresiones (VAL("2+2") no funciona; VAL("2.5") o VAL("-13") si). Es el
+; uso habitual de VAL(INPUT(...)) para leer numeros tecleados por el
+; usuario. Cualquier caracter no numerico corta el parseo en ese punto
+; (el resto de la cadena se ignora), en vez de dar un error.
+;
+; El numero se construye acumulando digito a digito con el propio
+; calculador ya portado (fp_calc.asm): valor = valor*10 + digito, y al
+; final se divide entre 10^(numero de decimales) si hubo parte fraccionaria.
+
+#include once
+#include once
+
+ push namespace core
+
+; --- Variables de trabajo (no reentrante, uso transitorio durante VAL) -----
+VAL_PTR: defw 0 ; puntero al siguiente caracter a leer
+VAL_LEN: defw 0 ; caracteres restantes por leer
+VAL_STRPTR: defw 0 ; puntero original a la cadena (para liberarla)
+VAL_FREE_FLAG: defb 0 ; 1 si hay que liberar la cadena al terminar
+VAL_NEG: defb 0 ; 1 si el numero es negativo
+VAL_INFRAC: defb 0 ; 1 si ya se paso el punto decimal
+VAL_DECIMALS: defb 0 ; numero de digitos leidos tras el punto decimal
+
+VAL:
+ ; Entrada: HL = direccion de a$ (2 bytes de longitud + datos)
+ ; A = 1 si hay que liberar a$ al terminar (no es variable)
+ ; Salida: A EDCB = numero en punto flotante (via __FPSTACK_POP)
+ PROC
+
+ LOCAL VAL_EMPTY
+ LOCAL VAL_LOOP
+ LOCAL VAL_GOT_SIGN
+ LOCAL VAL_NOT_DOT
+ LOCAL VAL_DIGIT
+ LOCAL VAL_DIGIT_ADVANCE
+ LOCAL VAL_DONE
+ LOCAL VAL_DIV_LOOP
+ LOCAL VAL_NOT_NEG
+ LOCAL VAL_EMPTY_SKIP
+ LOCAL VAL_NO_FREE
+ LOCAL PUSH_DIGIT
+
+ ld (VAL_FREE_FLAG), a
+ ld a, h
+ or l
+ jp z, VAL_EMPTY ; cadena NULL -> 0 (jp: VAL_EMPTY queda lejos)
+
+ ld (VAL_STRPTR), hl
+
+ ld e, (hl)
+ inc hl
+ ld d, (hl)
+ inc hl ; DE = longitud de la cadena
+ ld (VAL_LEN), de
+ ld (VAL_PTR), hl ; HL = inicio del texto
+
+ xor a
+ ld (VAL_NEG), a
+ ld (VAL_INFRAC), a
+ ld (VAL_DECIMALS), a
+
+ ld hl, (VAL_LEN)
+ ld a, h
+ or l
+ jp z, VAL_DONE ; cadena vacia -> 0
+
+ ld hl, (VAL_PTR)
+ ld a, (hl)
+ cp '-'
+ jr nz, VAL_GOT_SIGN
+ ld a, 1
+ ld (VAL_NEG), a
+ inc hl
+ ld (VAL_PTR), hl
+ ld de, (VAL_LEN)
+ dec de
+ ld (VAL_LEN), de
+VAL_GOT_SIGN:
+
+ ; acumulador FP = 0
+ xor a
+ ld e, a
+ ld d, a
+ ld c, a
+ ld b, a
+ call __FPSTACK_PUSH
+
+VAL_LOOP:
+ ld hl, (VAL_LEN)
+ ld a, h
+ or l
+ jp z, VAL_DONE
+
+ ld hl, (VAL_PTR)
+ ld a, (hl)
+
+ cp '.'
+ jr nz, VAL_NOT_DOT
+ ld a, 1
+ ld (VAL_INFRAC), a
+ jr VAL_DIGIT_ADVANCE ; el punto no cuenta como digito, solo avanza
+
+VAL_NOT_DOT:
+ cp '0'
+ jp c, VAL_DONE
+ cp '9' + 1
+ jp nc, VAL_DONE
+
+VAL_DIGIT:
+ sub '0' ; A = digito 0-9
+ push af
+
+ ld a, 10
+ call PUSH_DIGIT
+ rst 28h
+ defb $04 ;;multiply
+ defb $38 ;;end-calc
+
+ pop af
+ call PUSH_DIGIT
+ rst 28h
+ defb $0F ;;addition
+ defb $38 ;;end-calc
+
+ ld a, (VAL_INFRAC)
+ or a
+ jr z, VAL_DIGIT_ADVANCE
+ ld hl, VAL_DECIMALS
+ inc (hl)
+
+VAL_DIGIT_ADVANCE:
+ ld hl, (VAL_PTR)
+ inc hl
+ ld (VAL_PTR), hl
+ ld hl, (VAL_LEN)
+ dec hl
+ ld (VAL_LEN), hl
+ jp VAL_LOOP
+
+VAL_DONE:
+ ld a, (VAL_DECIMALS)
+ or a
+ jr z, VAL_NOT_NEG
+ ld b, a
+VAL_DIV_LOOP:
+ push bc
+ ld a, 10
+ call PUSH_DIGIT
+ rst 28h
+ defb $05 ;;division
+ defb $38 ;;end-calc
+ pop bc
+ djnz VAL_DIV_LOOP
+
+VAL_NOT_NEG:
+ ld a, (VAL_NEG)
+ or a
+ jr z, VAL_EMPTY_SKIP
+ rst 28h
+ defb $1B ;;negate
+ defb $38 ;;end-calc
+VAL_EMPTY_SKIP:
+
+ call __FPSTACK_POP ; A EDCB = resultado
+
+ push af
+ push de
+ push bc
+ ld a, (VAL_FREE_FLAG)
+ or a
+ jr z, VAL_NO_FREE
+ ld hl, (VAL_STRPTR)
+ call __MEM_FREE
+VAL_NO_FREE:
+ pop bc
+ pop de
+ pop af
+ ret
+
+VAL_EMPTY:
+ xor a
+ ld e, a
+ ld d, a
+ ld c, a
+ ld b, a
+ jp __FPSTACK_POP
+
+; --- Apila A (0-255) como entero pequeño positivo ---------------------------
+PUSH_DIGIT:
+ ld d, a
+ xor a
+ ld e, a
+ ld c, a
+ ld b, a
+ jp __FPSTACK_PUSH
+
+ ENDP
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/runtime/vsync.asm b/src/lib/arch/zx81sd/runtime/vsync.asm
new file mode 100644
index 000000000..7cb85a3e9
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/vsync.asm
@@ -0,0 +1,42 @@
+; -----------------------------------------------------------------------
+; VSYNC — Sincronización con el refresco de pantalla
+; SD81 Booster: puerto $AFh
+; bit 0 = estado actual de VSYNC (0/1, informativo, no se usa aquí)
+; bits 6-1 = contador de pulsos de VSYNC ocurridos desde la última
+; lectura del puerto (se resetea con cada IN)
+;
+; Sustituye a HALT + interrupción IM1 del ZX Spectrum.
+; Las interrupciones están desactivadas (DI) en toda la ejecución.
+; -----------------------------------------------------------------------
+
+#include once
+
+ push namespace core
+
+SD81_DATA_PORT EQU $AF ; Puerto de datos MCU del SD81 Booster
+
+; VSYNC_WAIT — Espera a que ocurra al menos un pulso de VSYNC
+; Destruye: A
+; No modifica ningún otro registro.
+VSYNC_WAIT:
+ PROC
+ LOCAL WAIT_PULSE
+
+WAIT_PULSE:
+ in a, (SD81_DATA_PORT)
+ and $7E ; bits 6-1 = contador de pulsos
+ jr z, WAIT_PULSE ; si 0, ningún pulso desde la última lectura
+
+ ret
+ ENDP
+
+; VSYNC_TICK — Espera un frame e incrementa el contador FRAMES
+; Destruye: A, HL
+VSYNC_TICK:
+ call VSYNC_WAIT
+ ld hl, (FRAMES)
+ inc hl
+ ld (FRAMES), hl
+ ret
+
+ pop namespace
diff --git a/src/lib/arch/zx81sd/stdlib/cb/maskedsprites.bas b/src/lib/arch/zx81sd/stdlib/cb/maskedsprites.bas
new file mode 100644
index 000000000..6e2feff2c
--- /dev/null
+++ b/src/lib/arch/zx81sd/stdlib/cb/maskedsprites.bas
@@ -0,0 +1,1369 @@
+' ----------------------------------------------------------------
+' This file is released under the MIT License
+'
+' Copyright (C) 2026 Conrado Badenas
+' Ideas taken from
+' https://github.com/boriel-basic/zxbasic/blob/main/src/lib/arch/zx48k/stdlib/memorybank.bas
+' by Juan Segura (a.k.a. Duefectu),
+' https://github.com/oisee/antique-toy/blob/main/chapters/ch16-sprites/draft.md
+' by Alice Vinogradova (a.k.a. oisee), and
+' https://youtu.be/nBHXtI1Y-xU?t=434 and https://youtu.be/-AUmmzDiGlE?t=434
+' by Benjamín (a.k.a. RetrobenSoft)
+'
+' Print Masked (AND+OR) Sprites, version 2026.04.05
+'
+' Version zx81sd (2026-07-04): la version de zx48k asume paginacion de
+' memoria al estilo Spectrum 128K (banco visible en $c000-$ffff via
+' puerto $7FFDh, sysvar BANKM en $5B5C) para el subsistema MSFS (Masked
+' Sprites File System, un almacen de imagenes de sprite en RAM de
+' sobra). Nada de eso existe en zx81sd. Se sustituye por el mapeador de
+' memoria propio (puerto $E7h) sobre el BLOQUE 7 ($E000-$FFFF), que en
+' nuestro mapa de memoria ya esta reservado para "banking de datos
+' (mapas, sprites...)" -- justo este caso de uso.
+'
+' Las funciones de MSFS (RegisterSpriteImageInMSFS, FindFirstUnusedBlock
+' InMSFS, etc.) son agnosticas de banco/direccion: solo llaman a
+' GetBankPreservingRegs/SetBankPreservingINTs y leen/escriben la
+' variable BASIC MaskedSpritesFileSystemStart. Reescribiendo esas DOS
+' primitivas (y el calculo de esa direccion en InitMaskedSpritesFileSystem,
+' mas abajo, que en el original asumia RAM plana Spectrum hasta $FFFF)
+' el resto del fichero se copia sin tocar una sola linea.
+'
+' CheckMemoryPaging() aqui solo influye, en el ejemplo, en si se usa
+' pantalla visible doble (bancos 5/7 del Spectrum) -- zx81sd tiene un
+' unico framebuffer fisico (bloque 6, SCREEN_ADDR fijo en $C000) y esa
+' funcionalidad no esta cubierta aqui (ver nota en SetVisibleScreen mas
+' abajo). Las funciones de MSFS NO consultan CheckMemoryPaging() para
+' decidir si usar el banco -- lo hacen incondicionalmente -- asi que
+' devolver 0 (honesto: no hay doble pantalla visible) no le afecta en
+' nada a que MSFS funcione.
+'
+' RESIDENCIA: desde InitMaskedSpritesFileSystem(), la pagina de MSFS se
+' queda mapeada de forma PERMANENTE en el bloque 7 (SetBankPreservingINTs
+' con valor <> 7 solo anota el numero, no desmapea) -- las rutinas de
+' dibujo del bucle principal acceden a la MSFS sin envolver con
+' Get/SetBank y necesitan verla siempre. Si un programa usa el bloque 7
+' para su propio banking de datos, debe remapear su pagina el mismo y
+' llamar a SetBankPreservingINTs(7) antes de volver a usar MSFS.
+' ----------------------------------------------------------------
+
+#ifndef __CB_MASKEDSPRITES__
+
+REM Avoid recursive / multiple inclusion
+
+#define __CB_MASKEDSPRITES__
+
+#include
+#include
+#include
+
+
+' ----------------------------------------------------------------
+' Banco de MSFS sobre el mapeador de zx81sd (puerto $E7h, bloque 7)
+'
+' MaskedSprites_MSFS_Page es la pagina SD81 dedicada a guardar la MSFS.
+' Debe evitar las paginas 8-13 (codigo/heap, fijas por el bootstrap, ver
+' src/arch/zx81sd/backend/main.py _PAGE_MAP) y 63 (pagina "libre" por
+' convencion del cargador SD81: split_sd81.py deja siempre el bloque 7
+' apuntando a la pagina 63 tras cargar, antes de saltar al programa).
+' Redefinible con #define ANTES de este #include si el programa ya usa
+' la pagina 20 para otra cosa (no hay todavia un asignador global de
+' paginas en el proyecto).
+' ----------------------------------------------------------------
+#ifndef MaskedSprites_MSFS_Page
+ #define MaskedSprites_MSFS_Page 20
+#endif
+
+' Byte de estado puro en ASM (no DIM): un DIM normal se eliminaria por
+' "codigo muerto" al no tener ninguna referencia desde BASIC (solo se
+' toca desde las dos rutinas ASM de abajo).
+ASM
+_ZX81SD_MSFS_Bank:
+ DEFB 0
+END ASM
+
+' ----------------------------------------------------------------
+' Set a RAM bank in addresses $c000-$ffff, update BANKM,
+' and return with INTerrupts preserved (unchanged)
+' Only works on 128K and compatible models.
+' Parameters:
+' Ubyte: bank number 0,1,2,3,4,5,6,7
+' Changes:
+' A, B, C
+' Preserves:
+' D, E, H, L are not used
+'
+' Version zx81sd: "banco" aqui es puramente logico. Con 7 se asegura que
+' la pagina de MSFS este mapeada en el bloque 7; con cualquier otro
+' valor SOLO se anota el numero (la pagina de MSFS se queda RESIDENTE,
+' no se desmapea). Motivo: SaveBackgroundAndDrawSpriteRegisteredInMSFS
+' (el que dibuja en el bucle principal, y el que crea imagenes
+' desplazadas bajo demanda) accede a la MSFS SIN envolver con
+' Get/SetBank -- en el diseno original no lo necesita porque en 128K el
+' banco 7 se queda mapeado en $c000 (SetDrawingScreen7) y en 48K la
+' MSFS esta en RAM plana siempre visible. Una primera version de este
+' override "liberaba" el bloque 7 a la pagina 63 al restaurar tras
+' Init/Register... y el dibujo del bucle principal leia mascaras y
+' graficos de la pagina 63 (basura): sprites como lineas verticales.
+' Si un programa usa el bloque 7 para su propio banking de datos,
+' debe remapear su pagina el mismo y volver a llamar aqui con 7 antes
+' de usar MSFS (documentado tambien en la cabecera del fichero).
+'
+' No hace falta preservar interrupciones: en zx81sd estan
+' permanentemente deshabilitadas (DI todo el tiempo). IMPORTANTE:
+' escrita en ASM a mano (no en BASIC plano) para respetar EXACTAMENTE
+' el contrato de registros de arriba -- RegisterSpriteImageInMSFS y
+' companeros confian en que D,E,H,L sobrevivan a esta llamada (por
+' ejemplo, para no perder spriteImageAddr, que llega en HL); una
+' implementacion en BASIC normal no da esa garantia (usa registros
+' libremente por dentro) y provocaba que todos los sprites se
+' registraran en la misma direccion.
+' ----------------------------------------------------------------
+SUB FASTCALL SetBankPreservingINTs(bankNumber AS UByte)
+ASM
+ PROC
+ LOCAL NO_MAP
+ ld (_ZX81SD_MSFS_Bank), a
+ cp 7
+ jr nz, NO_MAP ; banco logico <> 7: solo anotar, la
+ ; pagina de MSFS se queda residente
+ push de
+ push hl
+ ld d, MaskedSprites_MSFS_Page
+ ld e, 0
+ push de ; parametro 'page' de Map (byte alto = D)
+ ld a, 7 ; parametro 'block' de Map (bloque 7, fijo)
+ call _Map
+ pop hl
+ pop de
+NO_MAP:
+ ENDP
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' Get which RAM bank is set in addresses $c000-$ffff
+' Only works on 128K and compatible models.
+' Preserves:
+' B, C, D, E, H, L are not used
+' Returns:
+' UByte: bank number 0,1,2,3,4,5,6,7
+'
+' Version zx81sd: devuelve el "banco logico" guardado por la ultima
+' llamada a SetBankPreservingINTs (ver arriba). En ASM a mano por el
+' mismo motivo que SetBankPreservingINTs: preservar B,C,D,E,H,L de
+' verdad para las rutinas de MSFS que dependen de ello.
+' ----------------------------------------------------------------
+FUNCTION FASTCALL GetBankPreservingRegs() AS UByte
+ASM
+ ld a, (_ZX81SD_MSFS_Bank)
+END ASM
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Check whether memory paging works (128,+2,...) or not (16,48)
+' Returns:
+' UByte: 1 if paging works, 0 if it does not
+'
+' Version zx81sd: siempre devuelve 0. No existe paginacion de pantalla
+' visible al estilo Spectrum 128K (un unico framebuffer fisico, bloque
+' 6); ver la nota de cabecera de este fichero sobre por que esto no
+' afecta a que MSFS funcione (usa el mapeador propio, no esta funcion).
+' ----------------------------------------------------------------
+FUNCTION FASTCALL CheckMemoryPaging() AS UByte
+ RETURN 0
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Set the visible screen (either in bank5 or bank7)
+' and updates the system variable BANKM.
+' Only works on 128K and compatible models.
+' Parameters:
+' Ubyte: bank number 5 or 7
+' Preserves:
+' D, E, H, L are not used
+'
+' Version zx81sd: no existe hardware de doble pantalla visible
+' intercambiable (bancos 5/7 del Spectrum 128K) -- zx81sd solo tiene un
+' framebuffer fisico (bloque 6, SCREEN_ADDR fijo en $C000). Se deja como
+' stub seguro (no toca $5B5C ni el puerto $7FFD, que no existen aqui);
+' CheckMemoryPaging() devuelve 0, asi que en la practica nunca se llama
+' de verdad. Doble buffer de pantalla real (alternando que pagina
+' respalda el bloque 6 via el mapeador) seria una funcionalidad aparte,
+' no cubierta aqui.
+' ----------------------------------------------------------------
+SUB FASTCALL SetVisibleScreen(bankNumber AS UByte)
+END SUB
+
+
+' ----------------------------------------------------------------
+' Returns the bank of visible screen (either 5 or 7)
+' according to system variable BANKM.
+' Only works on 128K and compatible models.
+' Returns:
+' UByte: bank 5 or 7
+'
+' Version zx81sd: stub seguro, ver SetVisibleScreen arriba.
+' ----------------------------------------------------------------
+FUNCTION FASTCALL GetVisibleScreen() AS UByte
+ RETURN 5
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Toggles the visible screen (from 5 to 7, or from 7 to 5)
+' and updates the system variable BANKM.
+' Only works on 128K and compatible models.
+'
+' Version zx81sd: stub seguro, ver SetVisibleScreen arriba.
+' ----------------------------------------------------------------
+SUB FASTCALL ToggleVisibleScreen()
+END SUB
+
+
+' ----------------------------------------------------------------
+' Copy contents of screen5 to screen7 (display file + attribs)
+' Only works on 128K and compatible models.
+'
+' Version zx81sd: stub seguro, ver SetVisibleScreen arriba.
+' ----------------------------------------------------------------
+SUB FASTCALL CopyScreen5ToScreen7()
+END SUB
+
+
+' ----------------------------------------------------------------
+' Copy contents of screen7 to screen5 (display file + attribs)
+' Only works on 128K and compatible models.
+'
+' Version zx81sd: stub seguro, ver SetVisibleScreen arriba.
+' ----------------------------------------------------------------
+SUB FASTCALL CopyScreen7ToScreen5()
+END SUB
+
+
+' ----------------------------------------------------------------
+' Set ScreenBufferAddr and AttrBufferAddr to screen5
+' Only works on 128K and compatible models.
+'
+' Version zx81sd: stub seguro, ver SetVisibleScreen arriba. IMPORTANTE:
+' NO se puede simplemente copiar el original -- SetScreenBufferAddr($4000)
+' redirigiria SCREEN_ADDR a plena zona de codigo/runtime ($1000-$7FFF en
+' nuestro mapa de memoria), corrompiendo cualquier PRINT/grafico
+' posterior. Se deja vacia.
+' ----------------------------------------------------------------
+SUB FASTCALL SetDrawingScreen5()
+END SUB
+
+
+' ----------------------------------------------------------------
+' Put screen7 at $c000 (in case it is not), and
+' Set ScreenBufferAddr and AttrBufferAddr to screen7
+' Only works on 128K and compatible models.
+' Returns:
+' Bank7 is set at $c000, old bank is removed
+' UByte: bank that was at $c000 (to restore it manually IYW)
+'
+' Version zx81sd: stub seguro, ver SetVisibleScreen arriba. $c000/$d800
+' coinciden por casualidad con nuestro SCREEN_ADDR/SCREEN_ATTR_ADDR
+' reales, pero esta funcion tambien mapea banco7 (aqui reinterpretado
+' como el banco de MSFS) al bloque 7, no al bloque 6 (pantalla) -- no
+' hay overlap real posible entre "pantalla" y "banco 7" en zx81sd, asi
+' que se deja como no-op en vez de fingir un intercambio que no existe.
+' ----------------------------------------------------------------
+FUNCTION FASTCALL SetDrawingScreen7() AS UByte
+ RETURN 5
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Toggle ScreenBufferAddr and AttrBufferAddr between screen5,7
+' Only works on 128K and compatible models.
+'
+' Version zx81sd: stub seguro, ver SetVisibleScreen arriba.
+' ----------------------------------------------------------------
+SUB FASTCALL ToggleDrawingScreen()
+END SUB
+
+
+' ----------------------------------------------------------------
+' MaskedSpritesBackgroundSet = 0 or 1 is the Set of Backgrounds
+'
+' MaskedSpritesBackground(i) is the address where Background i begins
+'
+' NumberofMaskedSprites is a MACRO that should be #define-d
+' before #include-ing this file
+'
+' MaskedSprites_USE_STACK_TRANSFER is a MACRO that should be #define-d
+' if you want this library to use Stack PUSH+POP instructions to speed-up
+' transfer of information between different parts of the RAM
+' (this library will disable interrupts before using Stack Transfer)
+'
+' ChangeMaskedSpritesBackgroundSet() changes the Set of Backgrounds
+' Returns:
+' Byte: new value of MaskedSpritesBackgroundSet (IYW to use it)
+' ----------------------------------------------------------------
+dim MaskedSpritesBackgroundSet AS UByte = 0
+
+#define MaskedSpritesBackground(i) ( $db00+48*CAST(UInteger,i)+48*CAST(UInteger,NumberofMaskedSprites)*MaskedSpritesBackgroundSet )
+
+FUNCTION FASTCALL ChangeMaskedSpritesBackgroundSet() AS UByte
+ MaskedSpritesBackgroundSet = MaskedSpritesBackgroundSet bXOR 1
+ RETURN MaskedSpritesBackgroundSet
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' MaskedSprites_NEXT_ROW is a MACRO of ASM code, based on code from
+' https://zonadepruebas.com/viewtopic.php?f=15&t=8372&start=40#p81507
+' and found by Joaquin Ferrero
+' ----------------------------------------------------------------
+#define MaskedSprites_NEXT_ROW \
+ ld a,e ; 4 A = E \
+ sub 224 ; 7 A = E + 32 (SUB 224 is similar to +32) \
+ ; CF = 0/1 iff E >=/< 224 iff a third is/isn't crossed \
+ ld e,a ; 4 \
+ sbc a,a ; 4 A = 0/255 \
+ and 248 ; 7 A = 0/248 (248 = -8) \
+ add a,d ; 4 A = D/D-8 iff a third is/isn't crossed \
+ ld d,a ; 4 += 34 Ts
+
+
+' ----------------------------------------------------------------
+' Save background and Draw sprite in screen
+' Parameters:
+' UByte: X coordinate (0:left to 240:right)
+' UByte: Y coordinate (0:up to 176:down)
+' UInteger: Address where background will be saved
+' UInteger: Address where sprite image begins
+' ----------------------------------------------------------------
+SUB FASTCALL SaveBackgroundAndDrawSprite(X AS UByte, Y AS UByte, backgroundAddr AS UInteger, spriteImageAddr AS UInteger)
+ASM
+ PROC
+ LOCAL shiftright, shiftleft, noshift
+ LOCAL loopSR, loopSL, loopNS, loopR, loopL, branchSR, branchSL, branchNS
+ ; A = X
+ pop de ; returnAddr
+ exx
+ pop bc ; B = Y
+ ld c,a ; C = X
+; BEGIN code from https://skoolkid.github.io/rom/asm/22AA.html
+ rlca
+ rlca
+ rlca ; A = %c4c3c2c1c0c7c6c5
+ xor b
+ and %11000111
+ xor b ; A = %c4c3b5b4b3c7c6c5
+ rlca
+ rlca
+ ld e,a ; E = %b5b4b3c7c6c5c4c3
+ ld a,b
+ and %11111000
+ rra
+ rra
+ rra ; A = %.0.0.0b7b6b5b4b3
+ xor b
+ and %11111000
+ xor b
+ ld d,a ; D = %.0.0.0b7b6b2b1b0
+; END code from https://skoolkid.github.io/rom/asm/22AA.html
+ ld hl,(.core.SCREEN_ADDR)
+ add hl,de
+ ex de,hl ; DE = screenAddr where drawing will start
+ ld a,c; ; A = X
+ and 7
+ jr z,noshift ; jump if X is a multiple of 8 (unlikely)
+ ; continue if sprite must be shifted
+ cp 4 ; is >= 4 ?
+ jp nc,shiftleft ; shift left if X MOD 8 = 4,5,6,7
+ ; shift right if X MOD 8 = 1,2,3
+shiftright:
+ pop bc ; backgroundAddr
+ exx
+ pop hl ; spriteImageAddr
+ push de ; returnAddr
+ push ix
+ ld ixh,16 ; 16 scanlines
+ ld ixl,a ; IXl = X MOD 8 = 1,2,3,4
+loopSR:
+ ld a,(hl) ; mask1
+ inc hl
+ ld c,(hl) ; graph1
+ inc hl
+ ld d,(hl) ; mask2
+ inc hl
+ ld e,(hl) ; graph2
+ inc hl
+ push hl ; spriteImageAddr
+
+ ld hl,$FF00 ; H = 255 , L = 0
+ ld b,ixl
+loopR:
+ scf ; 4
+ rra ; 4; SCF + RRA injects a 1 in bit7 of A
+ rr d ; 8
+ rr h ; 8
+ srl c ; 8; ShiftRightLogical injects a 0 in bit7 of C
+ rr e ; 8
+ rr l ; 8
+ djnz loopR ; 4+4+8+8+8+8+8 = 48 Ts
+ ld b,a
+ push hl ; H,L = mask,graph 3rd byte
+ push de ; D,E = mask,graph 2nd byte
+ push bc ; B,C = mask,graph 1st byte
+ exx
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 1st byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 2nd byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 3rd byte done
+ dec e
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branchSR ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ exx
+ pop hl ; spriteImageAddr
+ dec ixh
+ jp nz,loopSR
+ pop ix
+ ret
+branchSR:
+ MaskedSprites_NEXT_ROW
+ exx
+ pop hl ; spriteImageAddr
+ dec ixh
+ jp nz,loopSR
+ pop ix
+ ret
+
+noshift:
+ pop bc ; backgroundAddr
+ pop hl ; spriteImageAddr
+ exx
+ push de ; returnAddr
+ exx
+ push ix
+ ld ixh,16 ; 16 scanlines
+loopNS:
+ ld a,(de) ; screen
+ ld (bc),a; ; save
+ inc bc
+ and (hl); ; mask
+ inc hl
+ or (hl) ; graph
+ inc hl
+ ld (de),a ; 1st byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ and (hl); ; mask
+ inc hl
+ or (hl) ; graph
+ inc hl
+ ld (de),a ; 2nd byte done
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branchNS ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ dec ixh
+ jp nz,loopNS
+ pop ix
+ ret
+branchNS:
+ MaskedSprites_NEXT_ROW
+ dec ixh
+ jp nz,loopNS
+ pop ix
+ ret
+
+shiftleft:
+ pop bc ; backgroundAddr
+ exx
+ pop hl ; spriteImageAddr
+ push de ; returnAddr
+ push ix
+ ld ixh,16 ; 16 scanlines
+ sub 8
+ neg ; A = 8 - oldA
+ ld ixl,a ; IXl = 8 - (X MOD 8) = 8 - 4,5,6,7 = 4,3,2,1
+loopSL:
+ ld a,(hl) ; mask1
+ inc hl
+ ld c,(hl) ; graph1
+ inc hl
+ ld d,(hl) ; mask2
+ inc hl
+ ld e,(hl) ; graph2
+ inc hl
+ push hl ; spriteImageAddr
+
+ ld hl,$FF00 ; H = 255 , L = 0
+ ld b,ixl
+loopL:
+ sll d ; 8; ShiftLeftLogical injects a 1 in bit0 of D
+ rla ; 4
+ rl h ; 8
+ sla e ; 8; ShiftLeftArithmetic injects a 0 in bit0 of E
+ rl c ; 8
+ rl l ; 8
+ djnz loopL ; 8+4+8+8+8+8 = 44 Ts
+ ld b,a
+ push de ; D,E = mask,graph 3rd byte
+ push bc ; B,D = mask,graph 2nd byte
+ push hl ; H,L = mask,graph 1st byte
+ exx
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 1st byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 2nd byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+ pop hl
+ and h ; mask
+ or l ; graph
+ ld (de),a ; 3rd byte done
+ dec e
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branchSL ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ exx
+ pop hl ; spriteImageAddr
+ dec ixh
+ jp nz,loopSL
+ pop ix
+ ret
+branchSL:
+ MaskedSprites_NEXT_ROW
+ exx
+ pop hl ; spriteImageAddr
+ dec ixh
+ jp nz,loopSL
+ pop ix
+ ret
+ ENDP
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' Restore background in screen
+' Parameters:
+' UByte: X coordinate (0:left to 240:right)
+' UByte: Y coordinate (0:up to 176:down)
+' UInteger: Address where saved background begins
+' ----------------------------------------------------------------
+SUB FASTCALL RestoreBackground(X AS UByte, Y AS UByte, backgroundAddr AS UInteger)
+ASM
+ PROC
+ LOCAL loop2b, loop3b, branch2b, branch3b
+ ; A = X
+ pop de ; returnAddr
+ exx
+ pop bc ; B = Y
+ ld c,a ; C = X
+; BEGIN code from https://skoolkid.github.io/rom/asm/22AA.html
+ rlca
+ rlca
+ rlca ; A = %c4c3c2c1c0c7c6c5
+ xor b
+ and %11000111
+ xor b ; A = %c4c3b5b4b3c7c6c5
+ rlca
+ rlca
+ ld e,a ; E = %b5b4b3c7c6c5c4c3
+ ld a,b
+ and %11111000
+ rra
+ rra
+ rra ; A = %.0.0.0b7b6b5b4b3
+ xor b
+ and %11111000
+ xor b
+ ld d,a ; D = %.0.0.0b7b6b2b1b0
+; END code from https://skoolkid.github.io/rom/asm/22AA.html
+ ld hl,(.core.SCREEN_ADDR)
+ add hl,de
+ ex de,hl ; DE = screenAddr where restoring will start
+ pop hl ; backgroundAddr
+ exx
+ push de ; returnAddr
+ exx
+ ld a,c; ; A = X
+ ld bc,$10FF ; B = 16, C = 255 (up to 255 LDIs do not change B)
+ and 7
+ jr z,loop2b ; jump if X is a multiple of 8 (unlikely)
+ ; continue if restoring 3 bytes per scanline
+; 3bytes per scanline
+loop3b:
+ ldi ; 16 Ts vs 7+7+6+4=24 Ts
+ ldi
+ ldi ; 3 bytes background restored to screen
+ dec de ; last LDI could have increased D if initially E=253...
+ dec e ; ...so DEC DE restores D in that case
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branch3b ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ djnz loop3b
+ ret
+branch3b:
+ MaskedSprites_NEXT_ROW
+ djnz loop3b
+ ret
+; 2bytes per scanline
+loop2b:
+ ldi ; 16 Ts vs 7+7+6+4=24 Ts
+ ldi ; 2 bytes background restored to screen
+ dec de ; last LDI could have increased D if initially E=254...
+ dec e ; ...so DEC DE restores D in that case
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branch2b ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ djnz loop2b
+ ret
+branch2b:
+ MaskedSprites_NEXT_ROW
+ djnz loop2b
+ ret
+ ENDP
+END ASM
+END SUB
+
+
+' ----------------------------------------------------------------
+' Structure of the Masked Sprites FileSystem (MSFS):
+'
+' MSFS consists of many blocks of 96 bytes
+' MSFS starts at address stored in MaskedSpritesFileSystemStart, e.g., 56736
+' MSFS length is a multiple of 96 bytes, e.g., 8736 = 91*96 bytes
+' With that length, MSFS ranges from 56736 to 65471
+' MSFS stores Images (mask+graph) of Masked Sprites
+' Blocks used are marked in the FSB (https://en.wikipedia.org/wiki/Free-space_bitmap)
+'
+' First block of the MSFS (superblock, block number = 0)
+' start+0 DEFB number of blocks in MSFS = bits of the FSB, e.g., 91
+' start+1 DEFB number of bytes of the FSB, e.g., 12 (91/8 = 11.4)
+' start+2-13 DEFS 12 is the FSB (12 bytes = 96 bits is enough for 91 blocks)
+' start+14-15 unused
+'
+' Block of an unshifted image (block number n = 0,...,90)
+' start+n*96 \
+' ... | 16 bytes unused for n>0, superblock for n=0
+' start+n*96+15 /
+' start+n*96+16-17 DEFW start+n*96+32 = start of unshifted image
+' start+n*96+18-19 DEFW address of block for image shifted 1 pixel, or 0 if not used
+' start+n*96+20-21 DEFW address of block for image shifted 2 pixels, or 0 if not used
+' ...
+' start+n*96+30-31 DEFW address of block for image shifted 7 pixels, or 0 if not used
+' start+n*96+32-95 DEFS 64 the unshifted image (mask+graph)
+'
+' Block number 0 (n=0) is special because it contains
+' 16 bytes for the superblock (including 2 unused bytes) +
+' 16 bytes for addressing unshifted and shifted versions of the first image +
+' 64 bytes for the first unshifted image
+'
+' Block of a shifted image (block number m = 1,...,90, note m>0)
+' start+m*96+0 DEFS 96 the shifted image (mask+graph)
+' ----------------------------------------------------------------
+dim MaskedSpritesFileSystemStart AS UInteger = 0
+' ----------------------------------------------------------------
+' MaskedSpritesFileSystemStart = address where Masked Sprites FileSystem starts
+'
+' InitMaskedSpritesFileSystem() inits the MSFS
+' Returns:
+' UInteger: value of MaskedSpritesFileSystemStart (IYW to use it)
+'
+' Version zx81sd: MaskedSpritesFileSystemStart es una direccion FIJA
+' dentro del bloque 7 ($E000, la pagina dedicada MaskedSprites_MSFS_Page)
+' en vez de "lo que quede hasta $FFFF" (el original asumia RAM plana
+' Spectrum ahi, que en zx81sd es una ventana paginada de 8K, no RAM
+' contigua). Con $E000: l = Int((65536-57344)/96) = 85 bloques -- toda
+' la MSFS cabe de sobra en los 8K del bloque 7. El resto de la funcion
+' (calculo de bytes del FSB, pokes) es identico al original. Se llama
+' siempre a SetBankPreservingINTs(7) (no gated por CheckMemoryPaging(),
+' que aqui es irrelevante para MSFS -- ver cabecera del fichero).
+' ----------------------------------------------------------------
+FUNCTION FASTCALL InitMaskedSpritesFileSystem() AS UInteger
+ DIM b AS UByte
+ DIM l, j AS UInteger
+
+ b = GetBankPreservingRegs()
+ if b<>7 then SetBankPreservingINTs(7)
+ ' Llamada BASIC explicita a Map(), redundante con la que ya hace
+ ' SetBankPreservingINTs por ASM a mano (mapea el mismo bloque/pagina
+ ' otra vez, sin efecto): el analisis de "codigo muerto" del
+ ' compilador no cuenta las llamadas hechas desde ASM como uso, y sin
+ ' esto Map() se elimina del binario -> "Undefined GLOBAL label" al
+ ' intentar llamarla desde dentro de SetBankPreservingINTs.
+ Map(7, MaskedSprites_MSFS_Page)
+ MaskedSpritesFileSystemStart = $E000
+ l = -MaskedSpritesFileSystemStart
+ l = Int(l/96)
+ poke MaskedSpritesFileSystemStart, l ' MSFS blocks = FSB bits
+ if l=0 then STOP
+ l = 1+Int((l-1)/8)
+ poke MaskedSpritesFileSystemStart+1,l ' FSB bytes
+ ' Limpiar el FSB (bitmap de bloques libres, bytes start+2..start+1+l).
+ ' Ni esta version ni la original de zx48k lo inicializaban: en el
+ ' Spectrum no hace falta porque el test de RAM de la ROM deja toda
+ ' la memoria a cero en el arranque, asi que el bitmap nace "todo
+ ' libre" gratis. En zx81sd la pagina del bloque 7 llega con basura
+ ' de fabrica: todos los bits aparecian a 1 ("ocupado"),
+ ' FindFirstUnusedBlockInMSFS recorria el FSB entero sin encontrar
+ ' hueco y RegisterSpriteImageInMSFS devolvia siempre 0 (lleno).
+ FOR j = MaskedSpritesFileSystemStart+2 TO MaskedSpritesFileSystemStart+1+l
+ poke j, 0
+ NEXT j
+ if b<>7 then SetBankPreservingINTs(b)
+ return MaskedSpritesFileSystemStart
+
+' Now, some assembly routines needed for next SUB/FUNCTIONs
+ASM
+; ----------------------------------------------------------------
+; Find memory addres in MSFS for the start of a block
+; Parameters:
+; L = blocknumber = n = 0,1,2,... (probably less than 200)
+; Preserves:
+; A, B, C are not used
+; Returns:
+; HL = start+n*96
+; DE = start
+; ----------------------------------------------------------------
+FindMemoryAdressForBlockInMSFS:
+ PROC
+ ld h,0
+ ld d,h
+ ld e,l ; HL = DE = n = blocknumber
+ add hl,de
+ add hl,de ; HL = DE*3
+ add hl,hl
+ add hl,hl
+ add hl,hl
+ add hl,hl
+ add hl,hl ; HL = DE*3*(2^5) = DE*96
+ ld de,(_MaskedSpritesFileSystemStart)
+ add hl,de; ; HL = start+n*96
+ ret
+ ENDP
+
+; ----------------------------------------------------------------
+; Find First Unused Block in MSFS and (optionally) Book it
+; Parameters:
+; CarryFlag = 0/1 don't/do Book it
+; Preserves:
+; C is not used
+; Returns:
+; CarryFlag = 0 if found, 1 if not found
+; A = FirstUnusedBlock if found
+; HL = start+A*96 if found
+; ----------------------------------------------------------------
+FindFirstUnusedBlockInMSFS:
+ PROC
+ LOCAL loop1, loop2, full, found, loop3, compute
+ ex af,af' ; saves CarryFlag
+ ld hl,(_MaskedSpritesFileSystemStart)
+ ld d,(hl)
+ ld e,d ; D = E = number of bits in the FSB = N
+ inc hl
+ inc hl ; HL points to the first byte in the FSB
+loop1:
+ ld a,(hl)
+ ld b,8
+loop2:
+ rrca
+ jr nc,found
+ dec e
+ jr z,full
+ djnz loop2
+ inc hl
+ jp loop1
+full: ; E = 0
+ scf ; CarryFlag=1 = ERROR
+ ret
+found: ; E = N,N-1,N-2,...,1
+ ex af,af'
+ jr nc,compute
+ ex af,af'
+ rlca ; undo last RRCA
+ or 1 ; mark this block as used
+loop3:
+ rrca ; finish the 8-bit rotation to...
+ djnz loop3 ; leave bits where they were
+ ld (hl),a ; effective booking
+compute:
+ ld a,d ; A = N
+ sub e ; A = 0,1,2,...,N-1 = blocknumber
+ ld l,a
+ call FindMemoryAdressForBlockInMSFS
+ and a ; CarryFlag=0 = OK
+ ret ; A = blocknumber, HL = start+A*96
+ ENDP
+END ASM
+
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Get Number of Free Blocks in MSFS
+' Returns:
+' UByte: number of free blocks in MSFS
+' ----------------------------------------------------------------
+FUNCTION FASTCALL GetNumberofFreeBlocksInMSFS() AS UByte
+ASM
+ PROC
+ LOCAL loop1, loop2, exit
+ call _GetBankPreservingRegs
+ cp 7 ; ZeroFlag=0 (NZ) iff _GetBankPreservingRegs returns A<>7
+ push af ; ZF and original RAM bank when FUNCTION was called
+ ld a,7
+ call nz,_SetBankPreservingINTs ; set RAM7 if _GetBankPreservingRegs returns A<>7
+ xor a ; A = 0 = number of reset bits in the FSB
+ ld hl,(_MaskedSpritesFileSystemStart)
+ ld d,(hl) ; D = number of bits in the FSB = N
+ ld e,a ; E = 0 always
+ inc hl
+ inc hl ; HL points to the first byte in the FSB
+loop1:
+ ld c,(hl)
+ ld b,8
+loop2:
+ rr c
+ ccf ; CarryFlag = 0/1 = bit in the FSB is set/reset
+ adc a,e ; A += CarryFlag = number of reset bits in the FSB
+ dec d
+ jr z,exit ; return A if all bits in FSB have been checked
+ djnz loop2
+ inc hl
+ jr loop1
+exit:
+ ex af,af' ; A' = number of free blocks in MSFS
+ pop af ; ZF and original RAM bank when FUNCTION was called
+ call nz,_SetBankPreservingINTs
+ ex af,af' ; A = number of free blocks in MSFS
+ ret
+ ENDP
+END ASM
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Register spriteImage in MSFS
+' Parameters:
+' UInteger: address where spriteImage begins
+' Returns:
+' UInteger: registry number in the MSFS = start+n*96+16 if OK
+' 0 if not OK
+' ----------------------------------------------------------------
+FUNCTION FASTCALL RegisterSpriteImageInMSFS(spriteImageAddr AS UInteger) AS UInteger
+ASM
+ PROC
+ LOCAL full, exit
+ call _GetBankPreservingRegs
+ cp 7 ; ZeroFlag=0 (NZ) iff _GetBankPreservingRegs returns A<>7
+ push af ; ZF and original RAM bank when FUNCTION was called
+ push hl ; spriteImageAddr
+ ld a,7
+ call nz,_SetBankPreservingINTs ; set RAM7 if it was not set
+ scf
+ call FindFirstUnusedBlockInMSFS ; and book it (SCF)
+ jr c,full
+ ld bc,16
+ add hl,bc
+ push hl ; HL = start+A*96+16
+ ld d,h
+ ld e,l
+ inc de ; DE = start+A*96+17
+ dec bc ; BC = 15
+ ld (hl),0
+ ldir ; reset RAM from start+A*96+16 to start+A*96+31 (incl.)
+ pop hl ; HL = start+A*96+16
+ ld b,h
+ ld c,l ; BC = start+A*96+16
+ ld (hl),e ; DE = start+A*96+32 (after last LDIR)
+ inc hl
+ ld (hl),d ; start+A*96+16-17 DEFW start+A*96+32
+ pop hl ; spriteImageAddr
+ push bc ; BC = start+A*96+16
+ ld bc,64
+ ldir ; transfer from spriteImageAddr to start+A*96+32
+ pop hl ; HL = start+A*96+16
+exit:
+ pop af ; ZF and original RAM bank when FUNCTION was called
+ call nz,_SetBankPreservingINTs
+ ret
+full:
+ pop hl ; spriteImageAddr
+ ld hl,0
+ jr exit
+ ENDP
+END ASM
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Register spriteGraph and spriteMask in MSFS
+' (useful when different Graphs share the same Mask)
+'
+' Data in spriteGraph and spriteMask MUST be in "putchars format"
+'
+' Parameters:
+' UInteger: address where spriteGraph begins
+' UInteger: address where spriteMask begins
+' Returns:
+' UInteger: registry number in the MSFS = start+n*96+16 if OK
+' 0 if not OK
+' ----------------------------------------------------------------
+FUNCTION FASTCALL RegisterSpriteGraphAndMaskInMSFS(spriteGraphAddr AS UInteger,spriteMaskAddr AS UInteger) AS UInteger
+ASM
+ PROC
+ LOCAL full, exit, loop1, loop2
+ pop de ; returnAddr
+ pop bc ; spriteMaskAddr
+ push de ; returnAddr
+; stack is empty. Now we will push data to be preserved
+ exx ; HL' = spriteGraphAddr, BC' = spriteMaskAddr
+ call _GetBankPreservingRegs
+ cp 7 ; ZeroFlag=0 (NZ) iff _GetBankPreservingRegs returns A<>7
+ push af ; ZF and original RAM bank when FUNCTION was called
+ ld a,7
+ call nz,_SetBankPreservingINTs ; set RAM7 if it was not set
+ scf
+ call FindFirstUnusedBlockInMSFS ; and book it (SCF)
+ jr c,full
+ ld bc,16
+ add hl,bc
+ push hl ; HL = start+A*96+16
+ ld d,h
+ ld e,l
+ inc de ; DE = start+A*96+17
+ dec bc ; BC = 15
+ ld (hl),0
+ ldir ; reset RAM from start+A*96+16 to start+A*96+31 (incl.)
+ pop hl ; HL = start+A*96+16
+ ld (hl),e ; DE = start+A*96+32 (after last LDIR)
+ inc hl
+ ld (hl),d ; start+A*96+16-17 DEFW start+A*96+32
+ dec hl
+ push hl ; return value HL = start+A*96+16
+ push de ; DE = start+A*96+32 DEFS 64 the unshifted image (mask+graph)
+ exx ; HL = spriteGraphAddr, BC = spriteMaskAddr
+ pop de ; DE = DEFS 64 the unshifted image (mask+graph)
+ push ix
+ push de ; DE = start+A*96+32
+ ld ixl,16
+loop1:
+ ld a,(bc) ; mask
+ ld (de),a
+ inc bc
+ inc de
+ ld a,(hl) ; graph
+ ld (de),a
+ inc hl
+ inc de
+ inc de
+ inc de
+ dec ixl
+ jp nz,loop1
+ pop de ; DE = start+A*96+32
+ inc de
+ inc de ; DE = start+A*96+32 +2
+ ld ixl,16
+loop2:
+ ld a,(bc) ; mask
+ ld (de),a
+ inc bc
+ inc de
+ ld a,(hl) ; graph
+ ld (de),a
+ inc hl
+ inc de
+ inc de
+ inc de
+ dec ixl
+ jp nz,loop2
+ pop ix
+ pop hl ; return value HL = start+A*96+16
+exit:
+ pop af ; ZF and original RAM bank when FUNCTION was called
+ call nz,_SetBankPreservingINTs
+ ret
+full:
+ ld hl,0
+ jr exit
+ ENDP
+END ASM
+END FUNCTION
+
+
+' ----------------------------------------------------------------
+' Save background and Draw sprite registered in the MSFS
+' Parameters:
+' UByte: X coordinate (0:left to 240:right)
+' UByte: Y coordinate (0:up to 176:down)
+' UInteger: address where background will be saved
+' UInteger: registry number in the MSFS for the spriteImage
+' ----------------------------------------------------------------
+SUB FASTCALL SaveBackgroundAndDrawSpriteRegisteredInMSFS(X AS UByte, Y AS UByte, backgroundAddr AS UInteger, spriteImageReg AS UInteger)
+ASM
+ PROC
+ LOCAL full, makeShiftedImage, loopMSI, loopMSI1
+ LOCAL useShiftedImage, loopUSI, branchUSI, exitUSI
+ LOCAL noshift, loopNS, branchNS, exitNS
+ ; A = X
+ pop de ; returnAddr
+ exx
+ pop bc ; B = Y
+ ld c,a ; C = X
+; BEGIN code from https://skoolkid.github.io/rom/asm/22AA.html
+ rlca
+ rlca
+ rlca ; A = %c4c3c2c1c0c7c6c5
+ xor b
+ and %11000111
+ xor b ; A = %c4c3b5b4b3c7c6c5
+ rlca
+ rlca
+ ld e,a ; E = %b5b4b3c7c6c5c4c3
+ ld a,b
+ and %11111000
+ rra
+ rra
+ rra ; A = %.0.0.0b7b6b5b4b3
+ xor b
+ and %11111000
+ xor b
+ ld d,a ; D = %.0.0.0b7b6b2b1b0
+; END code from https://skoolkid.github.io/rom/asm/22AA.html
+ ld hl,(.core.SCREEN_ADDR)
+ add hl,de
+ ex de,hl ; DE = screenAddr where drawing will start
+ ld a,c; ; A = X, keep it secret, keep it safe
+ pop bc ; BC = backgroundAddr
+ pop hl ; HL = spriteImageReg
+ exx
+ push de ; returnAddr
+ exx
+; stack is empty. Now we will push data to be preserved
+ push ix
+ ld ixh,16 ; 16 scanlines
+ and 7
+ jr z,noshift ; jump if X is a multiple of 8 (unlikely)
+ push de ; DE = screenAddr where drawing will start
+ push bc ; BC = backgroundAddr
+ ld b,0
+ ld c,a ; BC = A = 1,2,3,4,5,6,7
+ add hl,bc
+ add hl,bc ; HL = start+n*96+16+C*2 DEFW address of block for image shifted C pixels, or 0 if not used
+ ld e,(hl)
+ inc hl
+ ld d,(hl) ; DE = address for image shifted C pixels, or 0 if not used
+ ld a,d
+ or e
+ jp z,makeShiftedImage
+;UseShiftedImage (USI)
+useShiftedImage:
+ ex de,hl ; HL = address for image shifted C pixels
+ pop bc ; BC = backgroundAddr
+ pop de ; DE = screenAddr where drawing will start
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even
+ jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI
+ ld a,i ; read IFF2 again to ensure that IFF2=0=DI
+1: ex af,af'
+ di
+ ld (exitUSI+1),sp
+ ld sp,hl
+#endif
+loopUSI:
+ ld a,(de) ; screen
+ ld (bc),a; ; save
+ inc bc
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ pop hl
+ and l ; mask
+ or h ; graph
+#else
+ and (hl); ; mask
+ inc l
+ or (hl) ; graph
+ inc hl
+#endif
+ ld (de),a ; 1st byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a; ; save
+ inc bc
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ pop hl
+ and l ; mask
+ or h ; graph
+#else
+ and (hl); ; mask
+ inc l
+ or (hl) ; graph
+ inc hl
+#endif
+ ld (de),a ; 2nd byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a; ; save
+ inc bc
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ pop hl
+ and l ; mask
+ or h ; graph
+#else
+ and (hl); ; mask
+ inc l
+ or (hl) ; graph
+ inc hl
+#endif
+ ld (de),a ; 3rd byte done
+ dec e
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branchUSI ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ dec ixh
+ jp nz,loopUSI
+exitUSI:
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ ld sp,$1234
+ pop ix
+ ex af,af'
+ ret po ; Return with DI if IFF2=0=DI at the beginning
+ ei ; Return with EI if IFF2=1=EI at the beginning
+ ret
+#else
+ pop ix
+ ret
+#endif
+branchUSI:
+ MaskedSprites_NEXT_ROW
+ dec ixh
+ jp nz,loopUSI
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ jp exitUSI
+#else
+ pop ix
+ ret
+#endif
+;NoShift (NS)
+noshift:
+ ld a,(hl) ; HL = spriteImageReg
+ inc hl
+ ld h,(hl)
+ ld l,a ; HL = start of unshifted image
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ ld a,i ; IFF2=0/1=DI/EI is saved in PF=0/1=Odd/Even
+ jp pe,1f ; if PF=Even=1, it is sure that IFF2=1=EI
+ ld a,i ; read IFF2 again to ensure that IFF2=0=DI
+1: ex af,af'
+ di
+ ld (exitNS+1),sp
+ ld sp,hl
+#endif
+loopNS:
+ ld a,(de) ; screen
+ ld (bc),a; ; save
+ inc bc
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ pop hl
+ and l ; mask
+ or h ; graph
+#else
+ and (hl); ; mask
+ inc l
+ or (hl) ; graph
+ inc hl
+#endif
+ ld (de),a ; 1st byte done
+ inc e
+
+ ld a,(de) ; screen
+ ld (bc),a ; save
+ inc bc
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ pop hl
+ and l ; mask
+ or h ; graph
+#else
+ and (hl); ; mask
+ inc l
+ or (hl) ; graph
+ inc hl
+#endif
+ ld (de),a ; 2nd byte done
+ dec e
+
+ inc d
+ ld a,d
+ and 7
+ jr z,branchNS ; 7Ts no jump (7/8 times), 12Ts jump (1/8 times)
+ dec ixh
+ jp nz,loopNS
+exitNS:
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ ld sp,$1234
+ pop ix
+ ex af,af'
+ ret po ; Return with DI if IFF2=0=DI at the beginning
+ ei ; Return with EI if IFF2=1=EI at the beginning
+ ret
+#else
+ pop ix
+ ret
+#endif
+branchNS:
+ MaskedSprites_NEXT_ROW
+ dec ixh
+ jp nz,loopNS
+#ifdef MaskedSprites_USE_STACK_TRANSFER
+ jp exitNS
+#else
+ pop ix
+ ret
+#endif
+;MakeShiftedImage (MSI)
+makeShiftedImage:
+ ld a,8
+ sub c ; C = X MOD 8 = 1,2,3,4,5,6,7 to the right
+ ld ixl,a ; IXl = 8 - C = 7,6,5,4,3,2,1 to the left
+ push hl ; HL = start+n*96+16+C*2 + 1
+ scf
+ call FindFirstUnusedBlockInMSFS ; and book it (SCF)
+ jr c,full
+ ex de,hl ; DE = start+m*96 = address for the shiftedimage-to-be
+ pop hl ; HL = start+n*96+16+C*2 + 1
+ ld (hl),d
+ dec hl
+ ld (hl),e; ; HL = start+n*96+16+C*2 DEFW address for the shifted image
+ ld b,0
+ sbc hl,bc
+ sbc hl,bc
+ ld a,(hl) ; HL = spriteImageReg
+ inc hl
+ ld h,(hl)
+ ld l,a ; HL = start of unshifted image
+ push de ; DE = address for image shifted C pixels
+ push de ; two PUSH because we will POP one just before useShiftedImage
+ exx
+ pop hl ; HL' = address for the shiftedimage-to-be
+ exx
+loopMSI:
+ ld a,(hl) ; mask1
+ inc hl
+ ld c,(hl) ; graph1
+ inc hl
+ ld d,(hl) ; mask2
+ inc hl
+ ld e,(hl) ; graph2
+ inc hl
+ push hl ; spriteImageAddr += 4
+ ld hl,$FF00 ; H = 255 , L = 0
+ ld b,ixl
+loopMSI1:
+ sll d ; 8; ShiftLeftLogical injects a 1 in bit0 of D
+ rla ; 4
+ rl h ; 8
+ sla e ; 8; ShiftLeftArithmetic injects a 0 in bit0 of E
+ rl c ; 8
+ rl l ; 8
+ djnz loopMSI1; 8+4+8+8+8+8 = 44 Ts
+ ld b,a
+ push de ; D,E = mask,graph 3rd byte
+ push bc ; B,D = mask,graph 2nd byte
+ push hl ; H,L = mask,graph 1st byte
+ exx
+ pop de ; D',E' = mask,graph 1st byte
+ ld (hl),d
+ inc hl
+ ld (hl),e
+ inc hl
+ pop de ; D',E' = mask,graph 2nd byte
+ ld (hl),d
+ inc hl
+ ld (hl),e
+ inc hl
+ pop de ; D',E' = mask,graph 3rd byte
+ ld (hl),d
+ inc hl
+ ld (hl),e
+ inc hl ; HL' += 6 in this loop
+ exx
+ pop hl ; spriteImageAddr
+ dec ixh
+ jp nz,loopMSI
+ pop de ; DE = address for image shifted C pixels
+ ld ixh,16 ; 16 scanlines
+ jp useShiftedImage
+full:
+ pop hl ; HL = start+n*96+16+C*2 + 1
+ pop bc ; BC = backgroundAddr
+ pop de ; DE = screenAddr where drawing will start
+ pop ix
+ ret
+ ENDP
+END ASM
+END SUB
+
+
+#endif
+
diff --git a/src/lib/arch/zx81sd/stdlib/dbuf.bas b/src/lib/arch/zx81sd/stdlib/dbuf.bas
new file mode 100644
index 000000000..39a7453da
--- /dev/null
+++ b/src/lib/arch/zx81sd/stdlib/dbuf.bas
@@ -0,0 +1,80 @@
+' SD81 Booster — double buffer (present-blit) control, port $E7 pseudo-block 8.
+'
+' Hardware spec (SD81 Booster, ay author): while enabled, the video
+' stops reading the live screen block (HFILE) and instead scans out a
+' private front buffer inside the FPGA's own screen shadow RAM. On every
+' vertical blanking period the hardware copies the live screen into that
+' front buffer, so what's on screen is always a complete snapshot taken
+' at the last VSYNC -- no tearing even if the program erases/redraws in
+' the middle of a scan.
+'
+' Two control paths exist on real hardware: POKE 2057 (memory-mapped IO)
+' and this port $E7 pseudo-block-8 path. zx81sd's boot (tools/boot1.asm)
+' always disables memory-mapped IO (POKE 2056,0) and leaves the $E7
+' mapper in full-paging mode before jumping to the compiled program, so
+' every zx81sd binary is in exactly the state the port path needs -- the
+' POKE 2057 path is NOT usable from zxbasic (memory-mapped IO is off).
+'
+' Front buffer block: this is NOT one of the program's own blocks 0-5
+' (code/data, mapped via boot1.asm/vectors.asm) nor block 6 (the live
+' screen, $C000) nor block 7 (data banking, see Map() in mcu.bas) -- the
+' front buffer lives in the FPGA's private screen shadow RAM, addressed
+' with its own independent 0-7 block index. The hardware spec recommends
+' 4 or 5 and explicitly says to avoid 0-3 and 6-7.
+'
+' Untested on the port $E7 path as of this writing (the POKE 2057 path
+' was the one validated on real hardware) -- this library is meant to be
+' its first test. See examples/sd81/bounce_sd81.bas.
+
+' Turns the double buffer ON. frontBlk: 0-7, see notes above (4 or 5
+' recommended).
+sub fastcall SD81DBufOn(frontBlk as ubyte)
+ asm
+ ; A = frontBlk (fastcall)
+ and 7
+ or 32 ; bit5 = enable
+ ld b, a
+ ld a, 8 ; pseudo-block 8 (D3=1, D2:0=0)
+ ld c, $E7
+ out (c), a
+ end asm
+end sub
+
+' Turns the double buffer OFF.
+sub SD81DBufOff()
+ asm
+ ld b, 0
+ ld a, 8
+ ld c, $E7
+ out (c), a
+ end asm
+end sub
+
+' Waits for the rising edge of VSYNC (bit 0 of port $AF): if currently
+' inside the vsync pulse, waits for it to end, then waits for the next
+' one to start. This is the point right after the double buffer's
+' automatic blit finishes (the copy starts as soon as the visible area
+' ends), so it's the right place to erase/redraw when using SD81DBufOn.
+'
+' Note: this is a different wait than VSYNC_TICK/PAUSE (which just waits
+' for at least one pulse since the last read, using bits 6-1 as a pulse
+' counter) -- this one specifically detects the edge to align with the
+' double buffer's blit timing.
+sub SD81WaitVSync()
+ asm
+ PROC
+ LOCAL SDVS_INSIDE, SDVS_WAIT
+
+SDVS_INSIDE:
+ in a, ($AF)
+ rrca
+ jr c, SDVS_INSIDE ; still inside the vsync pulse: wait for it to end
+
+SDVS_WAIT:
+ in a, ($AF)
+ rrca
+ jr nc, SDVS_WAIT ; wait for the next pulse to start
+
+ ENDP
+ end asm
+end sub
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/academy.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/academy.fzx
new file mode 100644
index 000000000..a2a7f2756
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/academy.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/belegost1.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/belegost1.fzx
new file mode 100644
index 000000000..f77eaf27b
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/belegost1.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/belegost2.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/belegost2.fzx
new file mode 100644
index 000000000..08c1cde10
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/belegost2.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/bigbold.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/bigbold.fzx
new file mode 100644
index 000000000..3f5ce6556
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/bigbold.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/cobra.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/cobra.fzx
new file mode 100644
index 000000000..e56c75498
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/cobra.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/crash.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/crash.fzx
new file mode 100644
index 000000000..5ab6ba53f
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/crash.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/d_o_c.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/d_o_c.fzx
new file mode 100644
index 000000000..e67f4edfc
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/d_o_c.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/eclipse.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/eclipse.fzx
new file mode 100644
index 000000000..d6a3e4b2e
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/eclipse.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/extra.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/extra.fzx
new file mode 100644
index 000000000..d7961f77c
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/extra.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/hijack.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/hijack.fzx
new file mode 100644
index 000000000..e365183dd
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/hijack.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/italika.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/italika.fzx
new file mode 100644
index 000000000..a252f264b
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/italika.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/just6.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/just6.fzx
new file mode 100644
index 000000000..709689c25
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/just6.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/locomotion.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/locomotion.fzx
new file mode 100644
index 000000000..e22bc25de
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/locomotion.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/midnight.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/midnight.fzx
new file mode 100644
index 000000000..7c08a9637
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/midnight.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/moonalert.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/moonalert.fzx
new file mode 100644
index 000000000..ff4a59f02
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/moonalert.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/nether.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/nether.fzx
new file mode 100644
index 000000000..4045e4b4b
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/nether.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/neverend.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/neverend.fzx
new file mode 100644
index 000000000..1b598e042
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/neverend.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/roman.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/roman.fzx
new file mode 100644
index 000000000..965e28676
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/roman.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/script.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/script.fzx
new file mode 100644
index 000000000..411de3443
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/script.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/script2.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/script2.fzx
new file mode 100644
index 000000000..2ac2fb3b0
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/script2.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/standard.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/standard.fzx
new file mode 100644
index 000000000..0fd4024fa
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/standard.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/tomahawk.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/tomahawk.fzx
new file mode 100644
index 000000000..67592d045
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/tomahawk.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/ultrabold.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/ultrabold.fzx
new file mode 100644
index 000000000..f7fecd495
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/ultrabold.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/upcasebold.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/upcasebold.fzx
new file mode 100644
index 000000000..7ba37c2bf
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/upcasebold.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/wildvest.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/wildvest.fzx
new file mode 100644
index 000000000..3dc5c7880
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/wildvest.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/fzx_fonts/winter.fzx b/src/lib/arch/zx81sd/stdlib/fzx_fonts/winter.fzx
new file mode 100644
index 000000000..adc7e36d2
Binary files /dev/null and b/src/lib/arch/zx81sd/stdlib/fzx_fonts/winter.fzx differ
diff --git a/src/lib/arch/zx81sd/stdlib/input.bas b/src/lib/arch/zx81sd/stdlib/input.bas
new file mode 100644
index 000000000..13320a11c
--- /dev/null
+++ b/src/lib/arch/zx81sd/stdlib/input.bas
@@ -0,0 +1,197 @@
+' ----------------------------------------------------------------
+' This file is released under the MIT License
+'
+' Copyleft (k) 2008
+' by Jose Rodriguez-Rosa (a.k.a. Boriel)
+'
+' Simple INPUT routine (not as powerful as Sinclair BASIC's).
+' Usage: A$ = INPUT(MaxChars)
+'
+' Version zx81sd: la version de zx48k depende de la ROM del ZX
+' Spectrum (system var LAST_K actualizada por interrupcion, y la
+' rutina BEEPER de la ROM). En zx81sd no hay ROM mapeada en tiempo
+' de ejecucion y las interrupciones estan deshabilitadas (DI), asi
+' que no existe ningun mecanismo que actualice LAST_K por si solo:
+' el teclado se escanea directamente bajo demanda (ver
+' runtime/io/keyboard/keyscan.asm). El pitido de tecla usa el
+' beeper emulado del SD81 Booster en modo Spectrum (puerto ULA
+' $FBh, ver runtime/beep.asm), no la rutina BEEPER de la ROM.
+' ----------------------------------------------------------------
+#ifndef __LIBRARY_INPUT__
+
+REM Avoid recursive / multiple inclusion
+
+#define __LIBRARY_INPUT__
+
+#include once
+#include once
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = True
+
+' ------------------------------------------------------------------
+' Function 'PRIVATE' to this module.
+' Escanea el teclado hasta que se pulsa una tecla, y hasta que se
+' suelta antes de devolver su codigo ASCII (evita que una pulsacion
+' se registre varias veces mientras dura el bucle DO/LOOP).
+' ------------------------------------------------------------------
+FUNCTION FASTCALL PRIVATEInputWaitKey AS UBYTE
+ ASM
+ push namespace core
+ PROC
+ LOCAL WAIT_PRESS
+ LOCAL WAIT_RELEASE
+
+WAIT_PRESS:
+ call __ZX81SD_KEYSCAN
+ or a
+ jr z, WAIT_PRESS
+
+ ld b, a
+ push bc ; __ZX81SD_KEYCLICK usa B como contador de retardo
+ call __ZX81SD_KEYCLICK
+ pop bc
+
+WAIT_RELEASE:
+ push bc
+ call __ZX81SD_KEYSCAN
+ pop bc
+ or a
+ jr nz, WAIT_RELEASE
+
+ ld a, b
+ ENDP
+ pop namespace
+ END ASM
+END FUNCTION
+#require "io/keyboard/keyscan.asm"
+#require "beep.asm"
+
+' ------------------------------------------------------------------
+' Function 'PRIVATE' to this module.
+' Dado el codigo ASCII de una tecla normal (sin modificadores), da el
+' simbolo del ZX81 que le corresponde bajo el antiguo SHIFT del ZX81
+' (o 0 si no tiene). Ver runtime/io/keyboard/keyscan.asm: SHIFT se ha
+' redefinido para dar mayuscula, asi que esos simbolos ya no se pueden
+' teclear pulsando dos teclas a la vez (no da tiempo entre pulsaciones
+' humanas, sobre todo leyendo desde INPUT); en su lugar, aqui se pulsan
+' de manera secuencial: primero "." y luego la tecla del simbolo.
+' ------------------------------------------------------------------
+FUNCTION FASTCALL PRIVATEInputSymbolFor(ch AS UByte) AS UByte
+ ASM
+ push namespace core
+ call __ZX81SD_SYMBOL_FOR
+ pop namespace
+ END ASM
+END FUNCTION
+
+
+FUNCTION input(MaxLen AS UINTEGER) AS STRING
+ DIM result$ AS STRING
+ DIM i as UINTEGER
+ DIM LastK as UByte
+ DIM sym as UByte
+
+ result$ = ""
+
+ DO
+ PRIVATEInputShowCursor()
+
+ REM Espera a que se pulse una tecla y a que se suelte (debounce)
+ LastK = PRIVATEInputWaitKey()
+
+ PRIVATEInputHideCursor()
+
+ IF LastK = CODE(".") THEN
+ REM Posible simbolo compuesto: "." seguido de otra tecla,
+ REM pulsadas una detras de otra (no a la vez). Se lee la
+ REM siguiente tecla y se comprueba si tiene simbolo.
+ PRIVATEInputShowCursor()
+ LastK = PRIVATEInputWaitKey()
+ PRIVATEInputHideCursor()
+
+ IF LastK = CODE(".") THEN
+ REM Punto dos veces seguidas: confirma el primer punto
+ REM como literal y descarta el combo (la coma ya sale
+ REM directamente con SHIFT+"."). La segunda pulsacion
+ REM se consume sin mas: no abre un nuevo combo ni se
+ REM imprime por si misma, asi la tecla que venga a
+ REM continuacion se lee como una pulsacion nueva, sin
+ REM combinar - permite escribir un punto delante de una
+ REM letra que de otro modo formaria simbolo con ella.
+ IF LEN(result$) < MaxLen THEN
+ LET result$ = result$ + "."
+ PRINT ".";
+ END IF
+ LET LastK = 0
+ ELSE
+ REM sym=12 (RUBOUT, tecla "0") se excluye a proposito: ya
+ REM se alcanza comodamente con SHIFT+0, y aceptarlo aqui
+ REM haria que escribir un numero decimal terminado en
+ REM ".0" (muy habitual) borrase el caracter anterior en
+ REM vez de escribir el "0".
+ sym = PRIVATEInputSymbolFor(LastK)
+ IF sym <> 0 AND sym <> 12 THEN
+ LET LastK = sym
+ ELSEIF LEN(result$) < MaxLen THEN
+ LET result$ = result$ + "."
+ PRINT ".";
+ END IF
+ REM Si no habia simbolo valido, LastK sigue siendo la
+ REM segunda tecla real y se procesa a continuacion con
+ REM normalidad: DEL, ENTER, o un caracter mas.
+ END IF
+ END IF
+
+ IF LastK = 12 THEN
+ IF LEN(result$) THEN REM "Del" key code is 12
+ IF LEN(result$) = 1 THEN
+ LET result$ = ""
+ ELSE
+ LET result$ = result$( TO LEN(result$) - 2)
+ END IF
+ PRINT CHR$(8);
+ END IF
+ ELSEIF LastK >= CODE(" ") AND LEN(result$) < MaxLen THEN
+ LET result$ = result$ + CHR$(LastK)
+ PRINT CHR$(LastK);
+ END IF
+
+ LOOP UNTIL LastK = 13 : REM "Enter" key code is 13
+
+ FOR i = 1 TO LEN(result$):
+ PRINT OVER 0; CHR$(8) + " " + chr$(8);
+ NEXT
+
+ RETURN result$
+
+END FUNCTION
+
+#pragma pop(case_insensitive)
+
+' ------------------------------------------------------------------
+' Function 'PRIVATE' to this module.
+' Shows a flashing cursor
+' ------------------------------------------------------------------
+SUB PRIVATEInputShowCursor
+ REM Print a Flashing cursor at current print position
+ DIM x, y as UBYTE
+ y = csrlin()
+ x = pos()
+ PRINT AT y, x; OVER 0; FLASH 1; " "; AT y, x;
+END SUB
+
+
+' ------------------------------------------------------------------
+' Function 'PRIVATE' to this module.
+' Hides the flashing cursor
+' ------------------------------------------------------------------
+SUB PRIVATEInputHideCursor
+ REM Print a Flashing cursor at current print position
+ DIM x, y as UBYTE
+ y = csrlin()
+ x = pos()
+ PRINT AT y, x; OVER 0; FLASH 0; " "; AT y, x;
+END SUB
+
+#endif
diff --git a/src/lib/arch/zx81sd/stdlib/joy.bas b/src/lib/arch/zx81sd/stdlib/joy.bas
new file mode 100644
index 000000000..2e48f8425
--- /dev/null
+++ b/src/lib/arch/zx81sd/stdlib/joy.bas
@@ -0,0 +1,77 @@
+' ----------------------------------------------------------------------
+' joy.bas (zx81sd) — Configuracion del joystick programable del SD81
+'
+' El SD81 Booster mapea el joystick fisico a pulsaciones de teclado.
+' El mapeo se configura con el comando 21 ($15) del MCU, que recibe 5
+' codigos de tecla ZX81: arriba, abajo, izquierda, derecha y fuego.
+'
+' Uso:
+' #include
+' dim st as ubyte
+' st = Joy("QAOPM") ' arriba=Q abajo=A izq=O der=P fuego=M
+' st = Joy("7657 ") ' cursores de Sinclair (fuego=espacio)
+'
+' La cadena debe tener exactamente 5 caracteres, en el orden
+' ARRIBA, ABAJO, IZQUIERDA, DERECHA, FUEGO. Caracteres validos:
+' letras (A-Z, indistinto mayus/minus), digitos (0-9) y espacio.
+'
+' Devuelve el byte de estado del MCU: 0 = OK, 14 = parametro invalido.
+' Si la cadena es invalida (longitud o caracteres) devuelve 14 sin
+' llegar a llamar al MCU.
+' ----------------------------------------------------------------------
+
+#pragma once
+
+#include
+
+#pragma push(explicit)
+#pragma explicit = true
+
+const _JOY_CMD as ubyte = $15 ' comando 21 del MCU: configurar joystick
+
+' Convierte un caracter ASCII al codigo de tecla ZX81 que espera el MCU
+' (0 = espacio, 28-37 = digitos, 38-63 = letras).
+' Devuelve 255 si el caracter no corresponde a ninguna tecla valida.
+function _JoyKeyCode(c as ubyte) as ubyte
+ if c = code(" ") then
+ return 0
+ end if
+ if c >= code("0") and c <= code("9") then
+ return 28 + (c - code("0"))
+ end if
+ if c >= code("a") and c <= code("z") then
+ c = c - 32
+ end if
+ if c >= code("A") and c <= code("Z") then
+ return 38 + (c - code("A"))
+ end if
+ return 255
+end function
+
+' Configura el mapeo del joystick programable.
+' keys: 5 caracteres en orden ARRIBA, ABAJO, IZQUIERDA, DERECHA, FUEGO.
+' Devuelve el estado del MCU (0 = OK).
+function Joy(keys as string) as ubyte
+ dim i as ubyte
+ dim kc as ubyte
+ dim zxcodes as string
+
+ if len(keys) <> 5 then
+ return 14 ' mismo codigo que usa el MCU
+ end if
+
+ ' valida y convierte ANTES de tocar el MCU, para no dejar el
+ ' protocolo a medias si hay un caracter invalido
+ zxcodes = ""
+ for i = 0 to 4
+ kc = _JoyKeyCode(code(keys(i)))
+ if kc = 255 then
+ return 14
+ end if
+ zxcodes = zxcodes + chr(kc)
+ next i
+
+ return McuCmdStr(_JOY_CMD, zxcodes)
+end function
+
+#pragma pop(explicit)
diff --git a/src/lib/arch/zx81sd/stdlib/mcu.bas b/src/lib/arch/zx81sd/stdlib/mcu.bas
new file mode 100644
index 000000000..1a7df9966
--- /dev/null
+++ b/src/lib/arch/zx81sd/stdlib/mcu.bas
@@ -0,0 +1,904 @@
+' ----------------------------------------------------------------------
+' mcu.bas (zx81sd) — Comunicacion con el MCU del SD81 Booster
+'
+' Primitivas del protocolo + wrappers de los comandos del MCU.
+' Los comandos con nombre propio (joy.bas, etc.) se apoyan en esta
+' libreria.
+'
+' Protocolo (puertos $A7 datos / $AF reloj):
+' El bit 7 del puerto $AF es el reloj de sincronizacion: el MCU lo
+' invierte cuando ha procesado cada lectura/escritura del puerto de
+' datos $A7. Tras cada operacion en $A7 hay que esperar ese cambio
+' antes de la siguiente.
+'
+' ATENCION: escribir CUALQUIER valor en $AF resetea el MCU (peligroso
+' si esta escribiendo en la SD). Esta libreria solo LEE $AF.
+'
+' Las transferencias de datos estan implementadas en ensamblador (son
+' el camino critico de LOAD/SAVE/F_READ/F_WRITE); el BASIC solo prepara
+' parametros y convierte codificaciones.
+'
+' Codificacion: los comandos de fichero/texto viajan en codigos de
+' caracter ZX81 (el MCU los convierte internamente). Esta libreria
+' convierte automaticamente desde ASCII. Excepciones ya en crudo:
+' F_OPEN (ASCII), BINARY_SAY (alofonos) y JOY (codigos de tecla).
+'
+' Convencion de resultados: las funciones devuelven el byte de estado
+' del MCU (0 = exito; ver tabla de errores del manual). Las que
+' devuelven datos (McuLoad, McuPwd, ...) dejan el estado en la variable
+' global McuStatus.
+' ----------------------------------------------------------------------
+
+#pragma once
+
+#pragma push(explicit)
+#pragma explicit = true
+
+' Estado devuelto por el ultimo comando "con datos" (McuLoad, McuPwd...)
+dim McuStatus as ubyte
+
+' Resultado de McuFree() en KB
+dim McuFreeTotalKb as ulong
+dim McuFreeFreeKb as ulong
+
+' ======================================================================
+' PRIMITIVAS DEL PROTOCOLO (ASM)
+' ======================================================================
+
+' Envia un byte al MCU y espera a que lo procese.
+sub fastcall McuSend(value as ubyte)
+ asm
+ PROC
+ LOCAL WAIT
+ ; A = value (convencion fastcall para ubyte)
+ push bc
+ ld b, a ; salva el dato
+ in a, ($AF)
+ ld c, a ; C = estado previo del reloj (bit 7)
+ ld a, b
+ out ($A7), a ; envia
+ WAIT:
+ in a, ($AF)
+ xor c
+ jp p, WAIT ; espera a que el bit 7 cambie
+ pop bc
+ ENDP
+ end asm
+end sub
+
+' Lee un byte del MCU (respuesta o status) y espera el toggle posterior.
+function fastcall McuRecv() as ubyte
+ asm
+ PROC
+ LOCAL WAIT
+ push bc
+ in a, ($AF)
+ ld c, a ; reloj previo
+ in a, ($A7) ; lee el dato (provoca el toggle del MCU)
+ ld b, a
+ WAIT:
+ in a, ($AF)
+ xor c
+ jp p, WAIT
+ ld a, b ; devuelve el dato en A (fastcall ubyte)
+ pop bc
+ ENDP
+ end asm
+end function
+
+' Envia un bloque de memoria al MCU (un byte por transaccion de reloj).
+' Bucle integramente en ASM: camino critico de SAVE / F_WRITE.
+sub fastcall McuSendBlock(addr as uinteger, size as uinteger)
+ asm
+ PROC
+ LOCAL LOOP, WAIT, DONE
+ ; HL = addr (fastcall); pila: [ret][size]
+ pop de ; DE = direccion de retorno
+ pop bc ; BC = size
+ push de ; restaura el retorno
+
+ ld a, b
+ or c
+ jr z, DONE
+ LOOP:
+ in a, ($AF)
+ ld e, a ; E = reloj previo
+ ld a, (hl)
+ out ($A7), a
+ WAIT:
+ in a, ($AF)
+ xor e
+ jp p, WAIT
+ inc hl
+ dec bc
+ ld a, b
+ or c
+ jr nz, LOOP
+ DONE:
+ ENDP
+ end asm
+end sub
+
+' Recibe un bloque del MCU en memoria (un byte por transaccion de reloj).
+' Bucle integramente en ASM: camino critico de LOAD / F_READ.
+sub fastcall McuRecvBlock(addr as uinteger, size as uinteger)
+ asm
+ PROC
+ LOCAL LOOP, WAIT, DONE
+ ; HL = addr (fastcall); pila: [ret][size]
+ pop de
+ pop bc
+ push de
+
+ ld a, b
+ or c
+ jr z, DONE
+ LOOP:
+ in a, ($AF)
+ ld e, a ; reloj previo
+ in a, ($A7) ; lee dato
+ ld (hl), a
+ WAIT:
+ in a, ($AF)
+ xor e
+ jp p, WAIT
+ inc hl
+ dec bc
+ ld a, b
+ or c
+ jr nz, LOOP
+ DONE:
+ ENDP
+ end asm
+end sub
+
+' ======================================================================
+' CONVERSION ASCII <-> CODIGOS DE CARACTER ZX81
+' ======================================================================
+
+' ASCII -> codigo ZX81. Caracteres sin equivalente -> '?' ($0F).
+function _McuToZx(c as ubyte) as ubyte
+ if c >= 97 and c <= 122 then
+ c = c - 32 ' minusculas a mayusculas
+ end if
+ if c >= 65 and c <= 90 then
+ return 38 + (c - 65) ' A-Z
+ end if
+ if c >= 48 and c <= 57 then
+ return 28 + (c - 48) ' 0-9
+ end if
+ if c = 32 then return 0 ' espacio
+ if c = 34 then return 11 ' "
+ if c = 36 then return 13 ' $
+ if c = 58 then return 14 ' :
+ if c = 63 then return 15 ' ?
+ if c = 40 then return 16 ' (
+ if c = 41 then return 17 ' )
+ if c = 62 then return 18 ' >
+ if c = 60 then return 19 ' <
+ if c = 61 then return 20 ' =
+ if c = 43 then return 21 ' +
+ if c = 45 then return 22 ' -
+ if c = 42 then return 23 ' *
+ if c = 47 then return 24 ' /
+ if c = 59 then return 25 ' ;
+ if c = 44 then return 26 ' ,
+ if c = 46 then return 27 ' .
+ return 15 ' resto: '?'
+end function
+
+' Codigo ZX81 -> ASCII (ignora el bit 7 de video inverso).
+function _McuFromZx(z as ubyte) as ubyte
+ z = z band 127
+ if z >= 38 and z <= 63 then
+ return 65 + (z - 38) ' A-Z
+ end if
+ if z >= 28 and z <= 37 then
+ return 48 + (z - 28) ' 0-9
+ end if
+ if z = 0 then return 32 ' espacio
+ if z = 11 then return 34 ' "
+ if z = 13 then return 36 ' $
+ if z = 14 then return 58 ' :
+ if z = 15 then return 63 ' ?
+ if z = 16 then return 40 ' (
+ if z = 17 then return 41 ' )
+ if z = 18 then return 62 ' >
+ if z = 19 then return 60 ' <
+ if z = 20 then return 61 ' =
+ if z = 21 then return 43 ' +
+ if z = 22 then return 45 ' -
+ if z = 23 then return 42 ' *
+ if z = 24 then return 47 ' /
+ if z = 25 then return 59 ' ;
+ if z = 26 then return 44 ' ,
+ if z = 27 then return 46 ' .
+ return 63 ' resto: '?'
+end function
+
+' Convierte una cadena ASCII completa a codigos ZX81.
+function McuZxStr(s as string) as string
+ dim i as uinteger
+ dim r as string
+ r = ""
+ for i = 0 to len(s) - 1
+ r = r + chr(_McuToZx(code(s(i))))
+ next i
+ return r
+end function
+
+' ======================================================================
+' PATRONES GENERICOS
+' ======================================================================
+
+' Envia una cadena Pascal (longitud + bytes crudos, max 255).
+sub _McuSendPascal(dat as string)
+ dim addr as uinteger
+ dim l as uinteger
+ addr = peek(uinteger, @dat)
+ l = peek(uinteger, addr)
+ if l > 255 then l = 255
+ McuSend(cast(ubyte, l))
+ if l > 0 then
+ McuSendBlock(addr + 2, l)
+ end if
+end sub
+
+' Comando sin parametros ni respuesta.
+sub McuCmd(cmd as ubyte)
+ McuSend(cmd)
+end sub
+
+' Comando + cadena en crudo + status.
+function McuCmdStr(cmd as ubyte, dat as string) as ubyte
+ McuSend(cmd)
+ _McuSendPascal(dat)
+ return McuRecv()
+end function
+
+' Comando + cadena (convertida a ZX81) + status. Para comandos de
+' fichero/texto (CD, DEL, LOAD, SAY...).
+function McuCmdStrZx(cmd as ubyte, dat as string) as ubyte
+ return McuCmdStr(cmd, McuZxStr(dat))
+end function
+
+' Recibe un stream de caracteres (protocolo NEXTCH $0D hasta EOT $6F)
+' y lo devuelve como string ASCII. $76 (newline ZX81) -> CHR(13).
+' Deja el status en McuStatus. Para respuestas cortas (PWD, RTC...).
+function _McuRecvStream() as string
+ dim c as ubyte
+ dim r as string
+ r = ""
+ do
+ McuSend($0D) ' CMD_NEXTCH: pide un caracter
+ c = McuRecv()
+ if c = $6F then ' EOT
+ exit do
+ end if
+ if c = $76 then
+ r = r + chr(13) ' nueva linea ZX81
+ else
+ r = r + chr(_McuFromZx(c))
+ end if
+ loop
+ McuStatus = McuRecv() ' byte de estado final
+ return r
+end function
+
+' Recibe un stream y lo imprime linea a linea (para listados largos:
+' DIR, TYPE, FREE_TXT). Devuelve el status.
+function _McuStreamPrint(newLine as ubyte) as ubyte
+ dim c as ubyte
+ dim line as string
+ line = ""
+ do
+ McuSend($0D)
+ c = McuRecv()
+ if c = $6F then
+ exit do
+ end if
+ if c = $76 then
+ ' print
+ print line;
+ if len(line) <> 32 then print
+ line = ""
+ else
+ line = line + chr(_McuFromZx(c))
+ 'print c; " ";
+ end if
+ loop
+ if len(line) > 0 then
+ if newLine=1 then
+ print line
+ else
+ print line;
+ end if
+ end if
+ return McuRecv()
+end function
+
+' ======================================================================
+' COMANDOS DE SISTEMA
+' ======================================================================
+
+' Cmd 0: sin operacion (sincroniza el reloj).
+sub McuNop()
+ McuCmd(0)
+end sub
+
+' Cmd 1: version del firmware del MCU.
+function McuVersion() as ubyte
+ McuSend(1)
+ return McuRecv()
+end function
+
+' Cmd 32: lee un byte de la memoria interna del MCU.
+' Indices 0-127: variables volatiles. 128-255: EEPROM (persistente).
+function McuGetByte(index as ubyte) as ubyte
+ McuSend(32)
+ McuSend(index)
+ return McuRecv()
+end function
+
+' Cmd 33: escribe un byte en la memoria interna del MCU.
+sub McuSetByte(index as ubyte, value as ubyte)
+ McuSend(33)
+ McuSend(index)
+ McuSend(value)
+end sub
+
+' ======================================================================
+' COMANDOS DE SISTEMA DE ARCHIVOS
+' ======================================================================
+
+' Cmd 2: directorio actual. Status en McuStatus.
+function McuPwd() as string
+ McuSend(2)
+ return _McuRecvStream()
+end function
+
+' Cmd 3: cambia de directorio (rutas absolutas con / o relativas).
+function McuCd(path as string) as ubyte
+ return McuCmdStrZx(3, path)
+end function
+
+' Cmd 4: borra un archivo del directorio actual.
+function McuDel(fname as string) as ubyte
+ return McuCmdStrZx(4, fname)
+end function
+
+' Cmd 5: crea un subdirectorio.
+function McuMkdir(dname as string) as ubyte
+ return McuCmdStrZx(5, dname)
+end function
+
+' Cmd 6: elimina un directorio vacio.
+function McuRmdir(dname as string) as ubyte
+ return McuCmdStrZx(6, dname)
+end function
+
+' Cmd 7/8: renombra-mueve / copia un archivo (dos cadenas seguidas).
+function _McuTwoStr(cmd as ubyte, src as string, dst as string) as ubyte
+ McuSend(cmd)
+ _McuSendPascal(McuZxStr(src))
+ _McuSendPascal(McuZxStr(dst))
+ return McuRecv()
+end function
+
+function McuMove(src as string, dst as string) as ubyte
+ return _McuTwoStr(7, src, dst)
+end function
+
+function McuCopy(src as string, dst as string) as ubyte
+ return _McuTwoStr(8, src, dst)
+end function
+
+' Cmd 9: carga un archivo de la SD en memoria a partir de addr.
+' Devuelve el numero de bytes cargados (0 si error); status en McuStatus.
+' Ojo: extensiones especiales — .ROM resetea la CPU (no retorna),
+' .WAV se reproduce (devuelve 0 bytes).
+function McuLoad(fname as string, addr as uinteger) as uinteger
+ dim lo as ubyte
+ dim hi as ubyte
+ dim size as uinteger
+
+ McuSend(9)
+ _McuSendPascal(McuZxStr(fname))
+ lo = McuRecv()
+ hi = McuRecv()
+ size = cast(uinteger, hi) * 256 + lo
+ if size > 0 then
+ McuRecvBlock(addr, size)
+ end if
+ McuStatus = McuRecv()
+ return size
+end function
+
+' Cmd 10: guarda un bloque de memoria como archivo en la SD.
+function McuSave(fname as string, addr as uinteger, size as uinteger) as ubyte
+ McuSend(10)
+ _McuSendPascal(McuZxStr(fname))
+ McuSend(cast(ubyte, size band 255))
+ McuSend(cast(ubyte, size >> 8))
+ if size > 0 then
+ McuSendBlock(addr, size)
+ end if
+ return McuRecv()
+end function
+
+' Cmd 11: imprime el contenido de un archivo de texto.
+' Si el nombre empieza por '*' busca en /MAN/ con extension .TXT.
+function McuTypePrint(fname as string) as ubyte
+ McuSend(11)
+ _McuSendPascal(McuZxStr(fname))
+ return _McuStreamPrint(1)
+end function
+
+' Cmd 12: imprime el listado de un directorio (admite comodines).
+function McuDirPrint(mask as string) as ubyte
+ McuSend(12)
+ _McuSendPascal(McuZxStr(mask))
+ return _McuStreamPrint(0)
+end function
+
+' Cmd 14: imprime el espacio total/libre de la SD como texto.
+function McuFreeTxtPrint() as ubyte
+ McuSend(14)
+ return _McuStreamPrint(1)
+end function
+
+' Cmd 15: espacio total y libre en KB -> McuFreeTotalKb / McuFreeFreeKb.
+' Devuelve el status.
+function McuFree() as ubyte
+ dim i as ubyte
+ dim v as ulong
+
+ McuSend(15)
+ v = 0
+ for i = 0 to 3
+ v = v bor (cast(ulong, McuRecv()) << (i * 8))
+ next i
+ McuFreeTotalKb = v
+ v = 0
+ for i = 0 to 3
+ v = v bor (cast(ulong, McuRecv()) << (i * 8))
+ next i
+ McuFreeFreeKb = v
+ return McuRecv()
+end function
+
+' Cmds 16/17/18: OPENDIR / GETROWLEN / GETROW — navegador de directorio.
+' ATENCION: no emulados por EightyOne a fecha de hoy (solo HW real);
+' en el emulador se quedarian esperando la respuesta.
+function McuOpenDir(mask as string) as ubyte
+ return McuCmdStrZx(16, mask)
+end function
+
+' Devuelve la longitud del nombre de la entrada index; status en McuStatus.
+function McuGetRowLen(index as uinteger) as ubyte
+ dim l as ubyte
+ McuSend(17)
+ McuSend(cast(ubyte, index band 255))
+ McuSend(cast(ubyte, index >> 8))
+ l = McuRecv()
+ McuStatus = McuRecv()
+ return l
+end function
+
+' Devuelve el nombre de la entrada index (0 = directorio actual);
+' los directorios van entre < y >. Status en McuStatus.
+function McuGetRow(index as uinteger) as string
+ dim l as ubyte
+ dim i as ubyte
+ dim r as string
+ McuSend(18)
+ McuSend(cast(ubyte, index band 255))
+ McuSend(cast(ubyte, index >> 8))
+ l = McuRecv()
+ r = ""
+ if l > 0 then
+ for i = 1 to l
+ r = r + chr(_McuFromZx(McuRecv()))
+ next i
+ end if
+ McuStatus = McuRecv()
+ return r
+end function
+
+' ======================================================================
+' ACCESO A FICHEROS GRANDES (handles 0-3, tamano 32 bits)
+' ======================================================================
+
+' Cmd 53: abre un fichero EXISTENTE para acceso aleatorio. El MCU asigna
+' el primer handle libre y lo devuelve (0-3, o $FF si error/no existe).
+' El nombre viaja en ASCII crudo (sin conversion; es el fopen de CP/M).
+function McuFOpen(fname as string) as ubyte
+ McuSend(53)
+ _McuSendPascal(fname) ' ASCII directo, sin conversion
+ return McuRecv()
+end function
+
+' Cmd 58: igual que McuFOpen pero el nombre viaja en codigos ZX81
+' (la conversion desde ASCII la hace esta funcion).
+function McuFOpenZx(fname as string) as ubyte
+ McuSend(58)
+ _McuSendPascal(McuZxStr(fname))
+ return McuRecv()
+end function
+
+' Cmd 54: situa el puntero de lectura/escritura (offset 32 bits LE).
+function McuFSeek(handle as ubyte, offset as ulong) as ubyte
+ dim i as ubyte
+ McuSend(54)
+ McuSend(handle)
+ for i = 0 to 3
+ McuSend(cast(ubyte, (offset >> (i * 8)) band 255))
+ next i
+ return McuRecv()
+end function
+
+' Cmd 55: lee count bytes al buffer addr. Si el fichero se acaba,
+' rellena con ceros (status 1 = lectura corta/EOF).
+function McuFRead(handle as ubyte, addr as uinteger, count as uinteger) as ubyte
+ McuSend(55)
+ McuSend(handle)
+ McuSend(cast(ubyte, count band 255))
+ McuSend(cast(ubyte, count >> 8))
+ if count > 0 then
+ McuRecvBlock(addr, count)
+ end if
+ return McuRecv()
+end function
+
+' Cmd 56: escribe count bytes desde addr.
+function McuFWrite(handle as ubyte, addr as uinteger, count as uinteger) as ubyte
+ McuSend(56)
+ McuSend(handle)
+ McuSend(cast(ubyte, count band 255))
+ McuSend(cast(ubyte, count >> 8))
+ if count > 0 then
+ McuSendBlock(addr, count)
+ end if
+ return McuRecv()
+end function
+
+' Cmd 57: cierra el fichero.
+function McuFClose(handle as ubyte) as ubyte
+ McuSend(57)
+ McuSend(handle)
+ return McuRecv()
+end function
+
+' ======================================================================
+' CONTROL DEL HARDWARE
+' ======================================================================
+' ATENCION: los cambios de paginacion/RAM pueden dejar sin sentido el
+' mapa de memoria del programa en ejecucion. Usar solo sabiendo lo que
+' se hace.
+
+' Mapeador de memoria del SD81 (puerto $E7, directo a la FPGA — no pasa
+' por el MCU). Equivale al LOAD *MAP bloque,pagina del BASIC.
+'
+' El espacio de 64K se divide en 8 bloques de 8K (bloque 0 = $0000-$1FFF
+' ... bloque 7 = $E000-$FFFF); cada bloque puede apuntar a cualquier
+' pagina fisica de 8K de la RAM de 512K (0-31 en paginacion simple,
+' 0-63 en paginacion completa — ver McuFullPaging/McuHalfPaging).
+'
+' La escritura vale para AMBOS modos: pone la pagina en B (A8-A13, que
+' es de donde la lee el modo completo) Y en los bits 7-3 del dato (de
+' donde la lee el modo simple).
+'
+' CUIDADO: remapear un bloque donde vive el propio programa, las
+' sysvars ($8000, bloque 4) o la pantalla ($C000, bloque 6) cuelga o
+' corrompe el sistema.
+sub fastcall Map(block as ubyte, page as ubyte)
+ asm
+ ; A = block (fastcall); pila: [ret][page en byte alto]
+ pop hl ; direccion de retorno
+ pop de ; D = page
+ push hl
+ and 7
+ ld l, a ; L = bloque (0-7)
+ ld b, d ; B = pagina completa (aparece en A8-A15)
+ ld c, $E7
+ ld a, d
+ and 31
+ rlca
+ rlca
+ rlca ; (pagina AND 31) << 3
+ or l ; dato = pagina<<3 | bloque (modo simple)
+ out (c), a ; una sola escritura vale para ambos modos
+ end asm
+end sub
+
+' Lee la pagina actualmente asignada a un bloque (lectura de $E7 con
+' el bloque en A10-A8).
+function fastcall MapGet(block as ubyte) as ubyte
+ asm
+ ; A = block (fastcall)
+ and 7
+ ld b, a ; B aparece en A8-A15 durante IN A,(C)
+ ld c, $E7
+ in a, (c) ; A = pagina asignada (resultado fastcall)
+ end asm
+end function
+
+sub McuEnableMc45()
+ McuCmd(19)
+end sub
+
+sub McuDisableMc45()
+ McuCmd(20)
+end sub
+
+sub McuSel128Chars()
+ McuCmd(27)
+end sub
+
+sub McuSel64Chars()
+ McuCmd(28)
+end sub
+
+sub McuFullPaging()
+ McuCmd(29)
+end sub
+
+sub McuHalfPaging()
+ McuCmd(30)
+end sub
+
+sub McuRam48On()
+ McuCmd(48)
+end sub
+
+sub McuRam48Off()
+ McuCmd(49)
+end sub
+
+' ======================================================================
+' SINTESIS DE VOZ
+' ======================================================================
+
+' Cmd 23: convierte texto a fonemas y lo reproduce.
+' Si el primer caracter es '*', reproduce en background.
+function McuSay(text as string) as ubyte
+ return McuCmdStrZx(23, text)
+end function
+
+' Cmd 22: reproduce alofonos SP0256-AL2 en crudo (codigos $00-$3F).
+' Sincrono: bloquea hasta terminar.
+function McuBinarySay(allophones as string) as ubyte
+ return McuCmdStr(22, allophones)
+end function
+
+' ======================================================================
+' AY POR MCU (chip 2: el AY "B" del SD81) Y PLAY EN BACKGROUND
+' ======================================================================
+
+' Cmd 24: escribe un registro del AY del MCU (chip 2). Sin status.
+sub McuAySetReg(reg as ubyte, value as ubyte)
+ McuSend(24)
+ McuSend(reg)
+ McuSend(value)
+end sub
+
+' Cmd 25: lee un registro del AY del MCU (chip 2).
+function McuAyGetReg(reg as ubyte) as ubyte
+ McuSend(25)
+ McuSend(reg)
+ return McuRecv()
+end function
+
+' Convierte una cadena MML de PLAY conservando la semantica de octavas
+' del interprete del MCU: minuscula ASCII -> letra ZX81 normal
+' (octava base - 1), MAYUSCULA ASCII -> letra ZX81 en video inverso
+' (octava base). Igual que el PLAY local de play.bas.
+function _McuPlayStr(s as string) as string
+ dim i as uinteger
+ dim c as ubyte
+ dim r as string
+ r = ""
+ for i = 0 to len(s) - 1
+ c = code(s(i))
+ if c >= 65 and c <= 90 then
+ r = r + chr(128 + 38 + (c - 65)) ' mayuscula: inverso
+ else
+ r = r + chr(_McuToZx(c))
+ end if
+ next i
+ return r
+end function
+
+' Cmd 26: PLAY de hasta 3 canales en el AY del MCU (chip 2).
+' Si el canal A empieza por '*', reproduce en background.
+function McuAyPlay(chanA as string, chanB as string, chanC as string) as ubyte
+ McuSend(26)
+ _McuSendPascal(_McuPlayStr(chanA))
+ _McuSendPascal(_McuPlayStr(chanB))
+ _McuSendPascal(_McuPlayStr(chanC))
+ return McuRecv()
+end function
+
+' ======================================================================
+' REPRODUCTOR VGM
+' ======================================================================
+
+' Cmd 34: abre un archivo VGM (anade .vgm si no tiene extension) y lo
+' prepara; iniciar con McuContVgm().
+function McuPlayVgm(fname as string) as ubyte
+ return McuCmdStrZx(34, fname)
+end function
+
+sub McuStopVgm()
+ McuCmd(35)
+end sub
+
+sub McuPauseVgm()
+ McuCmd(36)
+end sub
+
+sub McuContVgm()
+ McuCmd(37)
+end sub
+
+' Cmd 38: modo de bucle (0 = no, 1 = si). Sin status.
+sub McuLoopVgm(mode as ubyte)
+ McuSend(38)
+ McuSend(mode)
+end sub
+
+' ======================================================================
+' PEG (Programmable Effects Generator)
+' ======================================================================
+
+' Cmd 40: carga instrucciones en la memoria PEG a partir de address.
+' dat contiene los bytes crudos (2 bytes little-endian por instruccion).
+' Sin status.
+sub McuLoadPeg(address as ubyte, dat as string)
+ McuSend(40)
+ McuSend(address)
+ _McuSendPascal(dat)
+end sub
+
+' Cmd 41: arranca un hilo PEG (0-2) en la direccion dada. Sin status.
+sub McuPlayPeg(thread as ubyte, address as ubyte)
+ McuSend(41)
+ McuSend(thread)
+ McuSend(address)
+end sub
+
+sub McuStopPeg(thread as ubyte)
+ McuSend(42)
+ McuSend(thread)
+end sub
+
+sub McuPausePeg(thread as ubyte)
+ McuSend(43)
+ McuSend(thread)
+end sub
+
+sub McuContPeg(thread as ubyte)
+ McuSend(44)
+ McuSend(thread)
+end sub
+
+' Cmd 45: carga un archivo .PEB de la SD en la memoria PEG (max 512B).
+function McuSdLoadPeg(fname as string, address as ubyte) as ubyte
+ McuSend(45)
+ _McuSendPascal(McuZxStr(fname))
+ McuSend(address)
+ return McuRecv()
+end function
+
+' ======================================================================
+' RTC Y BATERIA
+' ======================================================================
+
+' Cmd 50 (lectura): devuelve "AAAA-MM-DD HH:MM:SS.CC" (22 caracteres).
+' Status en McuStatus.
+function McuRtc() as string
+ dim i as ubyte
+ dim r as string
+ McuSend(50)
+ McuSend(0) ' cadena vacia = lectura
+ r = ""
+ for i = 1 to 22
+ r = r + chr(_McuFromZx(McuRecv()))
+ next i
+ McuStatus = McuRecv()
+ return r
+end function
+
+' Cmd 50 (escritura): ajusta el reloj. Formatos: "AAAA-MM-DD HH:MM:SS",
+' "AAAA-MM-DD", "HH:MM:SS", "HH:MM", etc.
+function McuRtcSet(datetime as string) as ubyte
+ return McuCmdStrZx(50, datetime)
+end function
+
+' Cmd 52: nivel de bateria del RTC como texto "V.mmm" (5 caracteres).
+' Status en McuStatus.
+function McuBat() as string
+ dim i as ubyte
+ dim r as string
+ McuSend(52)
+ r = ""
+ for i = 1 to 5
+ r = r + chr(_McuFromZx(McuRecv()))
+ next i
+ McuStatus = McuRecv()
+ return r
+end function
+
+' ======================================================================
+' UTILIDADES (equivalentes de las extensiones BASIC del SD81)
+' ======================================================================
+' Para *LDIR/*LDDR usar MemMove de (stdlib estandar), que
+' ademas elige el sentido de copia correcto cuando hay solapamiento.
+' Para *OUT/*IN y los PEEK/POKE de 16 bits, zxbasic ya tiene OUT, IN(),
+' peek(uinteger, ...) y poke uinteger nativos.
+
+' Valor de un digito hexadecimal ASCII, o 255 si no lo es.
+function _HexDigit(c as ubyte) as ubyte
+ if c >= 48 and c <= 57 then
+ return c - 48 ' 0-9
+ end if
+ if c >= 65 and c <= 70 then
+ return c - 55 ' A-F
+ end if
+ if c >= 97 and c <= 102 then
+ return c - 87 ' a-f
+ end if
+ return 255
+end function
+
+' Equivalente de LOAD *HEX: vuelca una cadena hexadecimal en memoria
+' ("0A014020" -> bytes $0A,$01,$40,$20 a partir de addr).
+' Devuelve el numero de bytes escritos (se detiene en el primer
+' caracter no hexadecimal o si la cadena tiene longitud impar).
+function HexPoke(addr as uinteger, hx as string) as uinteger
+ dim i as uinteger
+ dim n as uinteger
+ dim hi as ubyte
+ dim lo as ubyte
+
+ n = 0
+ i = 0
+ do while i + 2 <= len(hx)
+ hi = _HexDigit(code(hx(i)))
+ lo = _HexDigit(code(hx(i + 1)))
+ if hi = 255 or lo = 255 then
+ exit do
+ end if
+ poke addr + n, (hi << 4) bor lo
+ n = n + 1
+ i = i + 2
+ loop
+ return n
+end function
+
+' Equivalente de LOAD *INV: invierte el bit 7 de todos los caracteres.
+' Nota: en zxbasic el video inverso al imprimir se hace con PRINT
+' INVERSE; esto sirve para preparar buffers en codificacion ZX81
+' (p.ej. cadenas para McuAyPlay o para escribir en pantalla nativa).
+function StrInv(s as string) as string
+ dim i as uinteger
+ dim r as string
+ r = ""
+ for i = 0 to len(s) - 1
+ r = r + chr(code(s(i)) bxor 128)
+ next i
+ return r
+end function
+
+' Equivalente de LOAD *BOLD: fuerza a 1 el bit 7 de todos los caracteres.
+function StrBold(s as string) as string
+ dim i as uinteger
+ dim r as string
+ r = ""
+ for i = 0 to len(s) - 1
+ r = r + chr(code(s(i)) bor 128)
+ next i
+ return r
+end function
+
+#pragma pop(explicit)
diff --git a/src/lib/arch/zx81sd/stdlib/play.bas b/src/lib/arch/zx81sd/stdlib/play.bas
new file mode 100644
index 000000000..84eab2546
--- /dev/null
+++ b/src/lib/arch/zx81sd/stdlib/play.bas
@@ -0,0 +1,510 @@
+' ----------------------------------------------------------------------
+' play.bas (zx81sd) — COPIA de zx48k/stdlib/play.bas adaptada al hardware
+' AY del SD81 Booster (interfaz ZonX-81). Mantener sincronizada con el
+' original de zx48k; los UNICOS cambios respecto a el son:
+'
+' 1. _PLAY_WRITE_TO_REGISTER: puertos ZonX del chip A (latch $CF,
+' dato $0F) en vez de los del Spectrum 128 ($FFFD/$BFFD).
+' 2. _Play_NoteDividers: tabla regenerada para reloj AY de 1.625 MHz
+' (la FPGA del SD81 clockea los AY a 3.25MHz/2) en vez de los
+' 1.7734 MHz del Spectrum 128. divisor = round(1625000/16/f).
+' 3. CpuCyclesPerSecond: 3250000 (ZX81) en vez de 3546900 (128K),
+' para que el tempo sea correcto.
+' 4. Sin EI al terminar: el runtime zx81sd funciona con las
+' interrupciones deshabilitadas (la FPGA genera el video).
+' ----------------------------------------------------------------------
+
+' ----------------------------------------------------------------------
+' This file is released under the MIT License
+'
+' Copyright (C) 2026
+' by Oleg S. Kostenko (a.k.a. Ollibony)
+' ----------------------------------------------------------------------
+
+#pragma once
+
+#pragma push(explicit)
+#pragma push(strict)
+
+#pragma explicit = true
+#pragma strict = true
+
+' ---------------------------------------------------------------------------------------------------------------------
+' Plays the given MML strings on the AY music chip.
+' The syntax is compatible with the Sinclair Basic Play routine.
+' The documentation can be found here: https://fizyka.umk.pl/~jacek/zx/doc/man128/sp128p09.html
+'
+' This is work in progress.
+
+' The following commands are already implemented:
+' - cdefgab, CDEFGAB - gives pitch of note within current octave range
+' - $ - flattens note following it
+' - # - sharpens note following it
+' - & - denotes a rest
+' - 1-12 - sets length of notes
+' - _ - creates tied notes (sums multiple durations)
+' - O - followed by a number 0 to 8 sets current octave range
+' - V - followed by a number 0 to 15 sets volume of notes
+' - T - followed by a number 60 to 240 sets tempo of music
+' - N - separates two numbers (actually, any unexpected character does this, including space)
+'
+' The following commands are not implemented yet:
+' - W, U, X - set volume effects
+' - () - repetition
+' - !! - comments
+' - H - stop
+' - M - channel mixer control
+' - Y, Z - MIDI control (also you can't now pass more than 3 parameters to Play).
+'
+' Notes:
+' - Unlike Sinclair Basic Play routine, this one doesn't insert tiny pauses between adjacent notes.
+' I consider this to be a feature, rather than a bug.
+' - There are no checks for incorrect commands or parameters. Unknown commands are silently ignored,
+' and incorrect parameters cause undefined behavior.
+' - This routine is more flexible in the way it parses commands than Sinclair Basic Play routine.
+' Some combinations that give errors in Sinclair Basic, will play fine in this implementation.
+' - This sub tends to provide more accurate timings than the original Sinclair Basic Play routine.
+' However, perfect timing is not guaranteed, it may fluctuate depending on the complexity of the melody.
+' - There can be subtle difference in behaviour between this sub and Sinclair Play,
+' especially in undocumented edge cases (such as using ties together with triplets).
+' - This sub disables interrupts at the start, and enables them in the end,
+' regardless of whether they were enabled or not before.
+' - The strings are passed by value and thus are copied on the routine invocation.
+' The memory-effective version of this routine is yet to be implemented.
+' - The compiler gives warning `[W150] Parameter 'microticks' is never used`.
+' This is false positive and, unfortunately, cannot be suppressed on library level.
+' ---------------------------------------------------------------------------------------------------------------------
+declare sub Play(channel0 as string, channel1 as string = "", channel2 as string = "")
+
+
+' Implementation ------------------------------------------------------------------------------------------------------
+
+' How many ticks there are in a bar (a whole note).
+' A tick is a single iteration of the main processing loop.
+const _Play_TicksPerBar as ubyte = 96
+
+const _Play_NotesPerOctave as ubyte = 12
+const _Play_TotalOctaves as ubyte = 9
+
+' Maps note length to the corresponding number of ticks.
+dim _Play_NoteLengthsInTicks(1 to 12) as ubyte => { _
+ _Play_TicksPerBar / 16, _ '1 - semi-quaver
+ _Play_TicksPerBar / 16 * 1.5, _ '2 - dotted semi-quaver
+ _Play_TicksPerBar / 8, _ '3 - quaver
+ _Play_TicksPerBar / 8 * 1.5, _ '4 - dotted quaver
+ _Play_TicksPerBar / 4, _ '5 - crotchet
+ _Play_TicksPerBar / 4 * 1.5, _ '6 - dotted crotchet
+ _Play_TicksPerBar / 2, _ '7 - minim
+ _Play_TicksPerBar / 2 * 1.5, _ '8 - dotted minim
+ _Play_TicksPerBar, _ '9 - semi-breve
+ _Play_TicksPerBar / 24, _ '10 - triplet semi-quaver
+ _Play_TicksPerBar / 12, _ '11 - triplet quaver
+ _Play_TicksPerBar / 6 _ '12 - triplet crotchet
+}
+
+' Divider values that need to be sent to the audio chip registers to play the notes.
+' Note that the lowest notes in octave 0 are unplayable because of 12-bit overflow and probably wrong notes will be
+' played instead of them.
+' TODO: replace them with maximum possible values or zeros? See how it's done in Sinclair Play.
+' TODO: in Sinclair Play it is possible to play notes in higher octaves (using several sharps in a row).
+' Need to add more values to the table.
+dim _Play_NoteDividers(0 to _Play_NotesPerOctave * _Play_TotalOctaves - 1) as uinteger = { _
+_ ' C C# D D# E F F# G G# A A# B
+ 6211, 5863, 5534, 5223, 4930, 4653, 4392, 4145, 3913, 3693, 3486, 3290, _ 'octave 0
+ 3106, 2931, 2767, 2611, 2465, 2327, 2196, 2073, 1956, 1847, 1743, 1645, _ 'octave 1
+ 1553, 1466, 1383, 1306, 1232, 1163, 1098, 1036, 978, 923, 871, 823, _ 'octave 2
+ 776, 733, 692, 653, 616, 582, 549, 518, 489, 462, 436, 411, _ 'octave 3
+ 388, 366, 346, 326, 308, 291, 274, 259, 245, 231, 218, 206, _ 'octave 4
+ 194, 183, 173, 163, 154, 145, 137, 130, 122, 115, 109, 103, _ 'octave 5
+ 97, 92, 86, 82, 77, 73, 69, 65, 61, 58, 54, 51, _ 'octave 6
+ 49, 46, 43, 41, 39, 36, 34, 32, 31, 29, 27, 26, _ 'octave 7
+ 24, 23, 22, 20, 19, 18, 17, 16, 15, 14, 14, 13 _ 'octave 8
+}
+
+' Maps ascii code of a letter to the corresponding index of `_Play_NoteDividers` (octave 0).
+dim _Play_NoteIndexes(code("A") to code("G")) as ubyte = { _
+ /'A'/ 9, _
+ /'B'/ 11, _
+ /'C'/ 0, _
+ /'D'/ 2, _
+ /'E'/ 4, _
+ /'F'/ 5, _
+ /'G'/ 7 _
+}
+
+' Pointer to the current channel context.
+' Made global for better performance, and also because it would be problematic to access it from nested subs if it were
+' local (see 'Implementation note' on `Play`).
+dim _Play_ContextPtr as uinteger
+
+' Switches to the first channel context.
+#define _PLAY_CTX_FIRST_CHANNEL() let _Play_ContextPtr = ChannelContextBufferPtr
+
+' Swithes to the next channel context.
+#define _PLAY_CTX_NEXT_CHANNEL() let _Play_ContextPtr = _Play_ContextPtr + ChannelContextSize
+
+' Gets the value of the given `type` at the given `offset` of the current channel context.
+#define _PLAY_CTX_GET(type, offset) (peek(type, _Play_ContextPtr + (offset)))
+
+' Sets the given `value` of the given `type` to the given `offset` of the current channel context.
+#define _PLAY_CTX_SET(type, offset, value) poke type _Play_ContextPtr + (offset), (value)
+
+' Arithmetically adds the given `value` to the value of the given `type` stored at the given `offset`
+' of the current channel context.
+#define _PLAY_CTX_ADD(type, offset, value) _PLAY_CTX_SET(type, offset, _PLAY_CTX_GET(type, offset) + (value))
+
+' Write the value to the given register of the sound chip.
+#define _PLAY_WRITE_TO_REGISTER(register, value) out $cf, (register) : out $0f, (value)
+
+
+' Main sub.
+'
+' Implementation note:
+' If you want to extend the inner subs or functions, or add new ones,
+' please beware that the current compiler version (v1.19.0-beta7 at the time of writing)
+' doesn't support accessing outer local vars from the inner sub/function,
+' if the inner sub/function has its own vars or params.
+'
+' TODO: add sub variants that accept strings byref, and that accept an array.
+' TODO: check valid ranges of command parameters, handle integer overflow/underflow
+'
+sub Play(channel0 as string, channel1 as string = "", channel2 as string = "")
+
+ const CpuCyclesPerSecond as ulong = 3250000
+ const CpuCyclesPerMicrotick as ubyte = 27 ' see the `Wait` sub
+ const BeatsPerBar as ubyte = 4
+ const SecondsPerMinute as ubyte = 60
+ const ChannelCount as ubyte = 3
+
+ const DefaultTempo as ubyte = 120
+ const DefaultOctave as ubyte = 5
+ const DefaultNoteLength as ubyte = 5
+ const DefaultVolume as ubyte = 15
+ const DefaultMixer as ubyte = %11111000
+
+ ' General processing overhead compensation. Applied to every `Wait` invocation.
+ ' Determined experimentally.
+ ' TODO: adjust if needed after everything is implemented.
+ const TickGeneralOverheadInMicroticks as uinteger = 90
+
+ ' Overhead compensation for commands processing of a single channel.
+ ' Applied only on those ticks when there are commands processed for the channel.
+ ' Determined experimentally.
+ ' TODO: adjust if needed after everything is implemented.
+ const TickChannelCommandsOverheadInMicroticks as uinteger = 140
+
+ ' Channel numbers are zero-based, because it's better in terms of performance (less arithmetics in runtime needed).
+ const MaxChannel as ubyte = ChannelCount - 1
+
+ ' Size of a single channel context in bytes. Don't forget to increase this if you add more context fields.
+ ' Note: this is used in macro `_PLAY_CTX_NEXT_CHANNEL`.
+ const ChannelContextSize as ubyte = 15
+
+ ' Channel context data is stored here.
+ dim ChannelContextBuffer(0 to ChannelContextSize * ChannelCount - 1) as ubyte
+
+ ' Pointer to context data.
+ ' Note: this is used in macro `_PLAY_CTX_FIRST_CHANNEL`.
+ dim ChannelContextBufferPtr as uinteger
+ ChannelContextBufferPtr = @ChannelContextBuffer(0)
+
+ ' Offsets of fields in a channel context.
+ ' If you add more fields, don't forget to increase `ChannelContextSize`.
+ const _CharPtr as ubyte = 0 ' (uinteger) Pointer to the current character in the channel string.
+ const _StringEndPtr as ubyte = 2 ' (uinteger) Pointer to the first byte after the last char of the channel
+ ' string.
+ const _TickBackCounter as ubyte = 4 ' (uinteger) How many ticks to wait before proceeding to the next command
+ ' in the channel string. Zero means we need to proceed now.
+ const _PrimaryNoteLengthInTicks as ubyte = 6 ' (uinteger) Current note length in ticks.
+ const _ActualNoteLengthInTicks as ubyte = 8 ' (uinteger) Actual note length in ticks.
+ ' Mostly the same as `_PrimaryNoteLengthInTicks`,
+ ' but may differ for triplets and ties.
+ const _ResetNoteLengthBackCount as ubyte = 10 ' (ubyte) How many notes left to play before actual note length must
+ ' be reset to primary note length.
+ const _BaseDividerIndex as ubyte = 11 ' (ubyte) Divider index (see `_Play_NoteDividers`) that corresponds to
+ ' note C of the current octave.
+ const _SemitoneAdjustment as ubyte = 12 ' (byte) How many semitones to add or subtract from the next note.
+ const _FinishedFlag as ubyte = 13 ' (ubyte) If nonzero, then the channel has finished playing.
+ const _Volume as ubyte = 14 ' (ubyte) Current volume.
+
+ ' Current tempo as beats per minute. A 'beat' is a 1/4-length note.
+ dim Tempo as ubyte
+
+ ' Current tempo as microticks per tick.
+ ' For 'microtick' definition, see the `CpuCyclesPerMicrotick` const.
+ ' For 'tick' definition, see the `_Play_TicksPerBar` const.
+ dim MicroticksPerTick as uinteger
+
+ dim LastChar as ubyte ' Last char read by `ReadChar` sub.
+ dim LastNumber as uinteger ' Last number read by `ReadNumber` sub.
+
+ ' Reads a char from the current channel string and puts it to `LastChar` variable.
+ ' Puts 0 if there's nothing left to read.
+ ' This is a sub, not a function, for performance reasons.
+ sub ReadChar
+ if _PLAY_CTX_GET(uinteger, _CharPtr) = _PLAY_CTX_GET(uinteger, _StringEndPtr) then
+ LastChar = 0
+ return
+ end if
+
+ LastChar = peek(_PLAY_CTX_GET(uinteger, _CharPtr))
+ _PLAY_CTX_ADD(uinteger, _CharPtr, 1)
+ end sub
+
+ ' Reads the number from the current channel string and puts it in `LastNumber` variable.
+ ' Puts 0 if the number is unreadable.
+ sub ReadNumber
+ LastNumber = 0
+
+ do
+ ReadChar
+
+ if LastChar >= code("0") and LastChar <= code("9") then
+ LastNumber = LastNumber * 10 + LastChar - code("0")
+ else
+ ' The number has ended.
+ if LastChar <> 0 then
+ ' Step back if the string has not ended.
+ _PLAY_CTX_ADD(uinteger, _CharPtr, -1)
+ end if
+ exit do
+ end if
+ loop
+ end sub
+
+ ' Sets octave for the current channel.
+ sub SetOctave(octave as ubyte)
+ _PLAY_CTX_SET(ubyte, _BaseDividerIndex, octave * _Play_NotesPerOctave)
+ end sub
+
+ ' Sets `MicroticksPerTick` according to the current `Tempo` value.
+ sub UpdateMicroticksPerTick
+ MicroticksPerTick = _
+ CpuCyclesPerSecond * SecondsPerMinute * BeatsPerBar _
+ / CpuCyclesPerMicrotick / _Play_TicksPerBar / Tempo
+ end sub
+
+ ' Waits for the given amount of microticks.
+ ' One microtick is 27 CPU cycles (if you change it, also change `CpuCyclesPerMicrotick` const).
+ ' TODO: is it possible to suppress the 'unused parameter' warning?
+ sub fastcall Wait(microticks as uinteger)
+ asm
+ proc
+ local loop
+
+ ld bc, 1 ; bc = 1
+ or a ; reset carry flag
+ loop:
+ sbc hl, bc ; microticks = microticks - bc ; 15 cycles
+ jr nz, loop ; if microticks <> 0 then goto loop ; 12 cycles
+
+ endp
+ end asm
+ end sub
+
+ ' Set pitch on the sound chip for a channel.
+ sub SetChipPitchDivider(channel as ubyte, divider as uinteger)
+ _PLAY_WRITE_TO_REGISTER(channel * 2, divider band $ff)
+ _PLAY_WRITE_TO_REGISTER(channel * 2 + 1, divider >> 8)
+ end sub
+
+ ' Set volume on the sound chip for a channel.
+ sub SetChipVolume(channel as ubyte, volume as ubyte)
+ _PLAY_WRITE_TO_REGISTER(channel + 8, volume)
+ end sub
+
+ ' Set mixer mode on the sound chip.
+ sub SetChipMixer(value as ubyte)
+ _PLAY_WRITE_TO_REGISTER(7, value)
+ end sub
+
+ ' If macro `_PLAY_BENCHMARK_MODE` is defined, then interrupts are not disabled, and the system timer is used to
+ ' measure the duration of play. The duration in ticks is printed on the screen after playing.
+ ' Note that interrupts add overhead and inaccuracies to timings, so this mode is not intended for fine-tuning
+ ' timings. This should only be used for differential analysis of code optimization efficiency.
+ #ifndef _PLAY_BENCHMARK_MODE
+ asm
+ di
+ end asm
+ #endif
+
+ _PLAY_CTX_FIRST_CHANNEL()
+ _PLAY_CTX_SET(uinteger, _CharPtr, @channel0)
+
+ _PLAY_CTX_NEXT_CHANNEL()
+ _PLAY_CTX_SET(uinteger, _CharPtr, @channel1)
+
+ _PLAY_CTX_NEXT_CHANNEL()
+ _PLAY_CTX_SET(uinteger, _CharPtr, @channel2)
+
+ dim channel as ubyte
+ dim strLen as uinteger
+
+ _PLAY_CTX_FIRST_CHANNEL()
+
+ for channel = 0 to MaxChannel
+ ' We need low-level access to the strings to achieve good performance.
+
+ ' dereference the pointer to the heap
+ _PLAY_CTX_SET(uinteger, _CharPtr, peek(uinteger, _PLAY_CTX_GET(uinteger, _CharPtr)))
+
+ ' read the string length
+ strLen = peek(uinteger, _PLAY_CTX_GET(uinteger, _CharPtr))
+
+ ' adjust the pointer so it points to the first char
+ _PLAY_CTX_ADD(uinteger, _CharPtr, 2)
+
+ ' calculate and store the pointer to the string end
+ _PLAY_CTX_SET(uinteger, _StringEndPtr, _PLAY_CTX_GET(uinteger, _CharPtr) + strLen)
+
+ SetOctave DefaultOctave
+ _PLAY_CTX_SET(ubyte, _Volume, DefaultVolume)
+ _PLAY_CTX_SET(uinteger, _PrimaryNoteLengthInTicks, _Play_NoteLengthsInTicks(DefaultNoteLength))
+ _PLAY_CTX_SET(uinteger, _ActualNoteLengthInTicks, _Play_NoteLengthsInTicks(DefaultNoteLength))
+ _PLAY_CTX_SET(uinteger, _TickBackCounter, 0)
+ _PLAY_CTX_SET(byte, _SemitoneAdjustment, 0)
+ _PLAY_CTX_SET(ubyte, _ResetNoteLengthBackCount, 0)
+ _PLAY_CTX_SET(ubyte, _FinishedFlag, 0)
+
+ _PLAY_CTX_NEXT_CHANNEL()
+ next channel
+
+ Tempo = DefaultTempo
+ UpdateMicroticksPerTick
+ SetChipMixer DefaultMixer
+
+ dim processedChannels as ubyte
+ dim finishedChannels as ubyte
+ dim lengthInTicks as uinteger
+ dim dividerIndex as ubyte
+
+ #ifdef _PLAY_BENCHMARK_MODE
+ dim SysFrames as uinteger at $5c78
+ dim startTime as uinteger = SysFrames
+ #endif
+
+ do
+ finishedChannels = 0
+ processedChannels = 0
+
+ _PLAY_CTX_FIRST_CHANNEL()
+
+ for channel = 0 to MaxChannel
+ if _PLAY_CTX_GET(uinteger, _TickBackCounter) = 0 then
+ do
+ ReadChar
+
+ if LastChar = 0 then
+ ' This channel has finished playing.
+ _PLAY_CTX_SET(ubyte, _FinishedFlag, 1)
+ ' While other channels are still playing, this one will do rests.
+ SetChipVolume channel, 0
+ exit do
+
+ else if LastChar = code("&") then
+ SetChipVolume channel, 0
+ exit do
+
+ else if (LastChar >= code("a") and LastChar <= code("g")) _
+ or (LastChar >= code("A") and LastChar <= code("G")) then
+
+ dividerIndex = _PLAY_CTX_GET(ubyte, _BaseDividerIndex)
+
+ if LastChar >= code("a") then
+ ' if lowercase, then transpose down 1 octave and make uppercase
+ dividerIndex = dividerIndex - _Play_NotesPerOctave
+ LastChar = LastChar - 32
+ end if
+
+ dividerIndex = dividerIndex + _
+ _Play_NoteIndexes(LastChar) + _PLAY_CTX_GET(byte, _SemitoneAdjustment)
+
+ _PLAY_CTX_SET(byte, _SemitoneAdjustment, 0)
+
+ SetChipPitchDivider channel, _Play_NoteDividers(dividerIndex)
+ SetChipVolume channel, _PLAY_CTX_GET(ubyte, _Volume)
+
+ exit do
+
+ else if LastChar = code("$") then
+ _PLAY_CTX_ADD(byte, _SemitoneAdjustment, -1)
+
+ else if LastChar = code("#") then
+ _PLAY_CTX_ADD(byte, _SemitoneAdjustment, 1)
+
+ else if LastChar >= code("0") and LastChar <= code("9")
+ _PLAY_CTX_ADD(uinteger, _CharPtr, -1) ' step back
+ ReadNumber
+ lengthInTicks = _Play_NoteLengthsInTicks(LastNumber)
+
+ _PLAY_CTX_SET(uinteger, _ActualNoteLengthInTicks, lengthInTicks)
+
+ if LastNumber >= 10 then
+ ' triplet mode (temporary length)
+ _PLAY_CTX_SET(ubyte, _ResetNoteLengthBackCount, 3)
+ else
+ _PLAY_CTX_SET(uinteger, _PrimaryNoteLengthInTicks, lengthInTicks)
+ end if
+
+ else if LastChar = code("_") then
+ ' TODO: ties with triplets work differently in Sinclair ROM.
+ ReadNumber
+ lengthInTicks = _Play_NoteLengthsInTicks(LastNumber)
+
+ _PLAY_CTX_SET(ubyte, _ResetNoteLengthBackCount, 1)
+ _PLAY_CTX_ADD(uinteger, _ActualNoteLengthInTicks, lengthInTicks)
+ _PLAY_CTX_SET(uinteger, _PrimaryNoteLengthInTicks, lengthInTicks)
+
+ else if LastChar = code("O") then
+ ReadNumber
+ SetOctave LastNumber
+
+ else if LastChar = code("V") then
+ ReadNumber
+ _PLAY_CTX_SET(ubyte, _Volume, LastNumber)
+
+ else if LastChar = code("T") then
+ ReadNumber
+
+ if channel = 0 then
+ Tempo = LastNumber
+ UpdateMicroticksPerTick
+ end if
+
+ ' TODO: process other commands here
+ end if
+ loop
+
+ if _PLAY_CTX_GET(ubyte, _ResetNoteLengthBackCount) > 0 then
+ _PLAY_CTX_ADD(ubyte, _ResetNoteLengthBackCount, -1)
+ else
+ _PLAY_CTX_SET(uinteger, _ActualNoteLengthInTicks, _PLAY_CTX_GET(uinteger, _PrimaryNoteLengthInTicks))
+ end if
+
+ _PLAY_CTX_SET(uinteger, _TickBackCounter, _PLAY_CTX_GET(uinteger, _ActualNoteLengthInTicks))
+
+ processedChannels = processedChannels + 1
+ end if
+
+ _PLAY_CTX_ADD(uinteger, _TickBackCounter, -1)
+
+ finishedChannels = finishedChannels + _PLAY_CTX_GET(ubyte, _FinishedFlag)
+
+ _PLAY_CTX_NEXT_CHANNEL()
+ next channel
+
+ Wait MicroticksPerTick _
+ - TickGeneralOverheadInMicroticks _
+ - TickChannelCommandsOverheadInMicroticks * processedChannels
+ loop until finishedChannels = ChannelCount
+
+ #ifdef _PLAY_BENCHMARK_MODE
+ print "Play duration: "; SysFrames - startTime; " ticks."
+ #endif
+
+ ' (zx81sd) Sin EI final: el runtime funciona con las interrupciones
+ ' deshabilitadas y rehabilitarlas podria disparar una INT sin handler.
+end sub
+
+#pragma pop(explicit)
+#pragma pop(strict)
diff --git a/src/lib/arch/zx81sd/stdlib/print42.bas b/src/lib/arch/zx81sd/stdlib/print42.bas
new file mode 100644
index 000000000..605676c28
--- /dev/null
+++ b/src/lib/arch/zx81sd/stdlib/print42.bas
@@ -0,0 +1,586 @@
+' vim:ts=4:et:
+
+#ifndef __PRINT42__
+#define __PRINT42__
+
+' --------------------------------------------------
+' A PRINT Routine with 42 COLUMNS!
+'
+' Contributed by Britlion
+'
+' Override zx81sd: la version zx48k calcula direcciones de pantalla y
+' atributos con constantes fijas de la ROM del Spectrum ($4000/$5800,
+' aqui "64"/"88" como bytes altos) y con sysvars fijos de la ROM
+' ($5C7B = UDG, 23693 = ATTR_P, 15360 = CHARS-256). En zx81sd:
+' - UDG/ATTR_P/CHARS son sysvars propios en $8000+ (ver sysvars.asm,
+' namespace core, de ahi el prefijo .core. en las referencias de
+' mas abajo), y CHARS/UDG son VARIABLES (punteros), no constantes,
+' asi que se leen con LD (r16),(CHARS) en vez de un LD inmediato.
+' - SCREEN_ADDR/SCREEN_ATTR_ADDR tambien son variables (no EQU fijos:
+' bloque 6 en $C000/$D800 en vez de $4000/$5800), pero el byte alto
+' se sigue sumando dentro de un ADD A,n con constante inmediata (el
+' algoritmo de "tercios" de pantalla es identico al del Spectrum,
+' solo cambia la base). Se resuelve parcheando esa constante una
+' vez al entrar en print42(), leyendola de SCREEN_ADDR+1/
+' SCREEN_ATTR_ADDR+1 (automodificacion de codigo, igual que hacia
+' la ROM original con sus propias rutinas).
+' --------------------------------------------------
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = TRUE
+
+SUB printat42 (y as uByte, x as uByte)
+ POKE @printAt42Coords,x
+ POKE @printAt42Coords+1,y
+END SUB
+
+
+SUB print42(characters$ as string)
+asm
+ ; No hace '#include once ' aqui: si esta libreria fuera
+ ; la primera en incluirlo en todo el programa, arrastraria tambien
+ ; bootstrap.asm/charset.asm (con su INCBIN del font completo) justo
+ ; en medio de este cuerpo de funcion, ejecutandose como si fueran
+ ; instrucciones (bug real, encontrado al portar este fichero). Las
+ ; referencias a CHARS/UDG/ATTR_P/SCREEN_ADDR/SCREEN_ATTR_ADDR de mas
+ ; abajo confian en que sysvars.asm ya este incluido por el resto del
+ ; runtime (CLS/PRINT lo requieren siempre, y este fichero no tiene
+ ; sentido usarlo sin ellos).
+ PROC
+
+ ; Parchea las constantes de base de pantalla/atributos usadas mas
+ ; abajo (ver __P42_SCR_HI/__P42_ATTR_HI) con los bytes altos reales
+ ; de .core.SCREEN_ADDR/.core.SCREEN_ATTR_ADDR de este programa (variables en
+ ; zx81sd, no EQU fijos como en el Spectrum).
+ LOCAL __P42_SCR_HI, __P42_ATTR_HI
+ ld a, (.core.SCREEN_ADDR+1)
+ ld (__P42_SCR_HI+1), a
+ ld a, (.core.SCREEN_ATTR_ADDR+1)
+ ld (__P42_ATTR_HI+1), a
+
+ LD A, H
+ OR L
+ JP Z, print42end
+
+ LD C,(HL)
+ INC HL
+ LD B,(HL) ; all told, LD BC with the length of the string.
+
+ LD A, C
+ OR B
+ JP Z, print42end ; Is the length of the string 0? If so, quit.
+
+ INC HL ;Puts HL to the first real character in the string.
+
+LOCAL examineChar
+examineChar:
+ LD A,(HL) ; Grab the character at our pointer position
+ CP 165 ; Too high to print?
+ JR NC, nextChar ; Then we go to the next
+
+ CP 144
+ JR NC, prn ; char is a UDG
+
+ CP 128
+ JR NC, nextChar ; char is a block graphic
+
+ CP 22 ; Is this an AT?
+ JR NZ, isNewline ; If not jump over the AT routine to isNewline
+
+LOCAL isAt
+isAt:
+ EX DE,HL ; Get DE to hold HL for a moment
+ ;;AND A ; Plays with the flags. One of the things it does is reset Carry.
+ ;;LD HL,00002
+ ;;SBC HL,BC ; Subtract length of string from HL.
+ LD HL, -2
+ ADD HL, BC
+ EX DE,HL ; Get HL back from DE
+ JP NC, print42end ; If the result WASN'T negative, return. (We need AT to have parameters to make sense)
+
+ INC HL ; Onto our Y co-ordinate
+ LD D,(HL) ; Put it in D
+ DEC BC ; and move our string remaining counter down one
+ INC HL ; Onto our X co-ordinate
+ LD E,(HL) ; Put the next one in E
+ DEC BC ; and move our string remaining counter down one
+ ld (xycoords), de
+ JR nextChar
+
+LOCAL isNewline
+isNewline:
+ CP 13 ; Is this character a newline?
+ JR NZ, checkdel ; If not, jump forward
+
+LOCAL newline
+newline:
+ ld de, (xycoords)
+ CALL nxtline ; move to next line
+ ld (xycoords), de
+ JR nextChar
+
+LOCAL checkdel
+checkdel:
+ CP 8
+ JR NZ, checkvalid
+ ld de, (xycoords)
+ dec de
+ ld (xycoords), de
+ ld a, 41
+ cp e
+ JR NC, nextChar
+ ld e, a
+ ld (xycoords), de
+ ld a, 23
+ cp d
+ JR NC, nextChar
+ ld d, a
+ ld (xycoords), de
+ JR nextChar
+
+LOCAL checkvalid
+checkvalid:
+ CP 31 ; Is character <31?
+ JR C, nextChar ; If not go to next character
+
+LOCAL prn
+prn:
+ PUSH HL ; Save our position
+ PUSH BC ; Save our countdown of chars left
+ CALL printachar ; Go print a character
+ POP BC ; Recover our count
+ POP HL ; Recover our position
+
+LOCAL nextChar
+nextChar:
+ INC HL ; Move to the next position
+ DEC BC ; count off a character
+ LD A,B
+ OR C ; Did we hit the end of our string? (BC=0?)
+ JR NZ, examineChar ; If not, we need to go look at the next character.
+ JP print42end ; End the print routine
+
+
+; This routine forms the new 6-bit wide characters and
+; alters the colours to match the text. The y,x co-ordinates and eight
+; bytes of workspace are located at the end of this chunk.
+; it starts with the character ascii code in the accumulator
+
+LOCAL printachar
+printachar:
+ EXX
+ PUSH HL ; Store H'L' where we can get it.
+ EXX
+
+ CP 144
+ jr nc,printudg
+
+ ld c, a ; Put a copy of the character in C
+ ld h, 0
+ ld l, a ; Put the Character in HL
+
+ ld de, whichcolumn-32 ; the character is at least 32, so space = 0th entry.
+ add hl, de ; HL -> table entry for char.
+ ld a, (hl) ; Load our column slice data from the table.
+ cp 32 ; Is it less than 32?
+ jr nc, calcChar ; If so, go to the calculated character subroutine
+
+; This is the special case 'we defined the character in the table' option
+ ld de, characters ; Point DE at our table
+ ld l, a ; Put our character number from our table lookup that's in HL in a
+ call mult8 ; multiplies L by 8 and adds in DE [so HL points at our table entry]
+ ld b, h
+ ld c, l ; Copy our character data address into BC
+ jr printdata ; We have our data source, so we print it.
+
+LOCAL printudg
+printudg
+ sub 144 ; get the udg offset
+ ld hl, .core.UDG ; examine our own .core.UDG sysvar for the udg address
+ ld e,(hl) ; lsb for udg address
+ inc hl
+ ld d,(hl) ; mbs for udg address
+ ld l,a ; character offset for udg
+ call mult8 ; multiplies L by 8 and adds in DE [so HL points at our table entry]
+ ld b, h
+ ld c, l ; Copy our character data address into BC
+ jr printdata ; We have our data source, so we print it.
+
+LOCAL calcChar
+calcChar: ; this is the calculate-from-charset option
+ ; a holds the column kill data
+ ld de, (.core.CHARS) ; Character set-256 (variable en zx81sd, no 15360 fijo)
+ ld l, c ; Get our character back from C
+ call mult8 ; Multiply l by 8 and add to DE. (HL points at the charset data for our character now)
+
+ ld de, workspace ; Point DE at our 8 byte workspace.
+ push de ; Save it
+ exx ;
+ ld c, a ; Put our kill column in C'
+ cpl ; Invert
+ ld b, a ; Put the inverse in B'
+ exx ;
+ ld b, 8 ; 8 bytes to a character loop counter
+
+LOCAL loop1
+loop1:
+ ld a, (hl) ; Load a byte of character data
+ inc hl ; point at the next byte
+ exx ;
+ ld e, a ; Put it in e'
+ and c ; keep the left column block we're using
+ ld d, a ; and put it in d'
+ ld a, e ; grab our original back
+ rla ; shift it left (which pushes out our unwanted column)
+ and b ; keep just the right block
+ or d ; mix with the left block
+ exx ;
+ ld (de), a ; put it into our workspace
+ inc de ; next workspace byte
+ djnz loop1 ; go round for our other bytes
+
+ pop bc ; Recover a pointer to our workspace.
+
+LOCAL printdata
+printdata:
+ call testcoords ; check our position, and wrap around if necessary. [returns with d=y,e=x]
+ inc e ; Bump along to next co-ordinate
+ ld (xycoords), de ; Store our coordinates for the next character
+ dec e ; Bump back to our current one
+ ld a, e ; get x
+ sla a ; Shift Left Arithmetic - *2
+ ld l, a ; put x*2 into L
+ sla a ; make it x*4
+ add a, l ; (x*2)+(x*4)=6x
+ ld l, a ; put 6x into L [Since we're in a 6 pixel font, L now contains the # of first pixel we're interested in]
+ srl a ; divide by 2
+ srl a ; divide by another 2 (/4)
+ srl a ; divide by another 2 (/8)
+ ld e, a ; Put the result in e (Since the screen has 8 pixel bytes, pixel/8 = which char pos along our first pixel is in)
+ ld a, l ; Grab our pixel number again
+ and 7 ; And do mod 8 [So now we have how many pixels into the character square we're starting at]
+ push af ; Save A
+ ex af, af'
+ ld a, d ; Put y Coord into A'
+ sra a ; Divide by 2
+ sra a ; Divide by another 2 (/4 total)
+ sra a ; Divide by another 2 (/8) [Gives us a 1/3 of screen number]
+__P42_ATTR_HI:
+ add a, 88 ; Add in start of screen attributes high byte (parcheado con .core.SCREEN_ATTR_ADDR+1 al entrar en print42)
+ ld h, a ; And put the result in H
+ ld a, d ; grab our Y co-ord again
+ and 7 ; Mod 8 (why? *I thought to give a line in this 1/3 of screen, but we're in attrs here)
+ rrca ;
+ rrca
+ rrca ; Bring the bottom 3 bits to the top - Multiply by 32
+ ; (since there are 32 bytes across the screen), here, in other words. [Faster than 5 SLA instructions]
+ add a, e ; add in our x coordinate byte to give us a low screen byte
+ ld l, a ; Put the result in L. So now HL -> screen byte at the top of the character
+
+ ld a, (.core.ATTR_P) ; ATTR P Permanent current colours, etc (as set up by colour statements).
+ ld e, a ; Copy ATTR into e
+ ld (hl), e ; Drop ATTR value into screen
+ inc hl ; Go to next position along
+ pop af ; Pull how many pixels into this square we are
+ cp 3 ; It more than 2?
+ jr c, hop1 ; No? It all fits in this square - jump changing the next attribute
+
+ ld (hl), e ; Must be yes - we're setting the attributes in the next square too.
+
+LOCAL hop1
+hop1:
+ dec hl ; Back up to last position
+ ld a, d ; Y Coord into A'
+ and 248 ; Turn it into 0,8 or 16. (y=0-23)
+__P42_SCR_HI:
+ add a, 64 ; Turn it into screen bitmap high byte (parcheado con .core.SCREEN_ADDR+1 al entrar en print42)
+ ld h, a ; Stick it in H
+ push hl ; Save it
+ exx ; Swap registers
+ pop hl ; Put it into H'L'
+ exx ; Swap Back
+ ld a, 8
+
+LOCAL hop4
+hop4:
+ push af ; Save Accumulator
+ ld a, (bc) ; Grab a byte of workspace
+ exx ; Swap registers
+ push hl ; Save h'l'
+ ld c, 0 ; put 0 into c'
+ ld de, 1023 ; Put 1023 into D'E'
+ ex af, af' ; Swap AF
+ and a ; Flags on A
+ jr z, hop3 ; If a is zero jump forward
+
+ ld b, a ; A -> B
+ ex af, af' ; Swap to A'F'
+
+LOCAL hop2
+hop2:; Slides a byte right to the right position in the block (and puts leftover bits in the left side of c)
+ and a ; Clear Carry Flag
+ rra ; Rotate Right A
+ rr c ; Rotate right C (Rotates a carry flag off A and into C)
+ scf ; Set Carry Flag
+ rr d ; Rotate Right D
+ rr e ; Rotate Right E (D flows into E, with help from the carry bit)
+ djnz hop2 ; Decrement B and loop back
+
+ ex af, af'
+
+LOCAL hop3
+hop3:
+ ex af, af'
+ ld b, a
+ ld a, (hl)
+ and d
+ or b
+ ld (hl), a ; Write out our byte
+ inc hl ; Go one byte right
+ ld a, (hl) ; Bring it in
+ and e
+ or c ; mix those leftover bits into the next block
+ ld (hl), a ; Write it out again
+ pop hl
+ inc h ; Next line
+ exx
+ inc bc ; Next workspace byte
+ pop af
+ dec a
+ jr nz, hop4 ; And go back!
+
+ exx ; Tidy up
+ pop hl ; Clear stack leftovers
+ exx ; And...
+ ret ; Go home.
+
+LOCAL mult8
+mult8: ; Multiplies L by 8 -> HL and adds it to DE. Used for 8 byte table vectors.
+ ld h, 0
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, de
+ ret
+
+LOCAL testcoords
+testcoords:
+ ld de, (xycoords) ; get our current screen co-ordinates (d=y,e=x - little endian)
+
+LOCAL nxtchar
+nxtchar:
+ ld a, e
+ cp 42 ; Are we >42?
+ jr c, ycoord ; if not, hop forward
+
+LOCAL nxtline
+nxtline:
+ inc d ; if so, so bump us to the next line down
+ ld e, 0 ; and reset x to left edge
+
+LOCAL ycoord
+ycoord:
+ ld a, d
+ cp 24 ; are we >24 lines?
+ ret c ; if no, exit subroutine
+ ld d, 0 ; if yes, wrap around to top line again.
+ ret ; exit subroutine
+end asm
+printAt42Coords:
+asm
+LOCAL xycoords
+xycoords:
+ defb 0 ; x coordinate
+ defb 0 ; y coordinate
+
+LOCAL workspace
+workspace:
+ defb 0
+ defb 0
+ defb 0
+ defb 0
+ defb 0
+ defb 0
+ defb 0
+ defb 0
+
+; The data below identifies a column in the character to remove. It consists of 1's
+; from the left edge. First zero bit is the column we're removing.
+; If the leftmost bit is NOT 1, then the byte represents a redefined character position
+; in the lookup table.
+
+LOCAL whichcolumn
+whichcolumn:
+ defb 254 ; SPACE
+ defb 254 ; !
+ defb 128 ; ""
+ defb 224 ; #
+ defb 128 ; $
+ defb 0 ; % (Redefined below)
+ defb 1 ; & (Redefined below)
+ defb 128 ; '
+ defb 128 ; (
+ defb 128 ; )
+ defb 128 ; *
+ defb 128 ; +
+ defb 128 ; ,
+ defb 128 ; -
+ defb 128 ; .
+ defb 128 ; /
+ defb 2 ; 0 (Redefined below)
+ defb 128 ; 1
+ defb 224 ; 2
+ defb 224 ; 3
+ defb 252 ; 4
+ defb 224 ; 5
+ defb 224 ; 6
+ defb 192 ; 7
+ defb 240 ; 8
+ defb 240 ; 9
+ defb 240 ; :
+ defb 240 ; ;
+ defb 192 ; <
+ defb 240 ; =
+ defb 192 ; >
+ defb 192 ; ?
+ defb 248 ; @
+ defb 240 ; A
+ defb 240 ; B
+ defb 240 ; C
+ defb 240 ; D
+ defb 240 ; E
+ defb 240 ; F
+ defb 240 ; G
+ defb 240 ; H
+ defb 128 ; I
+ defb 240 ; J
+ defb 192 ; K
+ defb 240 ; L
+ defb 240 ; M
+ defb 248 ; N
+ defb 240 ; O
+ defb 240 ; P
+ defb 248 ; Q
+ defb 240 ; R
+ defb 240 ; S
+ defb 3 ; T
+ defb 240 ; U
+ defb 240 ; V
+ defb 240 ; W
+ defb 240 ; X
+ defb 4 ; Y
+ defb 252 ; Z
+ defb 224 ; [
+ defb 252 ; \
+ defb 240 ; ]
+ defb 252 ; ^
+ defb 6 ; _
+ defb 240 ; UK Pound (Currency) Symbol
+ defb 255 ; a
+ defb 128 ; b
+ defb 255 ; c
+ defb 255 ; d
+ defb 255 ; e
+ defb 255 ; f
+ defb 255 ; g
+ defb 255 ; h
+ defb 255 ; i
+ defb 255 ; j
+ defb 255 ; k
+ defb 255 ; l
+ defb 255 ; m
+ defb 255 ; n
+ defb 255 ; o
+ defb 255 ; p
+ defb 255 ; q
+ defb 255 ; r
+ defb 255 ; s
+ defb 255 ; t
+ defb 255 ; u
+ defb 255 ; v
+ defb 255 ; w
+ defb 255 ; x
+ defb 255 ; y
+ defb 255 ; z
+ defb 128 ; {
+ defb 128 ; |
+ defb 255 ; }
+ defb 128 ; ~
+ defb 5 ; (c) end column data
+
+
+LOCAL characters
+characters:
+ defb 0 ; %
+ defb 0
+ defb 100
+ defb 104
+ defb 16
+ defb 44
+ defb 76
+ defb 0
+
+ defb 0 ; &
+ defb 32
+ defb 80
+ defb 32
+ defb 84
+ defb 72
+ defb 52
+ defb 0
+
+ defb 0 ; digit 0
+ defb 56
+ defb 76
+ defb 84
+ defb 84
+ defb 100
+ defb 56
+ defb 0
+
+ defb 0 ; Letter T
+ defb 124
+ defb 16
+ defb 16
+ defb 16
+ defb 16
+ defb 16
+ defb 0
+
+ defb 0 ; Letter Y
+ defb 68
+ defb 68
+ defb 40
+ defb 16
+ defb 16
+ defb 16
+ defb 0
+
+ defb 0 ; (c) symbol
+ defb 48
+ defb 72
+ defb 180
+ defb 164
+ defb 180
+ defb 72
+ defb 48
+
+ defb 0
+ defb 0
+ defb 0
+ defb 0
+ defb 0
+ defb 0
+ defb 0
+ defb 0xFC
+
+LOCAL print42end
+print42end:
+ ENDP
+
+end asm
+END SUB
+
+#pragma pop(case_insensitive)
+
+#endif
diff --git a/src/lib/arch/zx81sd/stdlib/print64.bas b/src/lib/arch/zx81sd/stdlib/print64.bas
new file mode 100644
index 000000000..bd6c134c5
--- /dev/null
+++ b/src/lib/arch/zx81sd/stdlib/print64.bas
@@ -0,0 +1,320 @@
+' vim:ts=4:et:
+' ---------------------------------------------------------
+' 64 Characters wide PRINT Routine for ZX BASIC
+' Contributed by Britlion
+'
+' Override zx81sd: la version zx48k asume las direcciones fijas de la
+' ROM del Spectrum para pantalla/atributos ($4000/$5800, aqui "64"/"88"
+' como bytes altos) y el sysvar fijo ATTR_P (23693). En zx81sd,
+' SCREEN_ADDR/SCREEN_ATTR_ADDR son variables (bloque 6, $C000/$D800) y
+' ATTR_P vive en $800E (namespace core, ver sysvars.asm -- de ahi el
+' prefijo .core. en las referencias de mas abajo). El charset de este
+' fichero (p64_charset) es propio, no depende de la ROM, asi que no
+' hace falta tocarlo. Igual que en print42.bas, las dos constantes de
+' base de pantalla/atributos se parchean una vez al entrar en
+' print64(), leyendo el byte alto real de SCREEN_ADDR/SCREEN_ATTR_ADDR.
+' ---------------------------------------------------------
+
+#ifndef __PRINT64__
+#define __PRINT64__
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = TRUE
+
+' Changes print coordinates.
+SUB printat64 (y as uByte, x as uByte)
+ POKE @p64coords,x
+ POKE @p64coords+1,y
+END sub
+
+' Print given string at current position
+SUB print64 (characters$ as String)
+ asm
+ ; No hace '#include once ' aqui: si esta libreria fuera
+ ; la primera en incluirlo en todo el programa, arrastraria tambien
+ ; bootstrap.asm/charset.asm (con su INCBIN del font completo) justo
+ ; en medio de este cuerpo de funcion, ejecutandose como si fueran
+ ; instrucciones (bug real, encontrado al portar este fichero). Las
+ ; referencias a ATTR_P/SCREEN_ADDR/SCREEN_ATTR_ADDR de mas abajo
+ ; confian en que sysvars.asm ya este incluido por el resto del
+ ; runtime (CLS/PRINT lo requieren siempre, y este fichero no tiene
+ ; sentido usarlo sin ellos).
+ PROC ; Declares begin of procedure
+ ; so we can now use LOCAL labels
+
+ ; Parchea las constantes de base de pantalla/atributos usadas mas
+ ; abajo (ver p64_SCR_HI/p64_ATTR_HI) con los bytes altos reales de
+ ; .core.SCREEN_ADDR/.core.SCREEN_ATTR_ADDR de este programa.
+ LOCAL p64_SCR_HI, p64_ATTR_HI
+ ld a, (.core.SCREEN_ADDR+1)
+ ld (p64_SCR_HI+1), a
+ ld a, (.core.SCREEN_ATTR_ADDR+1)
+ ld (p64_ATTR_HI+1), a
+
+ LD L,(IX+4)
+ LD H,(IX+5) ; Get String address of characters$ into HL.
+
+ ld a, h
+ or l
+ jp z, p64_END ; Return if NULL string
+
+ ; Load BC with length of string, and move HL to point to first character.
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ inc hl
+
+ ; Test string length. If Zero, exit.
+ ld a, c
+ or b
+ jp z, p64_END
+
+ LOCAL examineChar
+ examineChar:
+ ld a, (hl) ; Grab the character
+ cp 128 ; too high to print?
+ jr nc, nextChar ; then we go to next.
+
+ cp 22 ; Is this an AT?
+ jr nz, newLine ; If not, hop to newLine routine.
+ ex de, hl ; Swap DE and HL
+ and a ; Clear Carry
+ ld hl, 2 ;
+ sbc hl, bc ; Can we Shorten our string length by 2? If not then at (y,x) doesn't make sense.
+ ex de, hl ; Swap DE and HL back
+ jp nc, p64_END ; If we went negative, there wasn't anything to AT to, so we return.
+
+ inc hl ; Onto our Y co-ordinate
+ ld d, (hl) ; And load it into D
+ dec bc ; Shorten our remaining string counter.
+ inc hl ; Onto the X Co-ordinate
+ ld e, (hl) ; Load it into E
+ dec bc ; Shorten our remaining string counter
+ call p64_test_X ; Make xy legal
+ jr p64_eaa3 ; Go to save coords
+
+ LOCAL newLine
+ newLine:
+ cp 13 ; Is this a newline character?
+ jr nz, p64_isPrintable ; If not, hop to testing to see if we can print this
+ ld de, (p64_coords) ; Get coords
+ call p64_nxtLine ; Go to next line.
+
+ LOCAL p64_eaa3
+ p64_eaa3:
+ ld (p64_coords), de
+ jr nextChar
+
+ LOCAL p64_isPrintable
+ p64_isPrintable:
+ cp 31 ; Bigger than 31?
+ jr c, nextChar ; If not, get the next one.
+ push hl ; Save position
+ push bc ; Save Count
+ call p64_PrintChar ; Call Print SubRoutine
+ pop bc ; Recover length count
+ pop hl ; Recover Position
+
+ LOCAL nextChar
+ nextChar:
+ inc hl ; Point to next character
+ dec bc ; Count off this character
+ ld a, b ; Did we run out?
+ or c
+ jr nz, examineChar ; If not, examine the next one
+ jp p64_END ; Otherwise hop to END.
+
+ LOCAL p64_PrintChar
+ p64_PrintChar:
+ exx
+ push hl ; Save HL'
+ exx
+ sub 32 ; Take out 32 to convert ascii to position in charset
+ ld h, 0
+ rra ; Divide by 2
+ ld l, a ; Put our halved value into HL
+ ld a, 240 ; Set our mask to LEFT side
+ jr nc, p64_eacc ; If we didn't have a carry (even #), hop forward.
+ ld a, 15 ; If we were ab idd #, set our mask to RIGHT side instead
+
+ LOCAL p64_eacc
+ p64_eacc:
+ add hl, hl
+ add hl, hl
+ add hl, hl ; Multiply our char number by 8
+ ld de, p64_charset ; Get our Charset position
+ add hl, de ; And add our character count, so we're now pointed at the first
+ ; byte of the right character.
+ exx
+ ld de, (p64_coords)
+ ex af, af'
+ call p64_loadAndTest
+ ex af, af'
+ inc e
+ ld (p64_coords), de ; Put position+1 into coords
+ dec e
+ ld b, a
+ rr e ; Divide X position by 2
+ ld c, 0
+ rl c ; Bring carry flag into C (result of odd/even position)
+ and 1 ; Mask out lowest bit in A
+ xor c ; XOR with C (Matches position RightLeft with Char RightLeft)
+ ld c, a
+ jr z, p64_eaf6 ; If they are both the same, skip rotation.
+ ld a, b
+ rrca
+ rrca
+ rrca
+ rrca
+ ld b, a
+
+ LOCAL p64_eaf6
+ p64_eaf6:
+ ld a, d ; Get Y coord
+ sra a
+ sra a
+ sra a ; Multiply by 8
+ p64_ATTR_HI:
+ add a, 88 ; Attribute area high byte (parcheado con .core.SCREEN_ATTR_ADDR+1 al entrar en print64)
+ ld h, a ; Put high byte value for attribute into H.
+ ld a, d
+ and 7
+ rrca
+ rrca
+ rrca
+ add a, e
+ ld l, a ; Put low byte for attribute into l
+ ld a, (.core.ATTR_P) ; Get permanent Colours from our own sysvar
+ ld (hl), a ; Write new attribute
+
+ ld a, d
+ and 248
+ p64_SCR_HI:
+ add a, 64 ; Screen bitmap high byte (parcheado con .core.SCREEN_ADDR+1 al entrar en print64)
+ ld h, a
+ ld a, b
+ cpl
+ ld e, a
+ exx
+ ld b, 8
+
+ LOCAL p64_eb18
+ p64_eb18:
+ ld a, (hl)
+ exx
+ bit 0, c
+ jr z, p64_eb22
+ rrca
+ rrca
+ rrca
+ rrca
+
+ LOCAL p64_eb22
+ p64_eb22:
+ and b
+ ld d, a
+ ld a, (hl)
+ and e
+ or d
+ ld (hl), a
+ inc h
+ exx
+ inc hl
+ djnz p64_eb18
+ exx
+ pop hl
+ exx
+ ret
+
+ LOCAL p64_loadAndTest
+ p64_loadAndTest:
+ ld de, (p64_coords)
+
+ ; SubRoutine to go to legal character position.
+ LOCAL p64_test_X
+ p64_test_X:
+ ld a, e ; Get column from e
+ cp 64 ; more than 64 ?
+ jr c, p64_test_Y ; If not, then jump over nextline
+
+ LOCAL p64_nxtLine
+ p64_nxtLine:
+ inc d ; Move down 1
+ ld e, 0 ; reset x co-ord to zero
+
+ LOCAL p64_test_Y
+ p64_test_Y:
+ ld a, d ; get Y co-ord
+ cp 24 ; Past 24?
+ ret c ; Return if not.
+ ld d, 0 ; Rest y co-ord to top of screen.
+ ret ; Return.
+ end asm
+ p64coords:
+ asm
+ LOCAL p64_coords;
+ p64_coords:
+ defb 64; X Coordinate store
+ defb 23; Y Coordinate Store
+
+ LOCAL p64_charset
+ p64_charset:
+ DEFB 0,2,2,2,2,0,2,0 ; Space !
+ DEFB 0,80,82,7,2,7,2,0 ; "" #
+ DEFB 0,37,113,66,114,20,117,32 ; $ %
+ DEFB 0,34,84,32,96,80,96,0 ; & '
+ DEFB 0,36,66,66,66,66,36,0 ; ( )
+ DEFB 0,0,82,34,119,34,82,0 ; * +
+ DEFB 0,0,0,0,7,32,32,64 ; , -
+ DEFB 0,1,1,2,2,100,100,0 ; . /
+ DEFB 0,34,86,82,82,82,39,0 ; 0 1
+ DEFB 0,34,85,18,33,69,114,0 ; 2 3
+ DEFB 0,87,84,118,17,21,18,0 ; 4 5
+ DEFB 0,55,65,97,82,84,36,0 ; 6 7
+ DEFB 0,34,85,37,83,85,34,0 ; 8 9
+ DEFB 0,0,2,32,0,34,2,4 ; : ;
+ DEFB 0,0,16,39,64,39,16,0 ; < =
+ DEFB 0,2,69,33,18,32,66,0 ; > ?
+ DEFB 0,98,149,183,181,133,101,0 ; @ A Changed from ;0,2,37,87,117,85,53,0
+ DEFB 0,98,85,100,84,85,98,0 ; B C
+ DEFB 0,103,84,86,84,84,103,0 ; D E
+ DEFB 0,114,69,116,71,69,66,0 ; F G
+ DEFB 0,87,82,114,82,82,87,0 ; H I
+ DEFB 0,53,21,22,21,85,37,0 ; J K
+ DEFB 0,69,71,71,69,69,117,0 ; L M
+ DEFB 0,82,85,117,117,85,82,0 ; N O
+ DEFB 0,98,85,85,103,71,67,0 ; P Q
+ DEFB 0,98,85,82,97,85,82,0 ; R S
+ DEFB 0,117,37,37,37,37,34,0 ; T U
+ DEFB 0,85,85,85,87,39,37,0 ; V W
+ DEFB 0,85,85,37,82,82,82,0 ; X Y
+ DEFB 0,119,20,36,36,68,119,0 ; Z [
+ DEFB 0,71,65,33,33,17,23,0 ; \ ]
+ DEFB 0,32,112,32,32,32,47,0 ; ^ _
+ DEFB 0,32,86,65,99,69,115,0 ; £ a
+ DEFB 0,64,66,101,84,85,98,0 ; b c
+ DEFB 0,16,18,53,86,84,35,0 ; d e
+ DEFB 0,32,82,69,101,67,69,2 ; f g
+ DEFB 0,66,64,102,82,82,87,0 ; h i
+ DEFB 0,20,4,53,22,21,85,32 ; j k
+ DEFB 0,64,69,71,71,85,37,0 ; l m
+ DEFB 0,0,98,85,85,85,82,0 ; n o
+ DEFB 0,0,99,85,85,99,65,65 ; p q
+ DEFB 0,0,99,84,66,65,70,0 ; r s
+ DEFB 0,64,117,69,69,85,34,0 ; t u
+ DEFB 0,0,85,85,87,39,37,0 ; v w
+ DEFB 0,0,85,85,35,81,85,2 ; x y
+ DEFB 0,0,113,18,38,66,113,0 ; z {
+ DEFB 0,32,36,34,35,34,36,0 ; | {
+ DEFB 0,6,169,86,12,6,9,6 ; ~ (c)
+
+LOCAL p64_END
+ p64_END:
+ ENDP
+ end asm
+
+ end sub
+
+#pragma pop(case_insensitive)
+
+#endif
diff --git a/src/lib/arch/zx81sd/stdlib/puttile.bas b/src/lib/arch/zx81sd/stdlib/puttile.bas
new file mode 100644
index 000000000..f94b18544
--- /dev/null
+++ b/src/lib/arch/zx81sd/stdlib/puttile.bas
@@ -0,0 +1,325 @@
+' ----------------------------------------------------------------
+' This file is released under the MIT License
+'
+' Copyleft (k) 2008-2023
+' by Paul Fisher (a.k.a. BritLion)
+' ----------------------------------------------------------------
+'
+' zx81sd override: the zx48k version computes the screen bitmap and
+' attribute addresses with IMMEDIATE constants (`add a,64` / `add
+' a,88`, the high bytes of the Spectrum ROM's fixed $4000/$5800), not
+' by reading SCREEN_ADDR/SCREEN_ATTR_ADDR. On zx81sd the screen lives
+' at $C000/$D800 (block 6, a variable, not a literal), so those two
+' constants are wrong here -- same bug class found and fixed while
+' porting print42.bas/print64.bas. Fixed by patching both constants
+' once on entering putTile(), reading the real high byte of
+' SCREEN_ADDR/SCREEN_ATTR_ADDR at that point (self-modifying code,
+' same technique as print42.bas/print64.bas). Everything else in this
+' file (the interrupt save/restore dance, the stack-based pixel
+' pushing) is architecture-agnostic and copied unchanged: the
+' `ld a,i` / `jp po` idiom naturally never re-enables interrupts on
+' zx81sd, since IFF2 is always reset here (the runtime never does EI).
+' ----------------------------------------------------------------
+
+#ifndef __LIBRARY_PUTTILE__
+
+REM Avoid recursive / multiple inclusion
+
+#define __LIBRARY_PUTTILE__
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = True
+
+
+' ----------------------------------------------------------------
+' SUB putTile
+'
+' Routine to place a 16 pixel by 16 pixel "Tile" onto the screen at character position x,y from adddress given.
+' Data must be in the format of 16 bit rows, followed by attribute data.
+' (c) 2010 Britlion, donated to the ZX BASIC project.
+' Thanks to Boriel, LCD and Na_than for inspiration behind this.
+
+' This routine could be used as the basis for a fast sprite system, provided all sprites can be in 4 character blocks.
+' It can also be used to clean up dirty background (erase sprites), or put backgrounds from tiled blocks onto a screen.
+'
+' Note the comments about Self Modifying code should be ignored. This has been updated with IX+n methods, which overall
+' are faster than accessing and changing the code.
+' (They would have to be accessed to change the memory anyway - may as well just access them directly.)
+
+' Parameters:
+' x - x coordinate (cell column)
+' y - y coordinate (cell row)
+' graphicsAddr - Memory address of the graphicc
+'
+' ----------------------------------------------------------------
+
+SUB putTile(x as uByte, y as uByte, graphicsAddr as uInteger)
+ ASM
+ PROC
+ LOCAL ptstackSave, pt_start, ptNextThird3, ptSameThird3
+ LOCAL ptAddrDone3, ptNextSprite2, pt_nointerrupts
+ LOCAL PT_SCR_HI, PT_ATTR_HI
+
+ ; Patch the two screen/attribute base constants below with the
+ ; real high byte of SCREEN_ADDR/SCREEN_ATTR_ADDR (variables on
+ ; zx81sd, not the Spectrum ROM's fixed $4000/$5800).
+ ld a, (.core.SCREEN_ADDR+1)
+ ld (PT_SCR_HI+1), a
+ ld a, (.core.SCREEN_ATTR_ADDR+1)
+ ld (PT_ATTR_HI+1), a
+
+ jp pt_start
+
+ptstackSave:
+ defb 0,0
+
+pt_start:
+ ld a,i
+ push af ; Save interrupt status.
+
+ ; Routine to save the background to the buffer
+ di ; we REALLY can NOT be having interrupts while the stack and IX and IY are pointed elsewhere.
+
+ push ix
+ push iy
+ ld d,(ix+9)
+ ld e,(ix+8)
+ ex de,hl
+
+ ;; Print sprites routine
+ ld (ptstackSave), sp ; Save Stack Pointer
+ ld sp,hl ; now SP points at the start of the graphics.
+
+ ; This function returns the address into HL of the screen address
+ ld a,(IX+5) ; Load in x - note the Self Modifying value
+ ld IYH, a ; save it
+ ld l,a
+ ld a,(IX+7) ; Load in y - note the Self Modifying value
+ ld IYL, a ; save it
+ ld d,a
+ and 24
+PT_SCR_HI:
+ add a,64
+ ld h,a
+ ld a,d
+ and 7
+ rrca
+ rrca
+ rrca
+ or l
+ add a,2 ; Need to be to the right so backwards writing pushes land properly.
+ ld l,a
+
+ ; SO now, HL -> Screen address, and SP -> Graphics. Time to start loading.
+
+ pop bc ; row 0
+ pop de ; row 1
+ ex af,af'
+ pop af ; row 2
+ ex af,af'
+ exx
+ pop bc ; row 3
+ pop de ; row 4
+ pop hl ; row 5
+ exx
+
+ ; All right. We're loaded. Time to dump!
+
+ ld ix,0
+ add ix,sp ; Save our stack pointer into ix
+
+ ld sp,hl ; point at the screen.
+ push bc ; row 0
+
+ inc h
+ ld sp,hl
+ push de ; row 1
+
+ inc h
+ ld sp,hl
+ ex af,af'
+ push af ; row 2
+
+ inc h
+ ld sp,hl
+ exx
+ push bc ; row 3
+ exx
+
+ inc h
+ ld sp,hl
+ exx
+ push de ; row 4
+ exx
+
+ inc h
+ ld sp,hl
+ exx
+ push hl ; row 5
+ exx
+
+ ; We're empty. Time to load up again.
+
+ ld sp,ix
+ pop bc ; row 6
+ pop de ; row 7
+ ex af,af'
+ pop af ; row 8
+ ex af,af'
+ exx
+ pop bc ; row 9
+ pop de ; row 10
+ pop hl ; row 11
+ exx
+
+ ; and we're loaded up again! Time to dump this graphic on the screen.
+
+ ld ix,0
+ add ix,sp ; save SP in IX
+
+ inc h
+ ld sp,hl
+ push bc ; row 6
+
+ inc h
+ ld sp,hl
+ push de ; row 7
+
+ dec hl
+ dec hl
+
+ ; Aha. Snag. We're at the bottom of a character. What's the next address down?
+ ld a,l
+ and 224
+ cp 224
+ jr nz, ptNextThird3
+
+ptSameThird3:
+ ld de,32
+ add hl,de
+ jp ptAddrDone3
+
+ptNextThird3:
+ ld de,-1760
+ add hl,de
+
+ptAddrDone3:
+ inc hl
+ inc hl
+
+ ld sp,hl
+ ex af,af'
+ push af ; row 8
+
+ inc h
+ ld sp,hl
+ exx
+ push bc ; row 9
+ exx
+
+ inc h
+ ld sp,hl
+ exx
+ push de ; row 10
+ exx
+
+ inc h
+ ld sp,hl
+ exx
+ push hl ; row 11
+ exx
+
+ ; Okay. Registers empty. Reload time!
+ ld sp,ix
+ pop bc ; row 12
+ pop de ; row 13
+
+ exx
+ pop bc ; row 14
+ pop de ; row 15
+ pop hl ; top attrs
+ exx
+
+ ex af,af'
+ pop af ; bottom attrs
+ ex af,af'
+
+ ; and the last dump to screen
+
+ inc h
+ ld sp,hl
+ push bc
+
+ inc h
+ ld sp,hl
+ push de
+
+ inc h
+ ld sp,hl
+ exx
+ push bc
+ exx
+
+ inc h
+ ld sp,hl
+ exx
+ push de
+ exx
+
+ ; Pixels done. Just need to do the attributes.
+ ; So set HL to the attr address:
+
+ ld a,iyl ;ypos
+
+ rrca
+ rrca
+ rrca ; Multiply by 32
+ ld l,a ; Pass to L
+ and 3 ; Mask with 00000011
+PT_ATTR_HI:
+ add a,88 ; patched with the real SCREEN_ATTR_ADDR high byte on entry
+ ld h,a ; Put it in the High Byte
+ ld a,l ; We get y value *32
+ and 224 ; Mask with 11100000
+ ld l,a ; Put it in L
+ ld a,iyh ; xpos
+ adc a,l ; Add it to the Low byte
+ ld l,a ; Put it back in L, and we're done. HL=Address.
+ inc hl ; we need to be to the right of the attr point as pushes write backwards.
+ inc hl
+
+ ; attr
+ ld sp,hl
+ exx
+ push hl ; top row
+ exx
+
+ ld hl,34 ; we need to move down to the next row. We already backed up 2, so we add 34.
+ add hl,sp
+ ld sp,hl
+ ex af,af' ; bottom row
+ push af
+
+ptNextSprite2:
+ ; done. Cleanup.
+ ld sp,(ptstackSave) ; put our stack back together.
+
+ ; done all 4 final clean up
+
+ pop iy
+ pop ix
+
+ pop af ; recover interrupt status
+ jp po, pt_nointerrupts
+ ei ; Okay. We put everything back. If you need interrupts, you can go with em.
+
+pt_nointerrupts:
+ ENDP
+ END ASM
+END SUB
+
+#require "sysvars.asm"
+
+#pragma pop(case_insensitive)
+
+#endif
diff --git a/src/lib/arch/zx81sd/stdlib/scroll.bas b/src/lib/arch/zx81sd/stdlib/scroll.bas
new file mode 100644
index 000000000..303349adf
--- /dev/null
+++ b/src/lib/arch/zx81sd/stdlib/scroll.bas
@@ -0,0 +1,1043 @@
+' ----------------------------------------------------------------
+' This file is released under the MIT License
+'
+' Copyleft (k) 2008, 2025
+' by Jose Rodriguez-Rosa (a.k.a. Boriel)
+' and Conrado Badenas
+'
+' Version zx81sd: identica a la de zx48k/stdlib/scroll.bas salvo por el
+' calculo de direccion de pixel. El original llama directamente a
+' PIXEL-ADD ($22ACh), rutina de la ROM del Spectrum; en zx81sd no hay ROM
+' mapeada en tiempo de ejecucion, asi que esa direccion cae en mitad de
+' nuestro propio codigo compilado (causaba HALT via un salto salvaje).
+' Se sustituye por runtime/pixel_addr.asm, nuestra propia implementacion
+' con el MISMO contrato de registros que la rutina de ROM (documentado
+' en el propio fichero): entrada A=191,B=Y,C=X: salida HL=offset,
+' A=X AND 7; destruye B, preserva D y E — por eso encaja sin mas cambios
+' en el codigo de mas abajo, que fue escrito para la ROM del Spectrum.
+' SP.PixelDown/SP.PixelUp no se tocan: son aritmetica pura sobre
+' SCREEN_ADDR, sin llamadas a ROM, y ya funcionan igual en zx81sd.
+' ----------------------------------------------------------------
+
+#ifndef __LIBRARY_SCROLL__
+
+REM Avoid recursive / multiple inclusion
+
+#define __LIBRARY_SCROLL__
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = True
+
+' ----------------------------------------------------------------
+' sub ScrollRight
+' pixel by pixel right scroll
+' scrolls 1 pixel right the window defined by (x1, y1, x2, y2)
+' ----------------------------------------------------------------
+sub fastcall ScrollRight(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1, LOOP2, MASK1, MASK2, COLUMNN, NEQ1
+ LOCAL AND1, AND2, AND3, AND4, AND5, AND6, AND7, AND8
+
+; Read parameters, return if they are bad
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+; Compute height and masks
+ inc a
+ ld e, a ; e = y2 - y1 + 1
+
+ ld a,c ;e.g. x1 = 3, mask1 = %11100000, CPL = %00011111
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x1 MOD 8) = 1,2,...,8 for x1 = 0,1,...,7 + 8*n
+ xor a
+MASK1:
+ rra
+ scf ;inject B-1 1s from the left
+ djnz MASK1
+ ld (AND1+1),a ;e.g. %11100000
+ ld (AND5+1),a
+ cpl
+ ld (AND2+1),a ;e.g. %00011111
+ ld (AND7+1),a
+
+ ld a,d ;e.g. x2 = 5, mask2 = %11111100, CPL = %00000011
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x2 MOD 8) = 1,2,...,8 for x2 = 0,1,...,7 + 8*n
+ xor a
+MASK2:
+ scf ;inject B 1s from the left
+ rra
+ djnz MASK2
+ ld (AND4+1),a ;e.g. %11111100
+ ld (AND8+1),a
+ cpl
+ ld (AND3+1),a ;e.g. %00000011
+ ld (AND6+1),a
+
+; Compute Ncols and Display File address, and choose branch
+ ld a,c ;x1
+ and %11111000
+ rrca
+ rrca
+ rrca
+ ld b,a ;col1
+ ld a,d ;x2
+ and %11111000
+ rrca
+ rrca
+ rrca ;col2
+ sub b ;A = Ncols - 1 = col2 - col1
+ ex af,af' ;save Ncols-1 and ZeroFlag
+
+ ld b, h ; BC = y2x1
+ ld a, 191
+ call PIXEL_ADDR
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ex af,af' ;load Ncols-1 and ZeroFlag
+ jr z,NEQ1
+
+; N > 1 (there are 2 or more columns)
+ dec a ;A = Ncols - 2
+ ld d,a ;D = N-2
+LOOP1:
+ push hl
+; Scroll column 1
+ ld a,(hl)
+AND1:
+ and %11100000 ;e.g. x1 = 3
+ ld c,a ;get out-window part of byte
+ ld a,(hl)
+AND2:
+ and %00011111 ;e.g. x1 = 3
+ rra ;scroll in-window part of byte
+ rl b ;CF is stored as bit0 of B
+ or c ;put out-window part of byte
+ ld (hl),a
+ inc hl
+; Scroll columns 2,3,...,N-1
+ ld a,d ;N-2
+ and a ;A=0 iff N=2
+ jr z,COLUMNN
+ rr b ;CF is on stage
+ ld b,a
+LOOP2:
+ rr (hl)
+ inc hl
+ djnz LOOP2
+ rl b ;CF is stored as bit0 of B
+; Scroll column N
+COLUMNN:
+ ld a,(hl)
+AND3:
+ and %00000011 ;e.g. x2 = 5
+ ld c,a ;get out-window part of byte
+ ld a,(hl)
+ rr b ;CF is on stage
+ rra ;scroll in-window part of byte
+AND4:
+ and %11111100 ;e.g. x2 = 5
+ or c ;put out-window part of byte
+ ld (hl),a
+; Scroll another line
+ pop hl
+ dec e
+ ret z
+ call SP.PixelDown
+ jp LOOP1
+
+; N = 1 (there is only one column)
+NEQ1:
+ ld b,(hl)
+ ld a,b
+AND5:
+ and %11100000 ;e.g. x1 = 3
+ ld c,a
+ ld a,b
+AND6:
+ and %00000011 ;e.g. x2 = 5
+ or c
+ ld c,a ;get out-window part of byte
+ ld a,b
+AND7:
+ and %00011111 ;e.g. x1 = 3
+ rra ;scroll in-window part of byte
+AND8:
+ and %11111100 ;e.g. x2 = 5
+ or c ;put out-window part of byte
+ ld (hl),a
+; Scroll another line
+ dec e
+ ret z
+ call SP.PixelDown
+ jp NEQ1
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+' ----------------------------------------------------------------
+' sub ScrollLeft
+' pixel by pixel left scroll
+' scrolls 1 pixel left the window defined by (x1, y1, x2, y2)
+' ----------------------------------------------------------------
+sub fastcall ScrollLeft(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1, LOOP2, MASK1, MASK2, COLUMN1, NEQ1
+ LOCAL AND1, AND2, AND3, AND4, AND5, AND6, AND7, AND8
+
+; Read parameters, return if they are bad
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+; Compute height and masks
+ inc a
+ ld e, a ; e = y2 - y1 + 1
+
+ ld a,c ;e.g. x1 = 3, mask1 = %11100000, CPL = %00011111
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x1 MOD 8) = 1,2,...,8 for x1 = 0,1,...,7 + 8*n
+ xor a
+MASK1:
+ rra
+ scf ;inject B-1 1s from the left
+ djnz MASK1
+ ld (AND3+1),a ;e.g. %00000011
+ ld (AND6+1),a
+ cpl
+ ld (AND4+1),a ;e.g. %11111100
+ ld (AND8+1),a
+
+ ld a,d ;e.g. x2 = 5, mask2 = %11111100, CPL = %00000011
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x2 MOD 8) = 1,2,...,8 for x2 = 0,1,...,7 + 8*n
+ xor a
+MASK2:
+ scf ;inject B 1s from the left
+ rra
+ djnz MASK2
+ ld (AND2+1),a ;e.g. %00011111
+ ld (AND7+1),a
+ cpl
+ ld (AND1+1),a ;e.g. %11100000
+ ld (AND5+1),a
+
+; Compute Ncols and Display File address, and choose branch
+ ld a,c ;x1
+ and %11111000
+ rrca
+ rrca
+ rrca
+ ld b,a ;col1
+ ld a,d ;x2
+ and %11111000
+ rrca
+ rrca
+ rrca ;col2
+ sub b ;A = Ncols - 1 = col2 - col1
+ ex af,af' ;save Ncols-1 and ZeroFlag
+
+ ld c, d
+ ld b, h ; BC = y2x2
+ ld a, 191
+ call PIXEL_ADDR
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ex af,af' ;load Ncols-1 and ZeroFlag
+ jr z,NEQ1
+
+; N > 1 (there are 2 or more columns)
+ dec a ;A = Ncols - 2
+ ld d,a ;D = N-2
+LOOP1:
+ push hl
+; Scroll column N
+ ld a,(hl)
+AND1:
+ and %00000011 ;e.g. x2 = 5
+ ld c,a ;get out-window part of byte
+ ld a,(hl)
+AND2:
+ and %11111100 ;e.g. x2 = 5
+ rla ;scroll in-window part of byte
+ rl b ;CF is stored as bit0 of B
+ or c ;put out-window part of byte
+ ld (hl),a
+ dec hl
+; Scroll columns N-1,...,3,2
+ ld a,d ;N-2
+ and a ;A=0 iff N=2
+ jr z,COLUMN1
+ rr b ;CF is on stage
+ ld b,a
+LOOP2:
+ rl (hl)
+ dec hl
+ djnz LOOP2
+ rl b ;CF is stored as bit0 of B
+; Scroll column 1
+COLUMN1:
+ ld a,(hl)
+AND3:
+ and %11100000 ;e.g. x1 = 3
+ ld c,a ;get out-window part of byte
+ ld a,(hl)
+ rr b ;CF is on stage
+ rla ;scroll in-window part of byte
+AND4:
+ and %00011111 ;e.g. x1 = 3
+ or c ;put out-window part of byte
+ ld (hl),a
+; Scroll another line
+ pop hl
+ dec e
+ ret z
+ call SP.PixelDown
+ jp LOOP1
+
+; N = 1 (there is only one column)
+NEQ1:
+ ld b,(hl)
+ ld a,b
+AND5:
+ and %00000011 ;e.g. x2 = 5
+ ld c,a
+ ld a,b
+AND6:
+ and %11100000 ;e.g. x1 = 3
+ or c
+ ld c,a ;get out-window part of byte
+ ld a,b
+AND7:
+ and %11111100 ;e.g. x2 = 5
+ rla ;scroll in-window part of byte
+AND8:
+ and %00011111 ;e.g. x1 = 3
+ or c ;put out-window part of byte
+ ld (hl),a
+; Scroll another line
+ dec e
+ ret z
+ call SP.PixelDown
+ jp NEQ1
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+
+' ----------------------------------------------------------------
+' sub ScrollUp
+' pixel by pixel up scroll
+' scrolls 1 pixel up the window defined by (x1, y1, x2, y2)
+' ----------------------------------------------------------------
+sub fastcall ScrollUp(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1, MASK1, MASK2, COLUMNN, NEQ1
+ LOCAL AND1, AND2, AND3, AND4, AND5, AND6, AND7, AND8
+ LOCAL EMPTYLINE
+
+; Read parameters, return if they are bad
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+; Compute height and masks
+ push ix
+ inc a
+ ld ixL,a;ixL = y2 - y1 + 1
+
+ ld a,c ;e.g. x1 = 3, mask1 = %11100000, CPL = %00011111
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x1 MOD 8) = 1,2,...,8 for x1 = 0,1,...,7 + 8*n
+ xor a
+MASK1:
+ rra
+ scf ;inject B-1 1s from the left
+ djnz MASK1
+ ld (AND1+1),a ;e.g. %11100000
+ ld (AND5+1),a
+ cpl
+ ld (AND2+1),a ;e.g. %00011111
+ ld (AND7+1),a
+
+ ld a,d ;e.g. x2 = 5, mask2 = %11111100, CPL = %00000011
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x2 MOD 8) = 1,2,...,8 for x2 = 0,1,...,7 + 8*n
+ xor a
+MASK2:
+ scf ;inject B 1s from the left
+ rra
+ djnz MASK2
+ ld (AND4+1),a ;e.g. %11111100
+ ld (AND8+1),a
+ cpl
+ ld (AND3+1),a ;e.g. %00000011
+ ld (AND6+1),a
+
+; Compute Ncols and Display File address, and choose branch
+ ld a,c ;x1
+ and %11111000
+ rrca
+ rrca
+ rrca
+ ld b,a ;col1
+ ld a,d ;x2
+ and %11111000
+ rrca
+ rrca
+ rrca ;col2
+ sub b ;A = Ncols - 1 = col2 - col1
+ ex af,af' ;save Ncols-1 and ZeroFlag
+
+ ld b, h ; BC = y2x1
+ ld a, 191
+ call PIXEL_ADDR
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ex af,af' ;load Ncols-1 and ZeroFlag
+ jr z,NEQ1
+
+; N > 1 (there are 2 or more columns)
+ dec a ;A = Ncols - 2
+ ld ixH,a ;save Ncols-2
+ ld b,0
+LOOP1:
+ dec ixL
+ ld d,h
+ ld e,l
+ call z,EMPTYLINE ;HL at empty line
+ call nz,SP.PixelDown ;HL at line below
+ inc ixL ;restore iterative variable for a second check
+ push hl
+; Scroll column 1
+ ld a,(hl)
+AND2:
+ and %00011111 ;e.g. x1 = 3
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND1:
+ and %11100000 ;e.g. x1 = 3
+ or c ;put in-window part of byte
+ ld (de),a
+ inc hl
+ inc de
+; Scroll columns 2,3,...,N-1
+ ld a,ixH ;load Ncols-2
+ and a
+ jr z,COLUMNN
+ ld c,a
+ ldir
+; Scroll column N
+COLUMNN:
+ ld a,(hl)
+AND4:
+ and %11111100 ;e.g. x2 = 5
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND3:
+ and %00000011 ;e.g. x2 = 5
+ or c ;put in-window part of byte
+ ld (de),a
+; Scroll another line
+ pop hl
+ dec ixL
+ jp nz,LOOP1
+ pop ix
+ ret
+
+; N = 1 (there is only one column)
+NEQ1:
+ dec ixL
+ ld d,h
+ ld e,l
+ call z,EMPTYLINE ;HL at empty line
+ call nz,SP.PixelDown ;HL at line below
+ inc ixL ;restore iterative variable for a second check
+
+ ld a,(hl)
+AND7:
+ and %00011111 ;e.g. x1 = 3
+AND8:
+ and %11111100 ;e.g. x2 = 5
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND5:
+ and %11100000 ;e.g. x1 = 3
+ ld b,a
+ ld a,(de)
+AND6:
+ and %00000011 ;e.g. x2 = 5
+ or b
+ or c ;put in-window part of byte
+ ld (de),a
+; Scroll another line
+ dec ixL
+ jp nz,NEQ1
+ pop ix
+ ret
+
+ defs 32,0 ;empty line with 32 zero-bytes
+EMPTYLINE:
+ ld hl,EMPTYLINE-32
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+
+' ----------------------------------------------------------------
+' sub ScrollDown
+' pixel by pixel down scroll
+' scrolls 1 pixel down the window defined by (x1, y1, x2, y2)
+' ----------------------------------------------------------------
+sub fastcall ScrollDown(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1, MASK1, MASK2, COLUMNN, NEQ1
+ LOCAL AND1, AND2, AND3, AND4, AND5, AND6, AND7, AND8
+ LOCAL EMPTYLINE
+
+; Read parameters, return if they are bad
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+; Compute height and masks
+ push ix
+ inc a
+ ld ixL,a;ixL = y2 - y1 + 1
+ ld h,b ;y1
+
+ ld a,c ;e.g. x1 = 3, mask1 = %11100000, CPL = %00011111
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x1 MOD 8) = 1,2,...,8 for x1 = 0,1,...,7 + 8*n
+ xor a
+MASK1:
+ rra
+ scf ;inject B-1 1s from the left
+ djnz MASK1
+ ld (AND1+1),a ;e.g. %11100000
+ ld (AND5+1),a
+ cpl
+ ld (AND2+1),a ;e.g. %00011111
+ ld (AND7+1),a
+
+ ld a,d ;e.g. x2 = 5, mask2 = %11111100, CPL = %00000011
+ and 7
+ inc a
+ ld b,a ;B = 1 + (x2 MOD 8) = 1,2,...,8 for x2 = 0,1,...,7 + 8*n
+ xor a
+MASK2:
+ scf ;inject B 1s from the left
+ rra
+ djnz MASK2
+ ld (AND4+1),a ;e.g. %11111100
+ ld (AND8+1),a
+ cpl
+ ld (AND3+1),a ;e.g. %00000011
+ ld (AND6+1),a
+
+; Compute Ncols and Display File address, and choose branch
+ ld a,c ;x1
+ and %11111000
+ rrca
+ rrca
+ rrca
+ ld b,a ;col1
+ ld a,d ;x2
+ and %11111000
+ rrca
+ rrca
+ rrca ;col2
+ sub b ;A = Ncols - 1 = col2 - col1
+ ex af,af' ;save Ncols-1 and ZeroFlag
+
+ ld b, h ; BC = y1x1
+ ld a, 191
+ call PIXEL_ADDR
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ex af,af' ;load Ncols-1 and ZeroFlag
+ jr z,NEQ1
+
+; N > 1 (there are 2 or more columns)
+ dec a ;A = Ncols - 2
+ ld ixH,a ;save Ncols-2
+ ld b,0
+LOOP1:
+ dec ixL
+ ld d,h
+ ld e,l
+ call z,EMPTYLINE ;HL at empty line
+ call nz,SP.PixelUp ;HL at line above
+ inc ixL ;restore iterative variable for a second check
+ push hl
+; Scroll column 1
+ ld a,(hl)
+AND2:
+ and %00011111 ;e.g. x1 = 3
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND1:
+ and %11100000 ;e.g. x1 = 3
+ or c ;put in-window part of byte
+ ld (de),a
+ inc hl
+ inc de
+; Scroll columns 2,3,...,N-1
+ ld a,ixH ;load Ncols-2
+ and a
+ jr z,COLUMNN
+ ld c,a
+ ldir
+; Scroll column N
+COLUMNN:
+ ld a,(hl)
+AND4:
+ and %11111100 ;e.g. x2 = 5
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND3:
+ and %00000011 ;e.g. x2 = 5
+ or c ;put in-window part of byte
+ ld (de),a
+; Scroll another line
+ pop hl
+ dec ixL
+ jp nz,LOOP1
+ pop ix
+ ret
+
+; N = 1 (there is only one column)
+NEQ1:
+ dec ixL
+ ld d,h
+ ld e,l
+ call z,EMPTYLINE ;HL at empty line
+ call nz,SP.PixelUp ;HL at line below
+ inc ixL ;restore iterative variable for a second check
+
+ ld a,(hl)
+AND7:
+ and %00011111 ;e.g. x1 = 3
+AND8:
+ and %11111100 ;e.g. x2 = 5
+ ld c,a ;get in-window part of byte
+ ld a,(de)
+AND5:
+ and %11100000 ;e.g. x1 = 3
+ ld b,a
+ ld a,(de)
+AND6:
+ and %00000011 ;e.g. x2 = 5
+ or b
+ or c ;put in-window part of byte
+ ld (de),a
+; Scroll another line
+ dec ixL
+ jp nz,NEQ1
+ pop ix
+ ret
+
+ defs 32,0 ;empty line with 32 zero-bytes
+EMPTYLINE:
+ ld hl,EMPTYLINE-32
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+
+' ----------------------------------------------------------------
+' sub ScrollRightAligned
+' pixel by pixel right scroll.
+' scrolls 1 pixel right the window defined by (x1, y1, x2, y2)
+' This scroll is aligned to columns.
+' x1 and x2 are divided by 8 and rounded down (floor) to get the column number (0..31)
+' ----------------------------------------------------------------
+sub fastcall ScrollRightAligned(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1
+ LOCAL LOOP2
+
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ srl a
+ srl a
+ srl a
+ inc a
+ ld e, a ; e = (x2 - x1) / 8 + 1
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+ inc a
+ ld d, a ; d = y2 - y1 + 1
+
+ ld b, h ; BC = y2x1
+ ld a, 191
+ call PIXEL_ADDR
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+LOOP1:
+ push hl
+ ld b, e ; C cols
+ or a ; clear carry flag
+LOOP2:
+ rr (hl)
+ inc hl
+ djnz LOOP2
+ pop hl
+
+ dec d
+ ret z
+ call SP.PixelDown
+ jp LOOP1
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+
+' ----------------------------------------------------------------
+' sub ScrolLeftAligned
+' pixel by pixel left scroll
+' scrolls 1 pixel left the window defined by (x1, y1, x2, y2)
+' This scroll is aligned to columns.
+' x1 and x2 are divided by 8 and rounded down (floor) to get the column number (0..31)
+' ----------------------------------------------------------------
+sub fastcall ScrollLeftAligned(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1
+ LOCAL LOOP2
+
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ srl a
+ srl a
+ srl a
+ inc a
+ ld e, a ; e = (x2 - x1) / 8 + 1
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+ ld c, d
+ inc a
+ ld d, a ; d = y2 - y1 + 1
+
+ ld b, h ; BC = y2x1
+ ld a, 191
+ call PIXEL_ADDR
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+LOOP1:
+ push hl
+ ld b, e ; C cols
+ or a ; clear carry flag
+LOOP2:
+ rl (hl)
+ dec hl
+ djnz LOOP2
+ pop hl
+
+ dec d
+ ret z
+ call SP.PixelDown
+ jp LOOP1
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+
+' ----------------------------------------------------------------
+' sub ScrolUpAligned
+' pixel by pixel up scroll
+' scrolls 1 pixel up the window defined by (x1, y1, x2, y2)
+' This scroll is aligned to columns.
+' x1 and x2 are divided by 8 and rounded down (floor) to get the column number (0..31)
+' ----------------------------------------------------------------
+sub fastcall ScrollUpAligned(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1
+
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ srl a
+ srl a
+ srl a
+ inc a
+ ld e, a ; e = (x2 - x1) / 8 + 1
+ ex af, af' ; save it for later
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+ inc a
+ ld d, a ; d = y2 - y1 + 1
+
+ ld b, h ; BC = y2x1
+ ld a, 191
+ call PIXEL_ADDR
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ld a, d ; Num. of scan lines
+ ld b, 0
+ exx
+ ld b, a ; Scan lines counter
+ ex af, af' ; Recovers cols
+ ld c, a
+ jp LOOP_START
+
+LOOP1:
+ exx
+ ld d, h
+ ld e, l
+ ld c, a ; C cols
+ call SP.PixelDown
+ push hl
+ ldir
+ pop hl
+ exx
+ ld a, c ; Recovers C Cols
+ LOCAL LOOP_START
+LOOP_START:
+ djnz LOOP1
+
+ ; Clears bottom line
+ exx
+ ld (hl), 0
+ ld d, h
+ ld e, l
+ inc de
+ ld c, a
+ dec c
+ ret z
+ ldir
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+
+' ----------------------------------------------------------------
+' sub ScrolDownAligned
+' pixel by pixel down scroll
+' scrolls 1 pixel down the window defined by (x1, y1, x2, y2)
+' This scroll is aligned to columns.
+' x1 and x2 are divided by 8 and rounded down (floor) to get the column number (0..31)
+' ----------------------------------------------------------------
+sub fastcall ScrollDownAligned(x1 as uByte, y1 as uByte, x2 as Ubyte, y2 as Ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL LOOP1
+
+ ; a = x1
+ pop hl ; RET address
+ pop bc ; b = y1
+ pop de ; d = x2
+ ex (sp), hl ; h = y2, (sp) = RET address. Stack ok now
+
+ ld c, a ; BC = y1x1
+ ld a, d
+ sub c
+ ret c ; x1 > x2
+
+ srl a
+ srl a
+ srl a
+ inc a
+ ld e, a ; e = (x2 - x1) / 8 + 1
+ ex af, af' ; save it for later
+
+ ld a, h
+ sub b
+ ret c ; y1 > y2
+
+ inc a
+ ld d, a ; d = y2 - y1 + 1
+
+ ld a, 191
+ call PIXEL_ADDR
+ res 6, h ; Starts from 0
+ ld bc, (SCREEN_ADDR)
+ add hl, bc ; Now current offset
+
+ ld a, d ; Num. of scan lines
+ ld b, 0
+ exx
+ ld b, a ; Scan lines counter
+ ex af, af' ; Recovers cols
+ ld c, a
+ jp LOOP_START
+
+LOOP1:
+ exx
+ ld d, h
+ ld e, l
+ ld c, a ; C cols
+ call SP.PixelUp
+ push hl
+ ldir
+ pop hl
+ exx
+ ld a, c ; Recovers C Cols
+ LOCAL LOOP_START
+LOOP_START:
+ djnz LOOP1
+
+ ; Clears top line
+ exx
+ ld (hl), 0
+ ld d, h
+ ld e, l
+ inc de
+ ld c, a
+ dec c
+ ret z
+ ldir
+
+ ENDP
+
+ pop namespace
+ end asm
+end sub
+
+
+#pragma pop(case_insensitive)
+
+REM the following is required, because it defines SCREEN_ADDR and SCREEN_ATTR_ADDR
+#require "sysvars.asm"
+#require "SP/PixelDown.asm"
+#require "SP/PixelUp.asm"
+#require "pixel_addr.asm"
+
+#endif
diff --git a/src/lib/arch/zxnext/runtime/SP/CharLeft.asm b/src/lib/arch/zxnext/runtime/SP/CharLeft.asm
index 641754bfa..0d19132f1 100644
--- a/src/lib/arch/zxnext/runtime/SP/CharLeft.asm
+++ b/src/lib/arch/zxnext/runtime/SP/CharLeft.asm
@@ -1,59 +1,8 @@
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
-; CharLeft
-; Alvin Albrecht 2002
-;
-
-;INCLUDE "SPconfig.def"
-;XLIB SPCharLeft
-
-; Char Left
-;
-; Adjusts screen address HL to move one character to the left
-; on the display. Start of line wraps to the previous row.
-;
-; enter: HL = valid screen address
-; Carry reset
-; exit : Carry = moved off screen
-; HL = moves one character left, with line wrap
-; used : AF, HL
-
-;IF !DISP_HIRES
-
- push namespace core
-
-SP.CharLeft:
- ld a,l
- dec l
- or a
- ret nz
- ld a,h
- sub $08
- ld h,a
- cp $40
- ret
-
- pop namespace
-
-
-;ELSE
-
-;.SPCharLeft
-; ld a,h
-; xor $20
-; ld h,a
-; cp $58
-; ccf
-; ret nc
-; ld a,l
-; dec l
-; or a
-; ret nz
-; ld a,h
-; sub $08
-; ld h,a
-; and $18
-; cp $18
-; ccf
-; ret
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
-; ENDIF
\ No newline at end of file
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/SP/CharRight.asm b/src/lib/arch/zxnext/runtime/SP/CharRight.asm
index 1a47d84cb..52ce45186 100644
--- a/src/lib/arch/zxnext/runtime/SP/CharRight.asm
+++ b/src/lib/arch/zxnext/runtime/SP/CharRight.asm
@@ -1,54 +1,8 @@
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
-; CharRight
-; Alvin Albrecht 2002
-;
-
-;INCLUDE "SPconfig.def"
-;XLIB SPCharRight
-
-; Char Right
-;
-; Adjusts screen address HL to move one character to the right
-; on the display. End of line wraps to the next row.
-;
-; enter: HL = valid screen address
-; Carry reset
-; exit : Carry = moved off screen
-; HL = moves one character right, with line wrap
-; used : AF, HL
-
-;IF !DISP_HIRES
-
- push namespace core
-
-SP.CharRight:
- inc l
- ret nz
- ld a,8
- add a,h
- ld h,a
- cp $58
- ccf
- ret
-
- pop namespace
-
-
-;ELSE
-
-;.SPCharRight
-; ld a,h
-; xor $20
-; ld h,a
-; cp $58
-; ret nc
-; inc l
-; ret nz
-; ld a,8
-; add a,h
-; ld h,a
-; cp $58
-; ccf
-; ret
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
-; ENDIF
\ No newline at end of file
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/SP/GetScrnAddr.asm b/src/lib/arch/zxnext/runtime/SP/GetScrnAddr.asm
index 21f9a4afc..faca1b505 100644
--- a/src/lib/arch/zxnext/runtime/SP/GetScrnAddr.asm
+++ b/src/lib/arch/zxnext/runtime/SP/GetScrnAddr.asm
@@ -1,62 +1,8 @@
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
-; GetScrnAddr
-; Alvin Albrecht 2002
-;
-
-; Get Screen Address
-;
-; Computes the screen address given a valid pixel coordinate.
-; (0,0) is located at the top left corner of the screen.
-;
-; enter: h = y coord
-; l = x coord
-; In hi-res mode, Carry is most significant bit of x coord (0..511 pixels)
-; exit : de = screen address, b = pixel mask
-; uses : af, b, de, hl
-
- push namespace core
- PROC
- LOCAL rotloop, norotate
-
-SPGetScrnAddr:
- ld a,h
- and $07
- ld d,a
- ld a,h
- rra
- rra
- rra
- and $18
- or d
- ld d,a
-
- ld a,l
- and $07
- ld b,a
- ld a,$80
- jr z, norotate
-
-rotloop:
- rra
- djnz rotloop
-
-norotate:
- ld b,a
- srl l
- srl l
- srl l
- ld a,h
- rla
- rla
- and $e0
- or l
- ld e,a
- ld hl, (SCREEN_ADDR)
- add hl, de
- ex de, hl
- ret
-
- ENDP
- pop namespace
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
-#include once
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/SP/PixelDown.asm b/src/lib/arch/zxnext/runtime/SP/PixelDown.asm
index 3b5aaf62e..e644ae996 100644
--- a/src/lib/arch/zxnext/runtime/SP/PixelDown.asm
+++ b/src/lib/arch/zxnext/runtime/SP/PixelDown.asm
@@ -1,57 +1,8 @@
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
-; PixelDown
-; Alvin Albrecht 2002
-;
-
-; Pixel Down
-;
-; Adjusts screen address HL to move one pixel down in the display.
-; (0,0) is located at the top left corner of the screen.
-;
-; enter: HL = valid screen address
-; exit : Carry = moved off screen
-; Carry'= moved off current cell (needs ATTR update)
-; HL = moves one pixel down
-; used : AF, HL
-
- push namespace core
-
-SP.PixelDown:
- PROC
- LOCAL leave
-
- push de
- ld de, (SCREEN_ADDR)
- or a
- sbc hl, de
- inc h
- ld a,h
- and $07
- jr nz, leave
- scf ; Sets carry on F', which flags ATTR must be updated
- ex af, af'
- ld a,h
- sub $08
- ld h,a
- ld a,l
- add a,$20
- ld l,a
- jr nc, leave
- ld a,h
- add a,$08
- ld h,a
- cp $19 ; carry = 0 => Out of screen
- jr c, leave ; returns if out of screen
- ccf
- pop de
- ret
-
-leave:
- add hl, de ; This always sets Carry = 0
- pop de
- ret
-
- ENDP
- pop namespace
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
-#include once
\ No newline at end of file
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/SP/PixelLeft.asm b/src/lib/arch/zxnext/runtime/SP/PixelLeft.asm
index 20be4e294..39f9fb9f3 100644
--- a/src/lib/arch/zxnext/runtime/SP/PixelLeft.asm
+++ b/src/lib/arch/zxnext/runtime/SP/PixelLeft.asm
@@ -1,52 +1,8 @@
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
-; PixelLeft
-; Jose Rodriguez 2012
-;
-
-; PixelLeft
-;
-; Adjusts screen address HL and Pixel bit A to move one pixel to the left
-; on the display. Start of line set Carry (Out of Screen)
-;
-; enter: HL = valid screen address
-; A = Bit Set
-; exit : Carry = moved off screen
-; Carry' Set if moved off current ATTR CELL
-; HL = moves one character left, if needed
-; A = Bit Set with new pixel pos.
-; used : AF, HL
-
-
- push namespace core
-
-SP.PixelLeft:
- PROC
-
- LOCAL leave
-
- push de
- ld de, (SCREEN_ADDR)
- or a
- sbc hl, de ; This always sets Carry = 0
-
- rlca ; Sets new pixel bit 1 to the right
- jr nc, leave
- ex af, af' ; Signal in C' we've moved off current ATTR cell
- ld a,l
- dec a
- ld l,a
- cp 32 ; Carry if in screen
- ccf
- ld a, 1
-
-leave: ; Sets screen offset back again
- push af
- add hl, de
- pop af
- pop de
- ret
-
- ENDP
- pop namespace
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
-#include once
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/SP/PixelRight.asm b/src/lib/arch/zxnext/runtime/SP/PixelRight.asm
index 5c0688e93..8d7e8eda4 100644
--- a/src/lib/arch/zxnext/runtime/SP/PixelRight.asm
+++ b/src/lib/arch/zxnext/runtime/SP/PixelRight.asm
@@ -1,53 +1,8 @@
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
-; PixelRight
-; Jose Rodriguez 2012
-;
-
-
-; PixelRight
-;
-; Adjusts screen address HL and Pixel bit A to move one pixel to the left
-; on the display. Start of line set Carry (Out of Screen)
-;
-; enter: HL = valid screen address
-; A = Bit Set
-; exit : Carry = moved off screen
-; Carry' Set if moved off current ATTR CELL
-; HL = moves one character left, if needed
-; A = Bit Set with new pixel pos.
-; used : AF, HL
-
-
- push namespace core
-
-SP.PixelRight:
- PROC
-
- LOCAL leave
-
- push de
- ld de, (SCREEN_ADDR)
- or a
- sbc hl, de ; This always sets Carry = 0
-
- rrca ; Sets new pixel bit 1 to the right
- jr nc, leave
- ex af, af' ; Signal in C' we've moved off current ATTR cell
- ld a, l
- inc a
- ld l, a
- cp 32 ; Carry if IN screen
- ccf
- ld a, 80h
-
-leave: ; Sets screen offset back again
- push af
- add hl, de
- pop af
- pop de
- ret
-
- ENDP
- pop namespace
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
-#include once
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/SP/PixelUp.asm b/src/lib/arch/zxnext/runtime/SP/PixelUp.asm
index 1657f9571..cd99f7998 100644
--- a/src/lib/arch/zxnext/runtime/SP/PixelUp.asm
+++ b/src/lib/arch/zxnext/runtime/SP/PixelUp.asm
@@ -1,56 +1,8 @@
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
-; PixelUp
-; Alvin Albrecht 2002
-;
-
-; Pixel Up
-;
-; Adjusts screen address HL to move one pixel up in the display.
-; (0,0) is located at the top left corner of the screen.
-;
-; enter: HL = valid screen address
-; exit : Carry = moved off screen
-; HL = moves one pixel up
-; used : AF, HL
-
- push namespace core
-
-SP.PixelUp:
- PROC
-
- LOCAL leave
-
- push de
- ld de, (SCREEN_ADDR)
- or a
- sbc hl, de
-
- ld a,h
- dec h
- and $07
- jr nz, leave
- scf ; sets C' to 1 (ATTR update needed)
- ex af, af'
- ld a,$08
- add a,h
- ld h,a
- ld a,l
- sub $20
- ld l,a
- jr nc, leave
- ld a,h
- sub $08
- ld h,a
-
-leave:
- push af
- add hl, de
- pop af
- pop de
- ret
-
- ENDP
-
- pop namespace
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
-#include once
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/abs16.asm b/src/lib/arch/zxnext/runtime/abs16.asm
index 08a7699ff..c0ea9bac3 100644
--- a/src/lib/arch/zxnext/runtime/abs16.asm
+++ b/src/lib/arch/zxnext/runtime/abs16.asm
@@ -1,5 +1,8 @@
-; 16 bit signed integer abs value
-; HL = value
-
-#include once
+; ----------------------------------------------------------------
+; This file is released under the MIT License
+;
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/abs32.asm b/src/lib/arch/zxnext/runtime/abs32.asm
index 0ef7ebab3..1c24bcb97 100644
--- a/src/lib/arch/zxnext/runtime/abs32.asm
+++ b/src/lib/arch/zxnext/runtime/abs32.asm
@@ -1,5 +1,8 @@
-; 16 bit signed integer abs value
-; HL = value
-
-#include once
+; ----------------------------------------------------------------
+; This file is released under the MIT License
+;
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/abs8.asm b/src/lib/arch/zxnext/runtime/abs8.asm
index 499c8efa0..21c135129 100644
--- a/src/lib/arch/zxnext/runtime/abs8.asm
+++ b/src/lib/arch/zxnext/runtime/abs8.asm
@@ -1,12 +1,8 @@
-; Returns absolute value for 8 bit signed integer
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
- push namespace core
-
-__ABS8:
- or a
- ret p
- neg
- ret
-
- pop namespace
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/absf.asm b/src/lib/arch/zxnext/runtime/absf.asm
index 084e1d55f..a882ec4ab 100644
--- a/src/lib/arch/zxnext/runtime/absf.asm
+++ b/src/lib/arch/zxnext/runtime/absf.asm
@@ -1,8 +1,8 @@
-; ABS value for a floating point A EDCB
- push namespace core
+; ----------------------------------------------------------------
+; This file is released under the MIT License
+;
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
-__ABSF:
- res 7, e ; Sets sign to positive. Thast all! ;-)
- ret
-
- pop namespace
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/arith/_mul32.asm b/src/lib/arch/zxnext/runtime/arith/_mul32.asm
index 60aa191a5..48b0800f6 100644
--- a/src/lib/arch/zxnext/runtime/arith/_mul32.asm
+++ b/src/lib/arch/zxnext/runtime/arith/_mul32.asm
@@ -1,70 +1,8 @@
-
-; Ripped from: http://www.andreadrian.de/oldcpu/z80_number_cruncher.html#moztocid784223
-; Used with permission.
-; Multiplies 32x32 bit integer (DEHL x D'E'H'L')
-; 64bit result is returned in H'L'H L B'C'A C
-
-
- push namespace core
-
-__MUL32_64START:
- push hl
- exx
- ld b, h
- ld c, l ; BC = Low Part (A)
- pop hl ; HL = Load Part (B)
- ex de, hl ; DE = Low Part (B), HL = HightPart(A) (must be in B'C')
- push hl
-
- exx
- pop bc ; B'C' = HightPart(A)
- exx ; A = B'C'BC , B = D'E'DE
-
- ; multiply routine 32 * 32bit = 64bit
- ; h'l'hlb'c'ac = b'c'bc * d'e'de
- ; needs register a, changes flags
- ;
- ; this routine was with tiny differences in the
- ; sinclair zx81 rom for the mantissa multiply
-
-__LMUL:
- xor a ; reset carry flag
- ld h, a ; result bits 32..47 = 0
- ld l, a
- exx
- ld h, a ; result bits 48..63 = 0
- ld l, a
- exx
- ld a,b ; mpr is b'c'ac
- ld b,33 ; initialize loop counter
- jp __LMULSTART
-
-__LMULLOOP:
- jr nc,__LMULNOADD ; JP is 2 cycles faster than JR. Since it's inside a LOOP
- ; it can save up to 33 * 2 = 66 cycles
- ; But JR if 3 cycles faster if JUMP not taken!
- add hl,de ; result += mpd
- exx
- adc hl,de
- exx
-
-__LMULNOADD:
- exx
- rr h ; right shift upper
- rr l ; 32bit of result
- exx
- rr h
- rr l
-
-__LMULSTART:
- exx
- rr b ; right shift mpr/
- rr c ; lower 32bit of result
- exx
- rra ; equivalent to rr a
- rr c
- djnz __LMULLOOP
-
- ret ; result in h'l'hlb'c'ac
-
- pop namespace
+; ----------------------------------------------------------------
+; This file is released under the MIT License
+;
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
+
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/arith/addf.asm b/src/lib/arch/zxnext/runtime/arith/addf.asm
index cc6b03c04..176227d05 100644
--- a/src/lib/arch/zxnext/runtime/arith/addf.asm
+++ b/src/lib/arch/zxnext/runtime/arith/addf.asm
@@ -1,24 +1,8 @@
-#include once
-
-; -------------------------------------------------------------
-; Floating point library using the FP ROM Calculator (ZX 48K)
-; All of them uses A EDCB registers as 1st paramter.
-; For binary operators, the 2n operator must be pushed into the
-; stack, in the order AF DE BC (F not used).
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
-; Uses CALLEE convention
-; -------------------------------------------------------------
-
- push namespace core
-
-__ADDF: ; Addition
- call __FPSTACK_PUSH2
-
- ; ------------- ROM ADD
- rst 28h
- defb 0fh ; ADD
- defb 38h; ; END CALC
-
- jp __FPSTACK_POP
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
- pop namespace
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/arith/div16.asm b/src/lib/arch/zxnext/runtime/arith/div16.asm
index 1eed19848..e11169336 100644
--- a/src/lib/arch/zxnext/runtime/arith/div16.asm
+++ b/src/lib/arch/zxnext/runtime/arith/div16.asm
@@ -1,104 +1,8 @@
-; 16 bit division and modulo functions
-; for both signed and unsigned values
-
-#include once
-
- push namespace core
-
-__DIVU16: ; 16 bit unsigned division
- ; HL = Dividend, Stack Top = Divisor
-
- ; -- OBSOLETE ; Now uses FASTCALL convention
- ; ex de, hl
- ; pop hl ; Return address
- ; ex (sp), hl ; CALLEE Convention
-
-__DIVU16_FAST:
- ld a, h
- ld c, l
- ld hl, 0
- ld b, 16
-
-__DIV16LOOP:
- sll c
- rla
- adc hl,hl
- sbc hl,de
- jr nc, __DIV16NOADD
- add hl,de
- dec c
-
-__DIV16NOADD:
- djnz __DIV16LOOP
-
- ex de, hl
- ld h, a
- ld l, c
-
- ret ; HL = quotient, DE = Mudulus
-
-
-
-__MODU16: ; 16 bit modulus
- ; HL = Dividend, Stack Top = Divisor
-
- ;ex de, hl
- ;pop hl
- ;ex (sp), hl ; CALLEE Convention
-
- call __DIVU16_FAST
- ex de, hl ; hl = reminder (modulus)
- ; de = quotient
-
- ret
-
-
-__DIVI16: ; 16 bit signed division
- ; --- The following is OBSOLETE ---
- ; ex de, hl
- ; pop hl
- ; ex (sp), hl ; CALLEE Convention
-
-__DIVI16_FAST:
- ld a, d
- xor h
- ex af, af' ; BIT 7 of a contains result
-
- bit 7, d ; DE is negative?
- jr z, __DIVI16A
-
- ld a, e ; DE = -DE
- cpl
- ld e, a
- ld a, d
- cpl
- ld d, a
- inc de
-
-__DIVI16A:
- bit 7, h ; HL is negative?
- call nz, __NEGHL
-
-__DIVI16B:
- call __DIVU16_FAST
- ex af, af'
-
- or a
- ret p ; return if positive
- jp __NEGHL
-
-
-__MODI16: ; 16 bit modulus
- ; HL = Dividend, Stack Top = Divisor
-
- ;ex de, hl
- ;pop hl
- ;ex (sp), hl ; CALLEE Convention
-
- call __DIVI16_FAST
- ex de, hl ; hl = reminder (modulus)
- ; de = quotient
-
- ret
-
- pop namespace
+; ----------------------------------------------------------------
+; This file is released under the MIT License
+;
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
+
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/arith/div32.asm b/src/lib/arch/zxnext/runtime/arith/div32.asm
index 968a6879a..95758a3b3 100644
--- a/src/lib/arch/zxnext/runtime/arith/div32.asm
+++ b/src/lib/arch/zxnext/runtime/arith/div32.asm
@@ -1,144 +1,8 @@
-#include once
-
- ; ---------------------------------------------------------
- push namespace core
-
-__DIVU32: ; 32 bit unsigned division
- ; DEHL = Dividend, Stack Top = Divisor
- ; OPERANDS P = Dividend, Q = Divisor => OPERATION => P / Q
- ;
- ; Changes A, BC DE HL B'C' D'E' H'L'
- ; ---------------------------------------------------------
- exx
- pop hl ; return address
- pop de ; low part
- ex (sp), hl ; CALLEE Convention ; H'L'D'E' => Dividend
-
-__DIVU32START: ; Performs D'E'H'L' / HLDE
- ; Now switch to DIVIDEND = B'C'BC / DIVISOR = D'E'DE (A / B)
- push de ; push Lowpart(Q)
- ex de, hl ; DE = HL
- ld hl, 0
- exx
- ld b, h
- ld c, l
- pop hl
- push de
- ex de, hl
- ld hl, 0 ; H'L'HL = 0
- exx
- pop bc ; Pop HightPart(B) => B = B'C'BC
- exx
-
- ld a, 32 ; Loop count
-
-__DIV32LOOP:
- sll c ; B'C'BC << 1 ; Output most left bit to carry
- rl b
- exx
- rl c
- rl b
- exx
-
- adc hl, hl
- exx
- adc hl, hl
- exx
-
- sbc hl,de
- exx
- sbc hl,de
- exx
- jp nc, __DIV32NOADD ; use JP inside a loop for being faster
-
- add hl, de
- exx
- adc hl, de
- exx
- dec bc
-
-__DIV32NOADD:
- dec a
- jp nz, __DIV32LOOP ; use JP inside a loop for being faster
- ; At this point, quotient is stored in B'C'BC and the reminder in H'L'HL
-
- push hl
- exx
- pop de
- ex de, hl ; D'E'H'L' = 32 bits modulus
- push bc
- exx
- pop de ; DE = B'C'
- ld h, b
- ld l, c ; DEHL = quotient D'E'H'L' = Modulus
-
- ret ; DEHL = quotient, D'E'H'L' = Modulus
-
-
-
-__MODU32: ; 32 bit modulus for 32bit unsigned division
- ; DEHL = Dividend, Stack Top = Divisor (DE, HL)
-
- exx
- pop hl ; return address
- pop de ; low part
- ex (sp), hl ; CALLEE Convention ; H'L'D'E' => Dividend
-
- call __DIVU32START ; At return, modulus is at D'E'H'L'
-
-__MODU32START:
-
- exx
- push de
- push hl
-
- exx
- pop hl
- pop de
-
- ret
-
-
-__DIVI32: ; 32 bit signed division
- ; DEHL = Dividend, Stack Top = Divisor
- ; A = Dividend, B = Divisor => A / B
- exx
- pop hl ; return address
- pop de ; low part
- ex (sp), hl ; CALLEE Convention ; H'L'D'E' => Dividend
-
-__DIVI32START:
- exx
- ld a, d ; Save sign
- ex af, af'
- bit 7, d ; Negative?
- call nz, __NEG32 ; Negates DEHL
-
- exx ; Now works with H'L'D'E'
- ex af, af'
- xor h
- ex af, af' ; Stores sign of the result for later
-
- bit 7, h ; Negative?
- ex de, hl ; HLDE = DEHL
- call nz, __NEG32
- ex de, hl
-
- call __DIVU32START
- ex af, af' ; Recovers sign
- and 128 ; positive?
- ret z
-
- jp __NEG32 ; Negates DEHL and returns from there
-
-
-__MODI32: ; 32bits signed division modulus
- exx
- pop hl ; return address
- pop de ; low part
- ex (sp), hl ; CALLEE Convention ; H'L'D'E' => Dividend
-
- call __DIVI32START
- jp __MODU32START
-
- pop namespace
+; ----------------------------------------------------------------
+; This file is released under the MIT License
+;
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
+
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/arith/div8.asm b/src/lib/arch/zxnext/runtime/arith/div8.asm
index 465a7c2d3..5488bfb63 100644
--- a/src/lib/arch/zxnext/runtime/arith/div8.asm
+++ b/src/lib/arch/zxnext/runtime/arith/div8.asm
@@ -1,90 +1,8 @@
- ; --------------------------------
- push namespace core
-
-__DIVU8: ; 8 bit unsigned integer division
- ; Divides (Top of stack, High Byte) / A
- pop hl ; --------------------------------
- ex (sp), hl ; CALLEE
-
-__DIVU8_FAST: ; Does A / H
- ld l, h
- ld h, a ; At this point do H / L
-
- ld b, 8
- xor a ; A = 0, Carry Flag = 0
-
-__DIV8LOOP:
- sla h
- rla
- cp l
- jr c, __DIV8NOSUB
- sub l
- inc h
-
-__DIV8NOSUB:
- djnz __DIV8LOOP
-
- ld l, a ; save remainder
- ld a, h ;
-
- ret ; a = Quotient,
-
-
- ; --------------------------------
-__DIVI8: ; 8 bit signed integer division Divides (Top of stack) / A
- pop hl ; --------------------------------
- ex (sp), hl
-
-__DIVI8_FAST:
- ld e, a ; store operands for later
- ld c, h
-
- or a ; negative?
- jp p, __DIV8A
- neg ; Make it positive
-
-__DIV8A:
- ex af, af'
- ld a, h
- or a
- jp p, __DIV8B
- neg
- ld h, a ; make it positive
-
-__DIV8B:
- ex af, af'
-
- call __DIVU8_FAST
-
- ld a, c
- xor l ; bit 7 of A = 1 if result is negative
-
- ld a, h ; Quotient
- ret p ; return if positive
-
- neg
- ret
-
-
-__MODU8: ; 8 bit module. REturns A mod (Top of stack) (unsigned operands)
- pop hl
- ex (sp), hl ; CALLEE
-
-__MODU8_FAST: ; __FASTCALL__ entry
- call __DIVU8_FAST
- ld a, l ; Remainder
-
- ret ; a = Modulus
-
-
-__MODI8: ; 8 bit module. REturns A mod (Top of stack) (For singed operands)
- pop hl
- ex (sp), hl ; CALLEE
-
-__MODI8_FAST: ; __FASTCALL__ entry
- call __DIVI8_FAST
- ld a, l ; remainder
-
- ret ; a = Modulus
-
- pop namespace
+; ----------------------------------------------------------------
+; This file is released under the MIT License
+;
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
+
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/arith/divf.asm b/src/lib/arch/zxnext/runtime/arith/divf.asm
index 5269f3f8c..56194aa25 100644
--- a/src/lib/arch/zxnext/runtime/arith/divf.asm
+++ b/src/lib/arch/zxnext/runtime/arith/divf.asm
@@ -1,61 +1,8 @@
-#include once
-#include once
-
-; -------------------------------------------------------------
-; Floating point library using the FP ROM Calculator (ZX 48K)
-
-; All of them uses C EDHL registers as 1st paramter.
-; For binary operators, the 2n operator must be pushed into the
-; stack, in the order BC DE HL (B not used).
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
-; Uses CALLEE convention
-; -------------------------------------------------------------
-
- push namespace core
-
-__DIVF: ; Division
- PROC
- LOCAL __DIVBYZERO
- LOCAL TMP, ERR_SP
-
-TMP EQU 23629 ;(DEST)
-ERR_SP EQU 23613
-
- call __FPSTACK_PUSH2
-
- ld hl, (ERR_SP)
- ld (TMP), hl
- ld hl, __DIVBYZERO
- push hl
- ld (ERR_SP), sp
-
- ; ------------- ROM DIV
- rst 28h
- defb 01h ; EXCHANGE
- defb 05h ; DIV
- defb 38h; ; END CALC
-
- pop hl
- ld hl, (TMP)
- ld (ERR_SP), hl
-
- jp __FPSTACK_POP
-
-__DIVBYZERO:
- ld hl, (TMP)
- ld (ERR_SP), hl
-
- ld a, ERROR_NumberTooBig
- ld (ERR_NR), a
-
- ; Returns 0 on DIV BY ZERO error
- xor a
- ld b, a
- ld c, a
- ld d, a
- ld e, a
- ret
-
- ENDP
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
- pop namespace
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/arith/divf16.asm b/src/lib/arch/zxnext/runtime/arith/divf16.asm
index 2961c62fb..7b4144547 100644
--- a/src/lib/arch/zxnext/runtime/arith/divf16.asm
+++ b/src/lib/arch/zxnext/runtime/arith/divf16.asm
@@ -1,86 +1,8 @@
-#include once
-
-
- push namespace core
-
-__DIVF16: ; 16.16 Fixed point Division (signed)
-
- ; DE.HL = Dividend, Stack Top = Divisor
- ; A = Dividend, B = Divisor => A / B
- exx
- pop hl ; return address
- pop de ; low part
- ex (sp), hl ; CALLEE Convention ; H'L'D'E' => Dividend
- ex de, hl ; D'E'.H'L' Dividend
-
-__DIVF16START: ; FAST Entry: DEHL => Dividend, D'E'H'L' => Divisor
- ld a, d ; Save sign
- ex af, af'
- bit 7, d ; Negative?
- call nz, __NEG32 ; Negates DEHL
-
- exx ; Now works with D'E'.H'L'
- ex af, af'
- xor d
- ex af, af' ; Stores sign of the result for later
-
- bit 7, d ; Negative?
- call nz, __NEG32
- exx ; Now we have DE.HL => Dividend
-
- ld b, 16
-
-__SHIFTALOOP: ; Tries to shift Dividend to the left
- bit 7, d
- jp nz, __SHIFTB
- add hl, hl
- ex de, hl
- adc hl, hl
- ex de, hl
- djnz __SHIFTALOOP
- jp __DOF16_DIVRDY
-
-__SHIFTB: ; Cannot shift Dividend more to the left, try to shift Divisor to the right
- ld a, b
- exx
- ld b, a
- ; Divisor is in DEHL
-__SHIFTBLOOP:
- bit 1, l
- jp nz, __DOF16_DIVIDE
- sra d
- rr e
- rr h
- rr l
- djnz __SHIFTBLOOP
-
-__DOF16_DIVIDE:
- ld a, b
- exx
- ld b, a
-
-__DOF16_DIVRDY:
- exx
- ex de, hl
- push bc
- call __DIVU32START
- pop bc
-
- xor a
- or b
- jp z, __ENDF16DIV
-
-__SHIFTCLOOP:
- add hl, hl ; Shift DECIMAL PART << 1
- ex de, hl
- adc hl, hl ; Shift INTEGER PART << 1 Plus Carry
- ex de, hl
- djnz __SHIFTCLOOP
-
-__ENDF16DIV: ; Put the sign on the result
- ex af, af' ; Recovers sign
- and 128 ; positive?
- ret z
- jp __NEG32 ; Negates DEHL and returns from there
-
- pop namespace
+; ----------------------------------------------------------------
+; This file is released under the MIT License
+;
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)
+; ----------------------------------------------------------------
+
+#include once [arch:zx48k]
diff --git a/src/lib/arch/zxnext/runtime/arith/modf.asm b/src/lib/arch/zxnext/runtime/arith/modf.asm
index d37965eb7..0998f7245 100644
--- a/src/lib/arch/zxnext/runtime/arith/modf.asm
+++ b/src/lib/arch/zxnext/runtime/arith/modf.asm
@@ -1,27 +1,8 @@
-#include once
-
-; -------------------------------------------------------------
-; Floating point library using the FP ROM Calculator (ZX 48K)
-
-; All of them uses A EDCB registers as 1st paramter.
-; For binary operators, the 2n operator must be pushed into the
-; stack, in the order A DE BC.
+; ----------------------------------------------------------------
+; This file is released under the MIT License
;
-; Uses CALLEE convention
-; -------------------------------------------------------------
-
- push namespace core
-
-__MODF: ; MODULO
- call __FPSTACK_PUSH2 ; Enters B, A
-
- ; ------------- ROM DIV
- rst 28h
- defb 01h ; EXCHANGE
- defb 32h ; MOD
- defb 02h ; Discard (POP)
- defb 38h; ; END CALC
-
- jp __FPSTACK_POP
+; Copyleft (k) 2008
+; by Jose Rodriguez-Rosa (a.k.a. Boriel)