diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 9da15245e..8ad31de00 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,18 +1,18 @@
-[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}
+[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 2e337207c..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://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
+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 d68e4986c..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://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
+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/.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 d69ca1233..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.15.12
- 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/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/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/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/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 ca40be381..455d95296 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,1102 +1,1102 @@
-# 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 = "sys_platform == \"win32\" or platform_system == \"Windows\""
-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 = "78.1.1"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
-optional = false
-python-versions = ">=3.9"
-groups = ["dev"]
-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) ; sys_platform != \"cygwin\"", "ruff (>=0.8.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)", "platformdirs (>=4.2.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 (>=2.2)"]
-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.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"
-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 = "aa734f52ed314d0af56c786d5eae67b0964d6d199359f642a10418b5cafd1866"
+# 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 = "sys_platform == \"win32\" or platform_system == \"Windows\""
+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 = "78.1.1"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+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) ; sys_platform != \"cygwin\"", "ruff (>=0.8.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)", "platformdirs (>=4.2.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 (>=2.2)"]
+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.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"
+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 = "aa734f52ed314d0af56c786d5eae67b0964d6d199359f642a10418b5cafd1866"
diff --git a/pyproject.toml b/pyproject.toml
index 387408eb5..a52491fd2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,187 +1,187 @@
-[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 = ">=70.1.1,<79.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/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$',
- 'scratch/*',
- '.venv/*',
- 'venv/*',
-]
-
-[[tool.mypy.overrides]]
-module = [
- "src.ply.*",
-]
-follow_imports = "skip"
-
-[tool.ruff]
-line-length = 120
-target-version = "py314"
-exclude = [
- ".venv/",
- "venv/",
- "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",
-]
+[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 = ">=70.1.1,<79.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/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$',
+ 'scratch/*',
+ '.venv/*',
+ 'venv/*',
+]
+
+[[tool.mypy.overrides]]
+module = [
+ "src.ply.*",
+]
+follow_imports = "skip"
+
+[tool.ruff]
+line-length = 120
+target-version = "py314"
+exclude = [
+ ".venv/",
+ "venv/",
+ "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",
+]
diff --git a/src/arch/__init__.py b/src/arch/__init__.py
index fe5ba2cf3..241f40820 100755
--- a/src/arch/__init__.py
+++ b/src/arch/__init__.py
@@ -13,6 +13,7 @@
__all__ = (
"zx48k",
"zxnext",
+ "zx81sd",
)
AVAILABLE_ARCHITECTURES = __all__
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/zx81sd/README.md b/src/arch/zx81sd/README.md
new file mode 100644
index 000000000..8e781eb6c
--- /dev/null
+++ b/src/arch/zx81sd/README.md
@@ -0,0 +1,110 @@
+# `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/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/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..6efddcb0b
--- /dev/null
+++ b/src/arch/zx81sd/backend/main.py
@@ -0,0 +1,196 @@
+# --------------------------------------------------------------------
+# 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 vectors.asm — RST vectors + 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 (vectors.asm pads 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
+]
+
+_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 vectors.asm (RST vectors, included from sysvars.asm)
+ $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")
+ output.append("di") # $0038: RST $38 IM1 — permanent DI, never reached
+ output.append("halt")
+ 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..e9f2d7fef
--- /dev/null
+++ b/src/arch/zx81sd/doc/MAP.md
@@ -0,0 +1,560 @@
+# 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.
diff --git a/src/arch/zx81sd/doc/PRECAUTIONS.md b/src/arch/zx81sd/doc/PRECAUTIONS.md
new file mode 100644
index 000000000..3ab31ace0
--- /dev/null
+++ b/src/arch/zx81sd/doc/PRECAUTIONS.md
@@ -0,0 +1,216 @@
+# 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 equivalence table is in
+ [`../../../lib/arch/zx81sd/runtime/sysvars.asm`](../../../lib/arch/zx81sd/runtime/sysvars.asm)
+ (all live 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 wait on `HALT`/`EI` to synchronize
+
+The zx81sd runtime always runs with interrupts disabled (`DI`); the
+`$0038` vector is only a `DI;HALT` trap, not a real interrupt handler.
+Any code (typically inline ASM in an example, not the stdlib) that
+does `EI` followed by `HALT` waiting for the Spectrum ROM's 50Hz pulse
+**hangs forever** — nothing ever wakes it up.
+
+- **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.
+
+## See also
+
+- [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/USAGE.md b/src/arch/zx81sd/doc/USAGE.md
new file mode 100644
index 000000000..43571baf8
--- /dev/null
+++ b/src/arch/zx81sd/doc/USAGE.md
@@ -0,0 +1,88 @@
+# 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.
+
+## 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/split_sd81.py b/src/arch/zx81sd/tools/split_sd81.py
new file mode 100755
index 000000000..9f8bfc9e6
--- /dev/null
+++ b/src/arch/zx81sd/tools/split_sd81.py
@@ -0,0 +1,211 @@
+#!/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
+
+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[str]:
+ with open(input_path, "rb") as f:
+ data = f.read()
+
+ if not data:
+ sys.exit(f"Error: {input_path} is empty")
+
+ pages_written = []
+ page_num = FIRST_PAGE
+ offset = 0
+
+ while offset < len(data):
+ chunk = data[offset : offset + PAGE_SIZE]
+
+ # 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(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(page_files: list[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 i, page_file in enumerate(page_files):
+ page_num = FIRST_PAGE + i
+ 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).")
+ else:
+ print(f"Pages {FIRST_PAGE} to {FIRST_PAGE + len(pages) - 1}.")
+
+ 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/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/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/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/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..a9ddd84f9
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/bootstrap.asm
@@ -0,0 +1,106 @@
+; 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
+
+ ; 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/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..ec0a4c01a
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/sysvars.asm
@@ -0,0 +1,77 @@
+; -----------------------------------------------------------------------
+; 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)
+
+; Tamaño total del bloque de sysvars: $83 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/vectors.asm b/src/lib/arch/zx81sd/runtime/vectors.asm
new file mode 100644
index 000000000..64a3df145
--- /dev/null
+++ b/src/lib/arch/zx81sd/runtime/vectors.asm
@@ -0,0 +1,66 @@
+; VECTORS — Tabla de vectores RST del Z80 para ZX81 + SD81 Booster
+;
+; Ocupa $0000-$00FF (primer bloque de la zona de sistema).
+; Reemplaza la ROM del ZX81 (bloque 0 remapeado a página 8 por el stage 1).
+;
+; Tras el remapeo de bloque 0, el stage 1 salta a $0100 (stage 2 bootstrap).
+; El vector RST 0 redirige allí también, por si algo fuerza un reset software.
+
+ push namespace core
+
+ org $0000
+
+; $0000 — RST 0 / Reset software: salta al stage 2
+ jp $0100
+
+ defs $0008 - $, $00
+
+; $0008 — RST $08 (handler de errores Spectrum): redirige a __ERROR
+ jp __ERROR
+
+ defs $0010 - $, $00
+
+; $0010 — RST $10 (PRINT A en Spectrum): no usado, cuelga de forma segura
+ di
+ halt
+
+ defs $0018 - $, $00
+
+; $0018 — RST $18: no usado
+ di
+ halt
+
+ defs $0020 - $, $00
+
+; $0020 — RST $20: no usado
+ di
+ halt
+
+ defs $0028 - $, $00
+
+; $0028 — RST $28 (FP calculator Spectrum): stub, no debe llegarse aquí
+; con __ZXB_NO_FLOAT activo el compilador no genera RST $28
+ di
+ halt
+
+ defs $0030 - $, $00
+
+; $0030 — RST $30: no usado
+ di
+ halt
+
+ defs $0038 - $, $00
+
+; $0038 — RST $38 (IM1 interrupt): DI permanente, nunca debería llegar
+ di
+ halt
+
+ defs $0066 - $, $00
+
+; $0066 — NMI handler: NMI desactivadas por el cargador BASIC (modo FAST),
+; pero el vector debe existir por si acaso
+ retn
+
+ defs $0100 - $, $00 ; relleno hasta inicio del stage 2 en $0100
+
+ 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/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/spectranet.inc b/src/lib/arch/zxnext/runtime/spectranet.inc
index 052e61f22..73fbd2c1e 100644
--- a/src/lib/arch/zxnext/runtime/spectranet.inc
+++ b/src/lib/arch/zxnext/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/zxnext/stdlib/README b/src/lib/arch/zxnext/stdlib/README
index 401199747..7d24b7e8a 100644
--- a/src/lib/arch/zxnext/stdlib/README
+++ b/src/lib/arch/zxnext/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/parsetab/tabs.dbm.bak b/src/parsetab/tabs.dbm.bak
index 881659c7d..286dd8adf 100644
--- a/src/parsetab/tabs.dbm.bak
+++ b/src/parsetab/tabs.dbm.bak
@@ -1,4 +1,4 @@
-'zxbpp', (0, 71563)
-'asmparse', (71680, 234798)
-'zxnext_asmparse', (306688, 259879)
-'zxbparser', (566784, 641214)
+'zxbpp', (0, 71563)
+'asmparse', (71680, 234798)
+'zxnext_asmparse', (306688, 259879)
+'zxbparser', (566784, 641214)
diff --git a/src/parsetab/tabs.dbm.dir b/src/parsetab/tabs.dbm.dir
index 881659c7d..286dd8adf 100644
--- a/src/parsetab/tabs.dbm.dir
+++ b/src/parsetab/tabs.dbm.dir
@@ -1,4 +1,4 @@
-'zxbpp', (0, 71563)
-'asmparse', (71680, 234798)
-'zxnext_asmparse', (306688, 259879)
-'zxbparser', (566784, 641214)
+'zxbpp', (0, 71563)
+'asmparse', (71680, 234798)
+'zxnext_asmparse', (306688, 259879)
+'zxbparser', (566784, 641214)
diff --git a/src/zxbpp/zxbpp.py b/src/zxbpp/zxbpp.py
index 53c7f6449..9339e4d8c 100755
--- a/src/zxbpp/zxbpp.py
+++ b/src/zxbpp/zxbpp.py
@@ -167,6 +167,12 @@ def set_include_path():
pwd = get_include_path(arch_)
INCLUDE_MAP[arch_] = [os.path.join(pwd, "stdlib"), os.path.join(pwd, "runtime")]
+ # zx81sd inherits runtime files from zx48k (only overrides specific ones).
+ # zx81sd paths take priority: its own files shadow zx48k equivalents.
+ if "zx81sd" in INCLUDE_MAP:
+ zx48k_pwd = get_include_path("zx48k")
+ INCLUDE_MAP["zx81sd"].extend([os.path.join(zx48k_pwd, "stdlib"), os.path.join(zx48k_pwd, "runtime")])
+
INCLUDEPATH = INCLUDE_MAP.get(config.OPTIONS.architecture, [])
diff --git a/tests/functional/Makefile b/tests/functional/Makefile
index 55a0ed282..88b37ca88 100644
--- a/tests/functional/Makefile
+++ b/tests/functional/Makefile
@@ -1,41 +1,41 @@
-# vim:noet:ts=4:
-.PHONY: test test_ prepro asm bas all
-
-all: test test_
-
-diffbas:
- ./test.py -d '**/*.bas'
-
-test: prepro bin asm bas
-
-test_:
- pytest . -n auto --no-cov -k "cmdline"
-
-prepro:
- ./test.py zxbpp/*.bi
-
-asm:
- ./test.py asm/*.asm
-
-# This includes all .bas BASIC programs having a corresponding .asm
-bas:
- ./test.py '**/*.bas'
-
-# This only includes all .bas BASIC programs having a
-# corresponding .asm which starts by a digit.
-basic_tests:
- ./test.py '**/[0-9]*.bas'
-
-bin:
- ./test.py '**/tzx_*.bas' '**/tap_*.bas' '**/sna_*.bas' '**/z80_*.bas'
-
-# Measures coverage using only basic tests
-.PHONY: basic_coverage
-basic_coverage:
- ./coverage.sh [0-9]*.bas
- coverage html -d basic_coverage
-
-.PHONY: coverage
-coverage:
- ./coverage.sh *.bas
- coverage html -d coverage
+# vim:noet:ts=4:
+.PHONY: test test_ prepro asm bas all
+
+all: test test_
+
+diffbas:
+ ./test.py -d '**/*.bas'
+
+test: prepro bin asm bas
+
+test_:
+ pytest . -n auto --no-cov -k "cmdline"
+
+prepro:
+ ./test.py zxbpp/*.bi
+
+asm:
+ ./test.py asm/*.asm
+
+# This includes all .bas BASIC programs having a corresponding .asm
+bas:
+ ./test.py '**/*.bas'
+
+# This only includes all .bas BASIC programs having a
+# corresponding .asm which starts by a digit.
+basic_tests:
+ ./test.py '**/[0-9]*.bas'
+
+bin:
+ ./test.py '**/tzx_*.bas' '**/tap_*.bas' '**/sna_*.bas' '**/z80_*.bas'
+
+# Measures coverage using only basic tests
+.PHONY: basic_coverage
+basic_coverage:
+ ./coverage.sh [0-9]*.bas
+ coverage html -d basic_coverage
+
+.PHONY: coverage
+coverage:
+ ./coverage.sh *.bas
+ coverage html -d coverage
diff --git a/tests/functional/arch/zx48k/border00_IC.ic b/tests/functional/arch/zx48k/border00_IC.ic
index 14b733047..7031602f2 100644
--- a/tests/functional/arch/zx48k/border00_IC.ic
+++ b/tests/functional/arch/zx48k/border00_IC.ic
@@ -1,4 +1,4 @@
-('fparamu8', '7')
-('call', '.core.BORDER', '0')
-('end', '0')
-('inline', ';; --- end of user code ---')
+('fparamu8', '7')
+('call', '.core.BORDER', '0')
+('end', '0')
+('inline', ';; --- end of user code ---')
diff --git a/tests/functional/arch/zx48k/func_call_IC.ic b/tests/functional/arch/zx48k/func_call_IC.ic
index dbee01323..b061fa1b3 100644
--- a/tests/functional/arch/zx48k/func_call_IC.ic
+++ b/tests/functional/arch/zx48k/func_call_IC.ic
@@ -1,17 +1,17 @@
-('paramf', '0.0')
-('call', '_test', '0')
-('call', '.core.__MEM_FREE', '0')
-('end', '0')
-('label', '_test')
-('enter', '0')
-('ploadf', 't3', '0')
-('jzerof', 't3', '.LABEL.__LABEL1')
-('retstr', '#.LABEL.__LABEL2', '_test__leave')
-('label', '.LABEL.__LABEL1')
-('inline', '#line 3 "func_call_IC.bas"')
-('inline', '\nld hl, 0\n')
-('inline', '#line 6 "func_call_IC.bas"')
-('label', '_test__leave')
-('leave', '6')
-('vard', '.LABEL.__LABEL2', "['0008', '6E', '6F', '74', '20', '7A', '65', '72', '6F']")
-('inline', ';; --- end of user code ---')
+('paramf', '0.0')
+('call', '_test', '0')
+('call', '.core.__MEM_FREE', '0')
+('end', '0')
+('label', '_test')
+('enter', '0')
+('ploadf', 't3', '0')
+('jzerof', 't3', '.LABEL.__LABEL1')
+('retstr', '#.LABEL.__LABEL2', '_test__leave')
+('label', '.LABEL.__LABEL1')
+('inline', '#line 3 "func_call_IC.bas"')
+('inline', '\nld hl, 0\n')
+('inline', '#line 6 "func_call_IC.bas"')
+('label', '_test__leave')
+('leave', '6')
+('vard', '.LABEL.__LABEL2', "['0008', '6E', '6F', '74', '20', '7A', '65', '72', '6F']")
+('inline', ';; --- end of user code ---')
diff --git a/tests/functional/zxbpp/builtin.out b/tests/functional/zxbpp/builtin.out
index d0ceec53c..3f41ec510 100644
--- a/tests/functional/zxbpp/builtin.out
+++ b/tests/functional/zxbpp/builtin.out
@@ -1,28 +1,28 @@
-#line 1 "builtin.bi"
-
-
-
-#line 5 "builtin.bi"
-#line 6 "builtin.bi"
-
-"builtin.bi" + "a" 7 + 5
-"builtin.bi" + "a" 8 + 5
-
-5 + 1
-
-"builtin.bi"
-13
-
-
-#line 17 "builtin.bi"
-#line 18 "builtin.bi"
-
-0
-"a"
-
-5 + 1
-
-#line 25 "builtin.bi"
-5 + 1
-
-0 + "a" "a" + 5
+#line 1 "builtin.bi"
+
+
+
+#line 5 "builtin.bi"
+#line 6 "builtin.bi"
+
+"builtin.bi" + "a" 7 + 5
+"builtin.bi" + "a" 8 + 5
+
+5 + 1
+
+"builtin.bi"
+13
+
+
+#line 17 "builtin.bi"
+#line 18 "builtin.bi"
+
+0
+"a"
+
+5 + 1
+
+#line 25 "builtin.bi"
+5 + 1
+
+0 + "a" "a" + 5
diff --git a/tests/functional/zxbpp/emook0.out b/tests/functional/zxbpp/emook0.out
index b04b111e6..a297f8dae 100644
--- a/tests/functional/zxbpp/emook0.out
+++ b/tests/functional/zxbpp/emook0.out
@@ -1,9 +1,9 @@
-#line 1 "emook0.bi"
-
-
-ASM
-
- LD A,A
- LD BC,254
-#line 7
-end Asm
+#line 1 "emook0.bi"
+
+
+ASM
+
+ LD A,A
+ LD BC,254
+#line 7
+end Asm
diff --git a/tests/functional/zxbpp/iflogic.out b/tests/functional/zxbpp/iflogic.out
index e81ffc7be..7e1427ea9 100644
--- a/tests/functional/zxbpp/iflogic.out
+++ b/tests/functional/zxbpp/iflogic.out
@@ -1,18 +1,18 @@
-#line 1 "iflogic.bi"
-
-#line 3 "iflogic.bi"
-#line 4 "iflogic.bi"
-
-
-PRINT "Or works"
-#line 8 "iflogic.bi"
-
-
-PRINT "And works"
-#line 12 "iflogic.bi"
-
-
-PRINT "Parenthesis works"
-#line 16 "iflogic.bi"
-
-#line 20 "iflogic.bi"
+#line 1 "iflogic.bi"
+
+#line 3 "iflogic.bi"
+#line 4 "iflogic.bi"
+
+
+PRINT "Or works"
+#line 8 "iflogic.bi"
+
+
+PRINT "And works"
+#line 12 "iflogic.bi"
+
+
+PRINT "Parenthesis works"
+#line 16 "iflogic.bi"
+
+#line 20 "iflogic.bi"
diff --git a/tests/functional/zxbpp/init_dot.out b/tests/functional/zxbpp/init_dot.out
index 422b07b01..a30ea1f9a 100644
--- a/tests/functional/zxbpp/init_dot.out
+++ b/tests/functional/zxbpp/init_dot.out
@@ -1,11 +1,11 @@
-#line 1 "init_dot.bi"
-
-#init "XX.main"
-
-jp XX.main
-push namespace XX
-main:
- nop
-
-pop namespace
-ret
+#line 1 "init_dot.bi"
+
+#init "XX.main"
+
+jp XX.main
+push namespace XX
+main:
+ nop
+
+pop namespace
+ret
diff --git a/tests/functional/zxbpp/line_asm.out b/tests/functional/zxbpp/line_asm.out
index bd9d6ec71..f3e4d721e 100644
--- a/tests/functional/zxbpp/line_asm.out
+++ b/tests/functional/zxbpp/line_asm.out
@@ -1,4 +1,4 @@
-#line 1 "line_asm.bi"
-ASM
-NOP
-END ASM
+#line 1 "line_asm.bi"
+ASM
+NOP
+END ASM
diff --git a/tests/functional/zxbpp/once.out b/tests/functional/zxbpp/once.out
index ddfee5fe1..d639f4c4a 100644
--- a/tests/functional/zxbpp/once.out
+++ b/tests/functional/zxbpp/once.out
@@ -1,8 +1,8 @@
-#line 1 "once.bi"
-
-
-DIM a as Ubyte
-
-
-
-DIM b as Ubyte
+#line 1 "once.bi"
+
+
+DIM a as Ubyte
+
+
+
+DIM b as Ubyte
diff --git a/tests/functional/zxbpp/once_base.out b/tests/functional/zxbpp/once_base.out
index 9f0d4c4ab..cbdb6a01b 100644
--- a/tests/functional/zxbpp/once_base.out
+++ b/tests/functional/zxbpp/once_base.out
@@ -1,12 +1,12 @@
-#line 1 "once_base.bi"
-
-#line 1 "once.bi"
-
-
-DIM a as Ubyte
-
-
-
-DIM b as Ubyte
-#line 3 "once_base.bi"
-
+#line 1 "once_base.bi"
+
+#line 1 "once.bi"
+
+
+DIM a as Ubyte
+
+
+
+DIM b as Ubyte
+#line 3 "once_base.bi"
+
diff --git a/tests/functional/zxbpp/other_arch.out b/tests/functional/zxbpp/other_arch.out
index 161311b03..5b6695f33 100644
--- a/tests/functional/zxbpp/other_arch.out
+++ b/tests/functional/zxbpp/other_arch.out
@@ -1,255 +1,255 @@
-#line 1 "other_arch.bi"
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-
-
-
-
-
-
-
-
-
-
-
-#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-
-#pragma push(case_insensitive)
-#pragma case_insensitive = TRUE
-
-
-
-
-
-
-
-
-
-
-
-function attr(byval row as ubyte, byval col as ubyte) as ubyte
- asm
- push namespace core
-
- PROC
- LOCAL __ATTR_END
-
- ld e, (ix+7)
- ld d, (ix+5)
-
-
- call __IN_SCREEN
- jr nc, __ATTR_END
-
- call __ATTR_ADDR
- ld a, (hl)
-
-__ATTR_END:
- ENDP
-
- pop namespace
- end asm
-
-end function
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-sub setattr(byval row as ubyte, byval col as ubyte, byval value as ubyte)
- asm
- push namespace core
-
- PROC
- LOCAL __ATTR_END
-
- ld e, (ix+7)
- ld d, (ix+5)
-
-
- call __IN_SCREEN
- jr nc, __ATTR_END
-
- call __ATTR_ADDR
- ld a, (ix+9)
- ld (hl), a
-
-__ATTR_END:
- ENDP
-
- pop namespace
- end asm
-
-end sub
-
-
-
-
-
-
-
-
-
-
-
-function fastcall attraddr(byval row as ubyte, byval col as ubyte) as uinteger
- asm
- push namespace core
-
- pop hl
- ex (sp), hl
- ld d, a
- ld e, h
- jp __ATTR_ADDR
- pop namespace
- end asm
-end function
-
-
-
-#pragma pop(case_insensitive)
-
-
-#require "attr.asm"
-
-
-#require "in_screen.asm"
-
-#line 125 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-#line 2 "other_arch.bi"
-#line 1 "/zxbasic/src/lib/arch/zxnext/stdlib/hex.bas"
-
-
-
-
-
-
-
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/hex.bas"
-
-
-
-
-
-
-
-
-
-
-
-#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/hex.bas"
-
-#pragma push(case_insensitive)
-#pragma case_insensitive = TRUE
-
-#pragma push(string_base)
-#pragma string_base = 0
-
-
-
-
-
-
-
-
-
-
-function FASTCALL hex(num as ULong) as String
- asm
- push namespace core
- PROC
- LOCAL SUB_CHAR
- LOCAL SUB_CHAR2
- LOCAL END_CHAR
- LOCAL DIGIT
-
- push hl
- push de
- ld bc,10
- call __MEM_ALLOC
- ld a, h
- or l
- pop de
- pop bc
- ret z
-
- push hl
- ld (hl), 8
- inc hl
- ld (hl), 0
- inc hl
-
- call DIGIT
- ld d, e
- call DIGIT
- ld d, b
- call DIGIT
- ld d, c
- call DIGIT
- pop hl
- ret
-
-DIGIT:
- ld a, d
- call SUB_CHAR
- ld a, d
- jr SUB_CHAR2
-
-SUB_CHAR:
- rrca
- rrca
- rrca
- rrca
-
-SUB_CHAR2:
- and 0Fh
- add a, '0'
- cp '9' + 1
- jr c, END_CHAR
- add a, 7
-
-END_CHAR:
- ld (hl), a
- inc hl
- ret
-
- ENDP
- pop namespace
- end asm
-end function
-
-
-
-function hex16(n as UInteger) as String
- Dim a$ as String
- a$ = hex(n)
- return a$(4 TO 7)
-end function
-
-
-
-Function hex8 (n as UByte) as String
- Dim res$ as String
-
- res$ = hex(n)
- return res$(6 TO 7)
-end function
-
-
-#pragma pop(string_base)
-#pragma pop(case_insensitive)
-
-
-#require "mem/alloc.asm"
-
-#line 118 "/zxbasic/src/lib/arch/zx48k/stdlib/hex.bas"
-#line 9 "/zxbasic/src/lib/arch/zxnext/stdlib/hex.bas"
-#line 3 "other_arch.bi"
+#line 1 "other_arch.bi"
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+
+
+
+
+
+
+
+
+
+
+
+#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = TRUE
+
+
+
+
+
+
+
+
+
+
+
+function attr(byval row as ubyte, byval col as ubyte) as ubyte
+ asm
+ push namespace core
+
+ PROC
+ LOCAL __ATTR_END
+
+ ld e, (ix+7)
+ ld d, (ix+5)
+
+
+ call __IN_SCREEN
+ jr nc, __ATTR_END
+
+ call __ATTR_ADDR
+ ld a, (hl)
+
+__ATTR_END:
+ ENDP
+
+ pop namespace
+ end asm
+
+end function
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+sub setattr(byval row as ubyte, byval col as ubyte, byval value as ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL __ATTR_END
+
+ ld e, (ix+7)
+ ld d, (ix+5)
+
+
+ call __IN_SCREEN
+ jr nc, __ATTR_END
+
+ call __ATTR_ADDR
+ ld a, (ix+9)
+ ld (hl), a
+
+__ATTR_END:
+ ENDP
+
+ pop namespace
+ end asm
+
+end sub
+
+
+
+
+
+
+
+
+
+
+
+function fastcall attraddr(byval row as ubyte, byval col as ubyte) as uinteger
+ asm
+ push namespace core
+
+ pop hl
+ ex (sp), hl
+ ld d, a
+ ld e, h
+ jp __ATTR_ADDR
+ pop namespace
+ end asm
+end function
+
+
+
+#pragma pop(case_insensitive)
+
+
+#require "attr.asm"
+
+
+#require "in_screen.asm"
+
+#line 125 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+#line 2 "other_arch.bi"
+#line 1 "/zxbasic/src/lib/arch/zxnext/stdlib/hex.bas"
+
+
+
+
+
+
+
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/hex.bas"
+
+
+
+
+
+
+
+
+
+
+
+#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/hex.bas"
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = TRUE
+
+#pragma push(string_base)
+#pragma string_base = 0
+
+
+
+
+
+
+
+
+
+
+function FASTCALL hex(num as ULong) as String
+ asm
+ push namespace core
+ PROC
+ LOCAL SUB_CHAR
+ LOCAL SUB_CHAR2
+ LOCAL END_CHAR
+ LOCAL DIGIT
+
+ push hl
+ push de
+ ld bc,10
+ call __MEM_ALLOC
+ ld a, h
+ or l
+ pop de
+ pop bc
+ ret z
+
+ push hl
+ ld (hl), 8
+ inc hl
+ ld (hl), 0
+ inc hl
+
+ call DIGIT
+ ld d, e
+ call DIGIT
+ ld d, b
+ call DIGIT
+ ld d, c
+ call DIGIT
+ pop hl
+ ret
+
+DIGIT:
+ ld a, d
+ call SUB_CHAR
+ ld a, d
+ jr SUB_CHAR2
+
+SUB_CHAR:
+ rrca
+ rrca
+ rrca
+ rrca
+
+SUB_CHAR2:
+ and 0Fh
+ add a, '0'
+ cp '9' + 1
+ jr c, END_CHAR
+ add a, 7
+
+END_CHAR:
+ ld (hl), a
+ inc hl
+ ret
+
+ ENDP
+ pop namespace
+ end asm
+end function
+
+
+
+function hex16(n as UInteger) as String
+ Dim a$ as String
+ a$ = hex(n)
+ return a$(4 TO 7)
+end function
+
+
+
+Function hex8 (n as UByte) as String
+ Dim res$ as String
+
+ res$ = hex(n)
+ return res$(6 TO 7)
+end function
+
+
+#pragma pop(string_base)
+#pragma pop(case_insensitive)
+
+
+#require "mem/alloc.asm"
+
+#line 118 "/zxbasic/src/lib/arch/zx48k/stdlib/hex.bas"
+#line 9 "/zxbasic/src/lib/arch/zxnext/stdlib/hex.bas"
+#line 3 "other_arch.bi"
diff --git a/tests/functional/zxbpp/prepro00.out b/tests/functional/zxbpp/prepro00.out
index 7b2942837..af0893179 100644
--- a/tests/functional/zxbpp/prepro00.out
+++ b/tests/functional/zxbpp/prepro00.out
@@ -1,128 +1,128 @@
-#line 1 "prepro00.bi"
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-
-
-
-
-
-
-
-
-
-
-
-#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-
-#pragma push(case_insensitive)
-#pragma case_insensitive = TRUE
-
-
-
-
-
-
-
-
-
-
-
-function attr(byval row as ubyte, byval col as ubyte) as ubyte
- asm
- push namespace core
-
- PROC
- LOCAL __ATTR_END
-
- ld e, (ix+7)
- ld d, (ix+5)
-
-
- call __IN_SCREEN
- jr nc, __ATTR_END
-
- call __ATTR_ADDR
- ld a, (hl)
-
-__ATTR_END:
- ENDP
-
- pop namespace
- end asm
-
-end function
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-sub setattr(byval row as ubyte, byval col as ubyte, byval value as ubyte)
- asm
- push namespace core
-
- PROC
- LOCAL __ATTR_END
-
- ld e, (ix+7)
- ld d, (ix+5)
-
-
- call __IN_SCREEN
- jr nc, __ATTR_END
-
- call __ATTR_ADDR
- ld a, (ix+9)
- ld (hl), a
-
-__ATTR_END:
- ENDP
-
- pop namespace
- end asm
-
-end sub
-
-
-
-
-
-
-
-
-
-
-
-function fastcall attraddr(byval row as ubyte, byval col as ubyte) as uinteger
- asm
- push namespace core
-
- pop hl
- ex (sp), hl
- ld d, a
- ld e, h
- jp __ATTR_ADDR
- pop namespace
- end asm
-end function
-
-
-
-#pragma pop(case_insensitive)
-
-
-#require "attr.asm"
-
-
-#require "in_screen.asm"
-
-#line 125 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-#line 2 "prepro00.bi"
-PRINT "HELLO"
+#line 1 "prepro00.bi"
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+
+
+
+
+
+
+
+
+
+
+
+#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = TRUE
+
+
+
+
+
+
+
+
+
+
+
+function attr(byval row as ubyte, byval col as ubyte) as ubyte
+ asm
+ push namespace core
+
+ PROC
+ LOCAL __ATTR_END
+
+ ld e, (ix+7)
+ ld d, (ix+5)
+
+
+ call __IN_SCREEN
+ jr nc, __ATTR_END
+
+ call __ATTR_ADDR
+ ld a, (hl)
+
+__ATTR_END:
+ ENDP
+
+ pop namespace
+ end asm
+
+end function
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+sub setattr(byval row as ubyte, byval col as ubyte, byval value as ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL __ATTR_END
+
+ ld e, (ix+7)
+ ld d, (ix+5)
+
+
+ call __IN_SCREEN
+ jr nc, __ATTR_END
+
+ call __ATTR_ADDR
+ ld a, (ix+9)
+ ld (hl), a
+
+__ATTR_END:
+ ENDP
+
+ pop namespace
+ end asm
+
+end sub
+
+
+
+
+
+
+
+
+
+
+
+function fastcall attraddr(byval row as ubyte, byval col as ubyte) as uinteger
+ asm
+ push namespace core
+
+ pop hl
+ ex (sp), hl
+ ld d, a
+ ld e, h
+ jp __ATTR_ADDR
+ pop namespace
+ end asm
+end function
+
+
+
+#pragma pop(case_insensitive)
+
+
+#require "attr.asm"
+
+
+#require "in_screen.asm"
+
+#line 125 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+#line 2 "prepro00.bi"
+PRINT "HELLO"
diff --git a/tests/functional/zxbpp/prepro01.out b/tests/functional/zxbpp/prepro01.out
index 38fe92ced..bcc03a728 100644
--- a/tests/functional/zxbpp/prepro01.out
+++ b/tests/functional/zxbpp/prepro01.out
@@ -1,129 +1,129 @@
-#line 1 "prepro01.bi"
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-
-
-
-
-
-
-
-
-
-
-
-#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-
-#pragma push(case_insensitive)
-#pragma case_insensitive = TRUE
-
-
-
-
-
-
-
-
-
-
-
-function attr(byval row as ubyte, byval col as ubyte) as ubyte
- asm
- push namespace core
-
- PROC
- LOCAL __ATTR_END
-
- ld e, (ix+7)
- ld d, (ix+5)
-
-
- call __IN_SCREEN
- jr nc, __ATTR_END
-
- call __ATTR_ADDR
- ld a, (hl)
-
-__ATTR_END:
- ENDP
-
- pop namespace
- end asm
-
-end function
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-sub setattr(byval row as ubyte, byval col as ubyte, byval value as ubyte)
- asm
- push namespace core
-
- PROC
- LOCAL __ATTR_END
-
- ld e, (ix+7)
- ld d, (ix+5)
-
-
- call __IN_SCREEN
- jr nc, __ATTR_END
-
- call __ATTR_ADDR
- ld a, (ix+9)
- ld (hl), a
-
-__ATTR_END:
- ENDP
-
- pop namespace
- end asm
-
-end sub
-
-
-
-
-
-
-
-
-
-
-
-function fastcall attraddr(byval row as ubyte, byval col as ubyte) as uinteger
- asm
- push namespace core
-
- pop hl
- ex (sp), hl
- ld d, a
- ld e, h
- jp __ATTR_ADDR
- pop namespace
- end asm
-end function
-
-
-
-#pragma pop(case_insensitive)
-
-
-#require "attr.asm"
-
-
-#require "in_screen.asm"
-
-#line 125 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-#line 2 "prepro01.bi"
-
-PRINT "HOLA"
+#line 1 "prepro01.bi"
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+
+
+
+
+
+
+
+
+
+
+
+#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = TRUE
+
+
+
+
+
+
+
+
+
+
+
+function attr(byval row as ubyte, byval col as ubyte) as ubyte
+ asm
+ push namespace core
+
+ PROC
+ LOCAL __ATTR_END
+
+ ld e, (ix+7)
+ ld d, (ix+5)
+
+
+ call __IN_SCREEN
+ jr nc, __ATTR_END
+
+ call __ATTR_ADDR
+ ld a, (hl)
+
+__ATTR_END:
+ ENDP
+
+ pop namespace
+ end asm
+
+end function
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+sub setattr(byval row as ubyte, byval col as ubyte, byval value as ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL __ATTR_END
+
+ ld e, (ix+7)
+ ld d, (ix+5)
+
+
+ call __IN_SCREEN
+ jr nc, __ATTR_END
+
+ call __ATTR_ADDR
+ ld a, (ix+9)
+ ld (hl), a
+
+__ATTR_END:
+ ENDP
+
+ pop namespace
+ end asm
+
+end sub
+
+
+
+
+
+
+
+
+
+
+
+function fastcall attraddr(byval row as ubyte, byval col as ubyte) as uinteger
+ asm
+ push namespace core
+
+ pop hl
+ ex (sp), hl
+ ld d, a
+ ld e, h
+ jp __ATTR_ADDR
+ pop namespace
+ end asm
+end function
+
+
+
+#pragma pop(case_insensitive)
+
+
+#require "attr.asm"
+
+
+#require "in_screen.asm"
+
+#line 125 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+#line 2 "prepro01.bi"
+
+PRINT "HOLA"
diff --git a/tests/functional/zxbpp/prepro02.out b/tests/functional/zxbpp/prepro02.out
index 68aa48ce2..3c3d5c7b9 100644
--- a/tests/functional/zxbpp/prepro02.out
+++ b/tests/functional/zxbpp/prepro02.out
@@ -1,8 +1,8 @@
-#line 1 "prepro02.bi"
-
-
-
-
-#line 6 "prepro02.bi"
-
-
+#line 1 "prepro02.bi"
+
+
+
+
+#line 6 "prepro02.bi"
+
+
diff --git a/tests/functional/zxbpp/prepro03.out b/tests/functional/zxbpp/prepro03.out
index 29c0d0931..c4b2fe796 100644
--- a/tests/functional/zxbpp/prepro03.out
+++ b/tests/functional/zxbpp/prepro03.out
@@ -1,9 +1,9 @@
-#line 1 "prepro03.bi"
-
-#line 4 "prepro03.bi"
-#line 5 "prepro03.bi"
-
-x
- this line must be compiled
-#line 7
-y
+#line 1 "prepro03.bi"
+
+#line 4 "prepro03.bi"
+#line 5 "prepro03.bi"
+
+x
+ this line must be compiled
+#line 7
+y
diff --git a/tests/functional/zxbpp/prepro04.out b/tests/functional/zxbpp/prepro04.out
index 99b4448a6..ac005c346 100644
--- a/tests/functional/zxbpp/prepro04.out
+++ b/tests/functional/zxbpp/prepro04.out
@@ -1,6 +1,6 @@
-#line 1 "prepro04.bi"
-
-#line 5 "prepro04.bi"
-
-#line 7 "prepro04.bi"
-
+#line 1 "prepro04.bi"
+
+#line 5 "prepro04.bi"
+
+#line 7 "prepro04.bi"
+
diff --git a/tests/functional/zxbpp/prepro05.out b/tests/functional/zxbpp/prepro05.out
index 31c4c4012..002842a64 100644
--- a/tests/functional/zxbpp/prepro05.out
+++ b/tests/functional/zxbpp/prepro05.out
@@ -1,7 +1,7 @@
-#line 1 "prepro05.bi"
-
-#line 3 "prepro05.bi"
-#line 4 "prepro05.bi"
-
-y
-test2
+#line 1 "prepro05.bi"
+
+#line 3 "prepro05.bi"
+#line 4 "prepro05.bi"
+
+y
+test2
diff --git a/tests/functional/zxbpp/prepro06.out b/tests/functional/zxbpp/prepro06.out
index ec1f8f52d..a1adfc2fd 100644
--- a/tests/functional/zxbpp/prepro06.out
+++ b/tests/functional/zxbpp/prepro06.out
@@ -1,6 +1,6 @@
-#line 1 "prepro06.bi"
-
-#line 3 "prepro06.bi"
-
-
-func ()
+#line 1 "prepro06.bi"
+
+#line 3 "prepro06.bi"
+
+
+func ()
diff --git a/tests/functional/zxbpp/prepro09.out b/tests/functional/zxbpp/prepro09.out
index b39bdec97..5eed85abe 100644
--- a/tests/functional/zxbpp/prepro09.out
+++ b/tests/functional/zxbpp/prepro09.out
@@ -1,4 +1,4 @@
-#line 1 "prepro09.bi"
-
-
-
+#line 1 "prepro09.bi"
+
+
+
diff --git a/tests/functional/zxbpp/prepro10.out b/tests/functional/zxbpp/prepro10.out
index b5f7a2435..2ca0d7a86 100644
--- a/tests/functional/zxbpp/prepro10.out
+++ b/tests/functional/zxbpp/prepro10.out
@@ -1,6 +1,6 @@
-#line 1 "prepro10.bi"
-
-
-function test(byval a as byte)
- let test = 0
-end function
+#line 1 "prepro10.bi"
+
+
+function test(byval a as byte)
+ let test = 0
+end function
diff --git a/tests/functional/zxbpp/prepro11.out b/tests/functional/zxbpp/prepro11.out
index 58a87d63f..16d1cda9f 100644
--- a/tests/functional/zxbpp/prepro11.out
+++ b/tests/functional/zxbpp/prepro11.out
@@ -1,8 +1,8 @@
-#line 1 "prepro11.bi"
-
-
-
-#line 5 "prepro11.bi"
-#line 6 "prepro11.bi"
-
-PRINT "HELLO WORLD"
+#line 1 "prepro11.bi"
+
+
+
+#line 5 "prepro11.bi"
+#line 6 "prepro11.bi"
+
+PRINT "HELLO WORLD"
diff --git a/tests/functional/zxbpp/prepro12.out b/tests/functional/zxbpp/prepro12.out
index 8a9dc287a..1847f6a1a 100644
--- a/tests/functional/zxbpp/prepro12.out
+++ b/tests/functional/zxbpp/prepro12.out
@@ -1,8 +1,8 @@
-#line 1 "prepro12.bi"
-
-
-
-#line 5 "prepro12.bi"
-#line 6 "prepro12.bi"
-
-)PRINT "HELLO WORLD"
+#line 1 "prepro12.bi"
+
+
+
+#line 5 "prepro12.bi"
+#line 6 "prepro12.bi"
+
+)PRINT "HELLO WORLD"
diff --git a/tests/functional/zxbpp/prepro13.out b/tests/functional/zxbpp/prepro13.out
index 149ca4c6f..0ed5de8bc 100644
--- a/tests/functional/zxbpp/prepro13.out
+++ b/tests/functional/zxbpp/prepro13.out
@@ -1,8 +1,8 @@
-#line 1 "prepro13.bi"
-
-
-
-#line 5 "prepro13.bi"
-#line 6 "prepro13.bi"
-
-PRINT "HELLO WORLD"
+#line 1 "prepro13.bi"
+
+
+
+#line 5 "prepro13.bi"
+#line 6 "prepro13.bi"
+
+PRINT "HELLO WORLD"
diff --git a/tests/functional/zxbpp/prepro14.out b/tests/functional/zxbpp/prepro14.out
index 6b2b9e78d..d7e474e95 100644
--- a/tests/functional/zxbpp/prepro14.out
+++ b/tests/functional/zxbpp/prepro14.out
@@ -1,9 +1,9 @@
-#line 1 "prepro14.bi"
-
-
-
-#line 5 "prepro14.bi"
-#line 6 "prepro14.bi"
-#line 7 "prepro14.bi"
-
-PRINT "HELLO WORLD"
+#line 1 "prepro14.bi"
+
+
+
+#line 5 "prepro14.bi"
+#line 6 "prepro14.bi"
+#line 7 "prepro14.bi"
+
+PRINT "HELLO WORLD"
diff --git a/tests/functional/zxbpp/prepro15.out b/tests/functional/zxbpp/prepro15.out
index a57b86312..76f4d4747 100644
--- a/tests/functional/zxbpp/prepro15.out
+++ b/tests/functional/zxbpp/prepro15.out
@@ -1,12 +1,12 @@
-#line 1 "prepro15.bi"
-
-
-
-
-
-
-#line 8 "prepro15.bi"
-
-PRINT 10;
-PRINT "Hello";
-macro
+#line 1 "prepro15.bi"
+
+
+
+
+
+
+#line 8 "prepro15.bi"
+
+PRINT 10;
+PRINT "Hello";
+macro
diff --git a/tests/functional/zxbpp/prepro16.out b/tests/functional/zxbpp/prepro16.out
index 8da46a07b..1e616d55f 100644
--- a/tests/functional/zxbpp/prepro16.out
+++ b/tests/functional/zxbpp/prepro16.out
@@ -1,11 +1,11 @@
-#line 1 "prepro16.bi"
-
-
-
-
-
-#line 7 "prepro16.bi"
-#line 8 "prepro16.bi"
-
-PRINT 20;
-PRINT 10 + 20;
+#line 1 "prepro16.bi"
+
+
+
+
+
+#line 7 "prepro16.bi"
+#line 8 "prepro16.bi"
+
+PRINT 20;
+PRINT 10 + 20;
diff --git a/tests/functional/zxbpp/prepro17.out b/tests/functional/zxbpp/prepro17.out
index b3ad6e2ed..80349359a 100644
--- a/tests/functional/zxbpp/prepro17.out
+++ b/tests/functional/zxbpp/prepro17.out
@@ -1,8 +1,8 @@
-#line 1 "prepro17.bi"
-
-
-
-
-#line 6 "prepro17.bi"
-
-PRINT PRINT PRINT 10;;;
+#line 1 "prepro17.bi"
+
+
+
+
+#line 6 "prepro17.bi"
+
+PRINT PRINT PRINT 10;;;
diff --git a/tests/functional/zxbpp/prepro18.out b/tests/functional/zxbpp/prepro18.out
index 7fced6439..268595dbb 100644
--- a/tests/functional/zxbpp/prepro18.out
+++ b/tests/functional/zxbpp/prepro18.out
@@ -1,7 +1,7 @@
-#line 1 "prepro18.bi"
-
-
-
-#line 5 "prepro18.bi"
-
-PRINT (10, 20);
+#line 1 "prepro18.bi"
+
+
+
+#line 5 "prepro18.bi"
+
+PRINT (10, 20);
diff --git a/tests/functional/zxbpp/prepro19.out b/tests/functional/zxbpp/prepro19.out
index 7dde492a3..9dcd02cf3 100644
--- a/tests/functional/zxbpp/prepro19.out
+++ b/tests/functional/zxbpp/prepro19.out
@@ -1,7 +1,7 @@
-#line 1 "prepro19.bi"
-
-
-
-#line 5 "prepro19.bi"
-
-PRINT (10 + PRINT 20;);
+#line 1 "prepro19.bi"
+
+
+
+#line 5 "prepro19.bi"
+
+PRINT (10 + PRINT 20;);
diff --git a/tests/functional/zxbpp/prepro20.out b/tests/functional/zxbpp/prepro20.out
index ea97c3727..1d74d1c0a 100644
--- a/tests/functional/zxbpp/prepro20.out
+++ b/tests/functional/zxbpp/prepro20.out
@@ -1,7 +1,7 @@
-#line 1 "prepro20.bi"
-
-
-
-#line 5 "prepro20.bi"
-
-PRINT ;)
+#line 1 "prepro20.bi"
+
+
+
+#line 5 "prepro20.bi"
+
+PRINT ;)
diff --git a/tests/functional/zxbpp/prepro21.out b/tests/functional/zxbpp/prepro21.out
index dda7d1be9..094ff345e 100644
--- a/tests/functional/zxbpp/prepro21.out
+++ b/tests/functional/zxbpp/prepro21.out
@@ -1,8 +1,8 @@
-#line 1 "prepro21.bi"
-
-
-
-#line 5 "prepro21.bi"
-#line 6 "prepro21.bi"
-
-10
+#line 1 "prepro21.bi"
+
+
+
+#line 5 "prepro21.bi"
+#line 6 "prepro21.bi"
+
+10
diff --git a/tests/functional/zxbpp/prepro23.out b/tests/functional/zxbpp/prepro23.out
index 2ae9cd2cf..f091e4e73 100644
--- a/tests/functional/zxbpp/prepro23.out
+++ b/tests/functional/zxbpp/prepro23.out
@@ -1,9 +1,9 @@
-#line 1 "prepro23.bi"
-
-
-
-#line 5 "prepro23.bi"
-#line 6 "prepro23.bi"
-#line 7 "prepro23.bi"
-
-PRINT 10;
+#line 1 "prepro23.bi"
+
+
+
+#line 5 "prepro23.bi"
+#line 6 "prepro23.bi"
+#line 7 "prepro23.bi"
+
+PRINT 10;
diff --git a/tests/functional/zxbpp/prepro24.out b/tests/functional/zxbpp/prepro24.out
index d24b609e0..3d82bdadd 100644
--- a/tests/functional/zxbpp/prepro24.out
+++ b/tests/functional/zxbpp/prepro24.out
@@ -1,11 +1,11 @@
-#line 1 "prepro24.bi"
-
-
-#line 6 "prepro24.bi"
-
-
-FOR xxx = 1 TO 10
- PRINT xxx
- NEXT
-#line 9
-PRINT "Line 9"
+#line 1 "prepro24.bi"
+
+
+#line 6 "prepro24.bi"
+
+
+FOR xxx = 1 TO 10
+ PRINT xxx
+ NEXT
+#line 9
+PRINT "Line 9"
diff --git a/tests/functional/zxbpp/prepro25.out b/tests/functional/zxbpp/prepro25.out
index c3026c9cf..f8f76e628 100644
--- a/tests/functional/zxbpp/prepro25.out
+++ b/tests/functional/zxbpp/prepro25.out
@@ -1,7 +1,7 @@
-#line 1 "prepro25.bi"
-
-
-
-#line 5 "prepro25.bi"
-
-(x + y) * 2
+#line 1 "prepro25.bi"
+
+
+
+#line 5 "prepro25.bi"
+
+(x + y) * 2
diff --git a/tests/functional/zxbpp/prepro26.out b/tests/functional/zxbpp/prepro26.out
index efb28cc9e..0065c88eb 100644
--- a/tests/functional/zxbpp/prepro26.out
+++ b/tests/functional/zxbpp/prepro26.out
@@ -1,7 +1,7 @@
-#line 1 "prepro26.bi"
-
-
-
-#line 5 "prepro26.bi"
-
-(x + y) + 4 * 2
+#line 1 "prepro26.bi"
+
+
+
+#line 5 "prepro26.bi"
+
+(x + y) + 4 * 2
diff --git a/tests/functional/zxbpp/prepro27.out b/tests/functional/zxbpp/prepro27.out
index 2d9cbb470..79c511a1a 100644
--- a/tests/functional/zxbpp/prepro27.out
+++ b/tests/functional/zxbpp/prepro27.out
@@ -1,14 +1,14 @@
-#line 1 "prepro27.bi"
-Sub x
-End Sub
-
-
-
-
-#line 1 "prepro27.bi"
-Sub x
-End Sub
-
-#line 9 "prepro27.bi"
-#line 8 "prepro27.bi"
-#line 9 "prepro27.bi"
+#line 1 "prepro27.bi"
+Sub x
+End Sub
+
+
+
+
+#line 1 "prepro27.bi"
+Sub x
+End Sub
+
+#line 9 "prepro27.bi"
+#line 8 "prepro27.bi"
+#line 9 "prepro27.bi"
diff --git a/tests/functional/zxbpp/prepro29.out b/tests/functional/zxbpp/prepro29.out
index 0dd7ce49b..5f43a94c2 100644
--- a/tests/functional/zxbpp/prepro29.out
+++ b/tests/functional/zxbpp/prepro29.out
@@ -1,4 +1,4 @@
-#line 1 "prepro29.bi"
-
-
-ZZ
+#line 1 "prepro29.bi"
+
+
+ZZ
diff --git a/tests/functional/zxbpp/prepro30.out b/tests/functional/zxbpp/prepro30.out
index 4de9e4752..02c39216c 100644
--- a/tests/functional/zxbpp/prepro30.out
+++ b/tests/functional/zxbpp/prepro30.out
@@ -1,128 +1,128 @@
-#line 1 "prepro30.bi"
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-
-
-
-
-
-
-
-
-
-
-
-#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-
-#pragma push(case_insensitive)
-#pragma case_insensitive = TRUE
-
-
-
-
-
-
-
-
-
-
-
-function attr(byval row as ubyte, byval col as ubyte) as ubyte
- asm
- push namespace core
-
- PROC
- LOCAL __ATTR_END
-
- ld e, (ix+7)
- ld d, (ix+5)
-
-
- call __IN_SCREEN
- jr nc, __ATTR_END
-
- call __ATTR_ADDR
- ld a, (hl)
-
-__ATTR_END:
- ENDP
-
- pop namespace
- end asm
-
-end function
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-sub setattr(byval row as ubyte, byval col as ubyte, byval value as ubyte)
- asm
- push namespace core
-
- PROC
- LOCAL __ATTR_END
-
- ld e, (ix+7)
- ld d, (ix+5)
-
-
- call __IN_SCREEN
- jr nc, __ATTR_END
-
- call __ATTR_ADDR
- ld a, (ix+9)
- ld (hl), a
-
-__ATTR_END:
- ENDP
-
- pop namespace
- end asm
-
-end sub
-
-
-
-
-
-
-
-
-
-
-
-function fastcall attraddr(byval row as ubyte, byval col as ubyte) as uinteger
- asm
- push namespace core
-
- pop hl
- ex (sp), hl
- ld d, a
- ld e, h
- jp __ATTR_ADDR
- pop namespace
- end asm
-end function
-
-
-
-#pragma pop(case_insensitive)
-
-
-#require "attr.asm"
-
-
-#require "in_screen.asm"
-
-#line 125 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-#line 2 "prepro30.bi"
-
+#line 1 "prepro30.bi"
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+
+
+
+
+
+
+
+
+
+
+
+#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = TRUE
+
+
+
+
+
+
+
+
+
+
+
+function attr(byval row as ubyte, byval col as ubyte) as ubyte
+ asm
+ push namespace core
+
+ PROC
+ LOCAL __ATTR_END
+
+ ld e, (ix+7)
+ ld d, (ix+5)
+
+
+ call __IN_SCREEN
+ jr nc, __ATTR_END
+
+ call __ATTR_ADDR
+ ld a, (hl)
+
+__ATTR_END:
+ ENDP
+
+ pop namespace
+ end asm
+
+end function
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+sub setattr(byval row as ubyte, byval col as ubyte, byval value as ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL __ATTR_END
+
+ ld e, (ix+7)
+ ld d, (ix+5)
+
+
+ call __IN_SCREEN
+ jr nc, __ATTR_END
+
+ call __ATTR_ADDR
+ ld a, (ix+9)
+ ld (hl), a
+
+__ATTR_END:
+ ENDP
+
+ pop namespace
+ end asm
+
+end sub
+
+
+
+
+
+
+
+
+
+
+
+function fastcall attraddr(byval row as ubyte, byval col as ubyte) as uinteger
+ asm
+ push namespace core
+
+ pop hl
+ ex (sp), hl
+ ld d, a
+ ld e, h
+ jp __ATTR_ADDR
+ pop namespace
+ end asm
+end function
+
+
+
+#pragma pop(case_insensitive)
+
+
+#require "attr.asm"
+
+
+#require "in_screen.asm"
+
+#line 125 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+#line 2 "prepro30.bi"
+
diff --git a/tests/functional/zxbpp/prepro31.out b/tests/functional/zxbpp/prepro31.out
index 69152e43d..b3aca512a 100644
--- a/tests/functional/zxbpp/prepro31.out
+++ b/tests/functional/zxbpp/prepro31.out
@@ -1,4 +1,4 @@
-#line 1 "prepro31.bi"
-
-
-t(x, (y - 1))
+#line 1 "prepro31.bi"
+
+
+t(x, (y - 1))
diff --git a/tests/functional/zxbpp/prepro32.out b/tests/functional/zxbpp/prepro32.out
index 075b19cb5..ac0fc82c3 100644
--- a/tests/functional/zxbpp/prepro32.out
+++ b/tests/functional/zxbpp/prepro32.out
@@ -1,14 +1,14 @@
-#line 1 "prepro32.bi"
-DIM a As Ubyte = 1
-
-#line 10 "prepro32.bi"
-
-PRINT "HELLO WORLD"
-
- PRINT a; " MACRO START"
- ASM
- ld hl, _a
- inc (hl)
- END ASM
- PRINT a; " MACRO END"
-#line 13
+#line 1 "prepro32.bi"
+DIM a As Ubyte = 1
+
+#line 10 "prepro32.bi"
+
+PRINT "HELLO WORLD"
+
+ PRINT a; " MACRO START"
+ ASM
+ ld hl, _a
+ inc (hl)
+ END ASM
+ PRINT a; " MACRO END"
+#line 13
diff --git a/tests/functional/zxbpp/prepro33.out b/tests/functional/zxbpp/prepro33.out
index 8de3545f3..3617a96f7 100644
--- a/tests/functional/zxbpp/prepro33.out
+++ b/tests/functional/zxbpp/prepro33.out
@@ -1,35 +1,35 @@
-#line 1 "prepro33.bi"
-asm
- EX DE,HL
- PUSH DE
-
- LD L,(IX+8)
- LD H,(IX+9)
- EX AF,AF'
- LD A,4
- EX AF,AF'
- BLPutCharLoop:
- LD B,8
-
- BLPutCharOneCharLoop:
- XOR A
- LD C,A
-
-
- LDI
- LDI
- LDI
- LDI
- LD A,C
- ADD A,E
- LD E,A
- INC D
- LD A,B
- OR A
- JP NZ,BLPutCharOneCharLoop
-
- EX AF,AF'
- DEC A
- JR Z, BLPutCharsEnd
- EX AF,AF'
-end asm
+#line 1 "prepro33.bi"
+asm
+ EX DE,HL
+ PUSH DE
+
+ LD L,(IX+8)
+ LD H,(IX+9)
+ EX AF,AF'
+ LD A,4
+ EX AF,AF'
+ BLPutCharLoop:
+ LD B,8
+
+ BLPutCharOneCharLoop:
+ XOR A
+ LD C,A
+
+
+ LDI
+ LDI
+ LDI
+ LDI
+ LD A,C
+ ADD A,E
+ LD E,A
+ INC D
+ LD A,B
+ OR A
+ JP NZ,BLPutCharOneCharLoop
+
+ EX AF,AF'
+ DEC A
+ JR Z, BLPutCharsEnd
+ EX AF,AF'
+end asm
diff --git a/tests/functional/zxbpp/prepro34.out b/tests/functional/zxbpp/prepro34.out
index 645a33509..2108234f0 100644
--- a/tests/functional/zxbpp/prepro34.out
+++ b/tests/functional/zxbpp/prepro34.out
@@ -1,5 +1,5 @@
-#line 1 "prepro34.bi"
-
-
-LET a = _
- 5
+#line 1 "prepro34.bi"
+
+
+LET a = _
+ 5
diff --git a/tests/functional/zxbpp/prepro36.out b/tests/functional/zxbpp/prepro36.out
index ad9a29896..c9c27cc18 100644
--- a/tests/functional/zxbpp/prepro36.out
+++ b/tests/functional/zxbpp/prepro36.out
@@ -1,4 +1,4 @@
-#line 1 "prepro36.bi"
-
-
-
+#line 1 "prepro36.bi"
+
+
+
diff --git a/tests/functional/zxbpp/prepro37.out b/tests/functional/zxbpp/prepro37.out
index 9412f442d..fcfa1ffab 100644
--- a/tests/functional/zxbpp/prepro37.out
+++ b/tests/functional/zxbpp/prepro37.out
@@ -1,6 +1,6 @@
-#line 1 "prepro37.bi"
-
-
-
-
-PRINT
+#line 1 "prepro37.bi"
+
+
+
+
+PRINT
diff --git a/tests/functional/zxbpp/prepro38.out b/tests/functional/zxbpp/prepro38.out
index 21858f01a..df07706e2 100644
--- a/tests/functional/zxbpp/prepro38.out
+++ b/tests/functional/zxbpp/prepro38.out
@@ -1,7 +1,7 @@
-#line 1 "prepro38.bi"
-
-#line 4 "prepro38.bi"
-
-
- Test
-#line 6
+#line 1 "prepro38.bi"
+
+#line 4 "prepro38.bi"
+
+
+ Test
+#line 6
diff --git a/tests/functional/zxbpp/prepro39.out b/tests/functional/zxbpp/prepro39.out
index 42c310b70..2723ffcb1 100644
--- a/tests/functional/zxbpp/prepro39.out
+++ b/tests/functional/zxbpp/prepro39.out
@@ -1,9 +1,9 @@
-#line 1 "prepro39.bi"
-
-
-
-#line 6 "prepro39.bi"
-
-
- PRINT ,
-#line 8
+#line 1 "prepro39.bi"
+
+
+
+#line 6 "prepro39.bi"
+
+
+ PRINT ,
+#line 8
diff --git a/tests/functional/zxbpp/prepro40.out b/tests/functional/zxbpp/prepro40.out
index 611dbd187..987bc779c 100644
--- a/tests/functional/zxbpp/prepro40.out
+++ b/tests/functional/zxbpp/prepro40.out
@@ -1,4 +1,4 @@
-#line 1 "prepro40.bi"
-
-
-macro
+#line 1 "prepro40.bi"
+
+
+macro
diff --git a/tests/functional/zxbpp/prepro41.out b/tests/functional/zxbpp/prepro41.out
index 02670ced4..76c952b31 100644
--- a/tests/functional/zxbpp/prepro41.out
+++ b/tests/functional/zxbpp/prepro41.out
@@ -1,8 +1,8 @@
-#line 1 "prepro41.bi"
-
-
-10 PAUSE 0 :
- asm
- call 65012
- end asm : PAUSE 0
-#line 7
+#line 1 "prepro41.bi"
+
+
+10 PAUSE 0 :
+ asm
+ call 65012
+ end asm : PAUSE 0
+#line 7
diff --git a/tests/functional/zxbpp/prepro42.out b/tests/functional/zxbpp/prepro42.out
index bda8844f4..932a27240 100644
--- a/tests/functional/zxbpp/prepro42.out
+++ b/tests/functional/zxbpp/prepro42.out
@@ -1,4 +1,4 @@
-#line 1 "prepro42.bi"
-
-
-#line 6 "prepro42.bi"
+#line 1 "prepro42.bi"
+
+
+#line 6 "prepro42.bi"
diff --git a/tests/functional/zxbpp/prepro43.out b/tests/functional/zxbpp/prepro43.out
index c77d10e4d..1d5bc3876 100644
--- a/tests/functional/zxbpp/prepro43.out
+++ b/tests/functional/zxbpp/prepro43.out
@@ -1,6 +1,6 @@
-#line 1 "prepro43.bi"
-
-
-
-print 1
-#line 6 "prepro43.bi"
+#line 1 "prepro43.bi"
+
+
+
+print 1
+#line 6 "prepro43.bi"
diff --git a/tests/functional/zxbpp/prepro44.out b/tests/functional/zxbpp/prepro44.out
index 62908a59e..12b7f6558 100644
--- a/tests/functional/zxbpp/prepro44.out
+++ b/tests/functional/zxbpp/prepro44.out
@@ -1,4 +1,4 @@
-#line 1 "prepro44.bi"
-
-
-#line 6 "prepro44.bi"
+#line 1 "prepro44.bi"
+
+
+#line 6 "prepro44.bi"
diff --git a/tests/functional/zxbpp/prepro45.out b/tests/functional/zxbpp/prepro45.out
index f23f903a2..61af4f5a4 100644
--- a/tests/functional/zxbpp/prepro45.out
+++ b/tests/functional/zxbpp/prepro45.out
@@ -1,4 +1,4 @@
-#line 1 "prepro45.bi"
-
-
-#line 7 "prepro45.bi"
+#line 1 "prepro45.bi"
+
+
+#line 7 "prepro45.bi"
diff --git a/tests/functional/zxbpp/prepro46.out b/tests/functional/zxbpp/prepro46.out
index 0cecdef91..1ba95a53b 100644
--- a/tests/functional/zxbpp/prepro46.out
+++ b/tests/functional/zxbpp/prepro46.out
@@ -1,6 +1,6 @@
-#line 1 "prepro46.bi"
-
-
-
-print 1
-#line 6 "prepro46.bi"
+#line 1 "prepro46.bi"
+
+
+
+print 1
+#line 6 "prepro46.bi"
diff --git a/tests/functional/zxbpp/prepro47.out b/tests/functional/zxbpp/prepro47.out
index 3a4b99af7..813aaa1e9 100644
--- a/tests/functional/zxbpp/prepro47.out
+++ b/tests/functional/zxbpp/prepro47.out
@@ -1,5 +1,5 @@
-#line 1 "prepro47.bi"
-
-#line 3 "prepro47.bi"
-
-#line 8 "prepro47.bi"
+#line 1 "prepro47.bi"
+
+#line 3 "prepro47.bi"
+
+#line 8 "prepro47.bi"
diff --git a/tests/functional/zxbpp/prepro48.out b/tests/functional/zxbpp/prepro48.out
index 976bbb3f7..bfc8f8d82 100644
--- a/tests/functional/zxbpp/prepro48.out
+++ b/tests/functional/zxbpp/prepro48.out
@@ -1,5 +1,5 @@
-#line 1 "prepro48.bi"
-
-#line 3 "prepro48.bi"
-
-#line 8 "prepro48.bi"
+#line 1 "prepro48.bi"
+
+#line 3 "prepro48.bi"
+
+#line 8 "prepro48.bi"
diff --git a/tests/functional/zxbpp/prepro49.out b/tests/functional/zxbpp/prepro49.out
index 7b358facf..87aa9583f 100644
--- a/tests/functional/zxbpp/prepro49.out
+++ b/tests/functional/zxbpp/prepro49.out
@@ -1,5 +1,5 @@
-#line 1 "prepro49.bi"
-
-#line 3 "prepro49.bi"
-
-#line 8 "prepro49.bi"
+#line 1 "prepro49.bi"
+
+#line 3 "prepro49.bi"
+
+#line 8 "prepro49.bi"
diff --git a/tests/functional/zxbpp/prepro50.out b/tests/functional/zxbpp/prepro50.out
index e4560671c..76e000824 100644
--- a/tests/functional/zxbpp/prepro50.out
+++ b/tests/functional/zxbpp/prepro50.out
@@ -1,8 +1,8 @@
-#line 1 "prepro50.bi"
-
-#line 3 "prepro50.bi"
-
-
-print 1
-
-#line 8 "prepro50.bi"
+#line 1 "prepro50.bi"
+
+#line 3 "prepro50.bi"
+
+
+print 1
+
+#line 8 "prepro50.bi"
diff --git a/tests/functional/zxbpp/prepro51.out b/tests/functional/zxbpp/prepro51.out
index 67281624b..f09c1f080 100644
--- a/tests/functional/zxbpp/prepro51.out
+++ b/tests/functional/zxbpp/prepro51.out
@@ -1,4 +1,4 @@
-#line 1 "prepro51.bi"
-
-
-A(1)
+#line 1 "prepro51.bi"
+
+
+A(1)
diff --git a/tests/functional/zxbpp/prepro52.out b/tests/functional/zxbpp/prepro52.out
index e2c023bc4..b22576dca 100644
--- a/tests/functional/zxbpp/prepro52.out
+++ b/tests/functional/zxbpp/prepro52.out
@@ -1,5 +1,5 @@
-#line 1 "prepro52.bi"
-
-#line 3 "prepro52.bi"
-
-1
+#line 1 "prepro52.bi"
+
+#line 3 "prepro52.bi"
+
+1
diff --git a/tests/functional/zxbpp/prepro53.out b/tests/functional/zxbpp/prepro53.out
index bacaf8d97..051b7c171 100644
--- a/tests/functional/zxbpp/prepro53.out
+++ b/tests/functional/zxbpp/prepro53.out
@@ -1,5 +1,5 @@
-#line 1 "prepro53.bi"
-
-#line 3 "prepro53.bi"
-
-1
+#line 1 "prepro53.bi"
+
+#line 3 "prepro53.bi"
+
+1
diff --git a/tests/functional/zxbpp/prepro54.out b/tests/functional/zxbpp/prepro54.out
index dc8ee0c6d..5796d5e88 100644
--- a/tests/functional/zxbpp/prepro54.out
+++ b/tests/functional/zxbpp/prepro54.out
@@ -1,5 +1,5 @@
-#line 1 "prepro54.bi"
-
-#line 3 "prepro54.bi"
-
-#line 7 "prepro54.bi"
+#line 1 "prepro54.bi"
+
+#line 3 "prepro54.bi"
+
+#line 7 "prepro54.bi"
diff --git a/tests/functional/zxbpp/prepro55.out b/tests/functional/zxbpp/prepro55.out
index 57bc0cf34..627797956 100644
--- a/tests/functional/zxbpp/prepro55.out
+++ b/tests/functional/zxbpp/prepro55.out
@@ -1,7 +1,7 @@
-#line 1 "prepro55.bi"
-
-#line 3 "prepro55.bi"
-
-
-ok
-#line 7 "prepro55.bi"
+#line 1 "prepro55.bi"
+
+#line 3 "prepro55.bi"
+
+
+ok
+#line 7 "prepro55.bi"
diff --git a/tests/functional/zxbpp/prepro56.out b/tests/functional/zxbpp/prepro56.out
index 814a6b692..b829f00c2 100644
--- a/tests/functional/zxbpp/prepro56.out
+++ b/tests/functional/zxbpp/prepro56.out
@@ -1,5 +1,5 @@
-#line 1 "prepro56.bi"
-
-#line 3 "prepro56.bi"
-
-#line 7 "prepro56.bi"
+#line 1 "prepro56.bi"
+
+#line 3 "prepro56.bi"
+
+#line 7 "prepro56.bi"
diff --git a/tests/functional/zxbpp/prepro57.out b/tests/functional/zxbpp/prepro57.out
index a6d0ba11a..d62e8580b 100644
--- a/tests/functional/zxbpp/prepro57.out
+++ b/tests/functional/zxbpp/prepro57.out
@@ -1,5 +1,5 @@
-#line 1 "prepro57.bi"
-
-
-PRINT "LANG = es"
-#line 5 "prepro57.bi"
+#line 1 "prepro57.bi"
+
+
+PRINT "LANG = es"
+#line 5 "prepro57.bi"
diff --git a/tests/functional/zxbpp/prepro58.out b/tests/functional/zxbpp/prepro58.out
index 0d224895e..3ae83cfa0 100644
--- a/tests/functional/zxbpp/prepro58.out
+++ b/tests/functional/zxbpp/prepro58.out
@@ -1,6 +1,6 @@
-#line 1 "prepro58.bi"
-sub test()
- asm
- ex af,af'
- end asm
-end sub
+#line 1 "prepro58.bi"
+sub test()
+ asm
+ ex af,af'
+ end asm
+end sub
diff --git a/tests/functional/zxbpp/prepro59.out b/tests/functional/zxbpp/prepro59.out
index 0fb6b4e16..65a8a93a7 100644
--- a/tests/functional/zxbpp/prepro59.out
+++ b/tests/functional/zxbpp/prepro59.out
@@ -1,5 +1,5 @@
-#line 1 "prepro59.bi"
-
-#line 3 "prepro59.bi"
-
-LET a$ = (a$ + CHR$(0))
+#line 1 "prepro59.bi"
+
+#line 3 "prepro59.bi"
+
+LET a$ = (a$ + CHR$(0))
diff --git a/tests/functional/zxbpp/prepro60.out b/tests/functional/zxbpp/prepro60.out
index 1835bd294..eae521eb5 100644
--- a/tests/functional/zxbpp/prepro60.out
+++ b/tests/functional/zxbpp/prepro60.out
@@ -1,6 +1,6 @@
-#line 1 "prepro60.bi"
-Function FASTCALL ESXDOSWrite(ByVal handle as Byte, _
- ByVal buffer as UInteger, _
- ByVal nbytes as UInteger) as Uinteger
-#line 4
-End Function
+#line 1 "prepro60.bi"
+Function FASTCALL ESXDOSWrite(ByVal handle as Byte, _
+ ByVal buffer as UInteger, _
+ ByVal nbytes as UInteger) as Uinteger
+#line 4
+End Function
diff --git a/tests/functional/zxbpp/prepro61.out b/tests/functional/zxbpp/prepro61.out
index 8c6ed5188..c057071d0 100644
--- a/tests/functional/zxbpp/prepro61.out
+++ b/tests/functional/zxbpp/prepro61.out
@@ -1,6 +1,6 @@
-#line 1 "prepro61.bi"
-
-
-#line 4 "prepro61.bi"
-
-xx 1 aa 1 x(1)
+#line 1 "prepro61.bi"
+
+
+#line 4 "prepro61.bi"
+
+xx 1 aa 1 x(1)
diff --git a/tests/functional/zxbpp/prepro62.out b/tests/functional/zxbpp/prepro62.out
index f424ae130..39bb10aaf 100644
--- a/tests/functional/zxbpp/prepro62.out
+++ b/tests/functional/zxbpp/prepro62.out
@@ -1,7 +1,7 @@
-#line 1 "prepro62.bi"
-
-#line 3 "prepro62.bi"
-#line 4 "prepro62.bi"
-
-(1 + 1, 2 + 2)
-(3 + 1, 4 + 2)
+#line 1 "prepro62.bi"
+
+#line 3 "prepro62.bi"
+#line 4 "prepro62.bi"
+
+(1 + 1, 2 + 2)
+(3 + 1, 4 + 2)
diff --git a/tests/functional/zxbpp/prepro63.out b/tests/functional/zxbpp/prepro63.out
index 51b9f7222..8d9d72c5a 100644
--- a/tests/functional/zxbpp/prepro63.out
+++ b/tests/functional/zxbpp/prepro63.out
@@ -1,6 +1,6 @@
-#line 1 "prepro63.bi"
-
-
-#line 4 "prepro63.bi"
-
-XX5
+#line 1 "prepro63.bi"
+
+
+#line 4 "prepro63.bi"
+
+XX5
diff --git a/tests/functional/zxbpp/prepro64.out b/tests/functional/zxbpp/prepro64.out
index 15a3d3114..835aabb03 100644
--- a/tests/functional/zxbpp/prepro64.out
+++ b/tests/functional/zxbpp/prepro64.out
@@ -1,50 +1,50 @@
-#line 1 "prepro64.bi"
-
-#line 3 "prepro64.bi"
-
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-
-
-
-
-
-
-
-
-
-
-
-
-
-#line 15 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-
-#pragma push(case_insensitive)
-#pragma case_insensitive = true
-
-
-
-
-
-
-
-
-
-function FASTCALL pos as ubyte
- asm
- push namespace core
- PROC
-
- call __LOAD_S_POSN
- ld , e
-
- ENDP
- pop namespace
- end asm
-end function
-
-#pragma pop(case_insensitive)
-#require "sposn.asm"
-
-
-#line 45 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-#line 5 "prepro64.bi"
+#line 1 "prepro64.bi"
+
+#line 3 "prepro64.bi"
+
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+
+
+
+
+
+
+
+
+
+
+
+
+
+#line 15 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = true
+
+
+
+
+
+
+
+
+
+function FASTCALL pos as ubyte
+ asm
+ push namespace core
+ PROC
+
+ call __LOAD_S_POSN
+ ld , e
+
+ ENDP
+ pop namespace
+ end asm
+end function
+
+#pragma pop(case_insensitive)
+#require "sposn.asm"
+
+
+#line 45 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+#line 5 "prepro64.bi"
diff --git a/tests/functional/zxbpp/prepro70.out b/tests/functional/zxbpp/prepro70.out
index 47bdb70b4..dd55790ed 100644
--- a/tests/functional/zxbpp/prepro70.out
+++ b/tests/functional/zxbpp/prepro70.out
@@ -1,13 +1,13 @@
-#line 1 "prepro70.bi"
-DIM b
-
-
-
-#line 1 "prepro70.bi"
-DIM b
-#line 10 "prepro70.bi"
-#line 6 "prepro70.bi"
-
-DIM a
-
-#line 10 "prepro70.bi"
+#line 1 "prepro70.bi"
+DIM b
+
+
+
+#line 1 "prepro70.bi"
+DIM b
+#line 10 "prepro70.bi"
+#line 6 "prepro70.bi"
+
+DIM a
+
+#line 10 "prepro70.bi"
diff --git a/tests/functional/zxbpp/prepro71.out b/tests/functional/zxbpp/prepro71.out
index 9d1274edd..98e08c6fb 100644
--- a/tests/functional/zxbpp/prepro71.out
+++ b/tests/functional/zxbpp/prepro71.out
@@ -1,243 +1,243 @@
-#line 1 "prepro71.bi"
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/alloc.bas"
-
-
-
-
-
-
-
-
-
-
-
-#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/alloc.bas"
-
-#pragma push(case_insensitive)
-#pragma case_insensitive = True
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-function FASTCALL allocate(byval n as uinteger) as uinteger
-
-
-
-
- asm
- push namespace core
- ld b, h
- ld c, l
- jp __MEM_ALLOC
- pop namespace
- end asm
-end function
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-function FASTCALL callocate(byval n as uinteger) as uinteger
-
-
-
-
- asm
- push namespace core
- ld b, h
- ld c, l
- jp __MEM_CALLOC
- pop namespace
- end asm
-end function
-
-
-
-
-
-
-
-
-sub FASTCALL deallocate(byval addr as integer)
-
-
-
- asm
- push namespace core
- jp __MEM_FREE
- pop namespace
- end asm
-end sub
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-function FASTCALL reallocate(byval addr as uinteger, byval n as uinteger) as uinteger
-
-
-
-
-
- asm
- push namespace core
- ex de, hl
- pop hl
- ex (sp), hl
- ld b, h
- ld c, l
- ex de, hl
- jp __REALLOC
- pop namespace
- end asm
-end function
-
-
-
-
-
-
-
-
-function FASTCALL memavail as uInteger
- asm
- push namespace core
- PROC
-
- LOCAL LOOP
-
- ld hl, ZXBASIC_MEM_HEAP
- ld de, 0
-
-LOOP:
-
- ld c, (hl)
- inc hl
- ld b, (hl)
- inc hl
-
-
- ld a, (hl)
- inc hl
- ld h, (hl)
- ld l, a
-
-
- ex de, hl
- add hl, bc
- ex de, hl
-
-
- ld a, h
- or l
- jr nz, LOOP
-
- ex de, hl
-
- ENDP
- pop namespace
- end asm
-end function
-
-
-
-
-
-
-
-function FASTCALL maxavail as uInteger
- asm
- push namespace core
- PROC
-
- LOCAL LOOP, CONT
-
- ld hl, ZXBASIC_MEM_HEAP
- ld de, 0
-
-LOOP:
-
- ld c, (hl)
- inc hl
- ld b, (hl)
- inc hl
-
-
- ld a, (hl)
- inc hl
- ld h, (hl)
- ld l, a
-
-
-
- ex de, hl
- or a
- sbc hl, bc
- add hl, bc
- ex de, hl
-
-
- jr nc, CONT
-
- ld d, b
- ld e, c
-
-CONT:
-
- ld a, h
- or l
- jr nz, LOOP
-
- ex de, hl
-
- ENDP
- pop namespace
- end asm
-end function
-
-
-#pragma pop(case_insensitive)
-
-#require "mem/alloc.asm"
-#require "mem/free.asm"
-#require "mem/realloc.asm"
-#require "mem/calloc.asm"
-
-#line 239 "/zxbasic/src/lib/arch/zx48k/stdlib/alloc.bas"
-#line 2 "prepro71.bi"
-
-PRINT "HOLA"
+#line 1 "prepro71.bi"
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/alloc.bas"
+
+
+
+
+
+
+
+
+
+
+
+#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/alloc.bas"
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+function FASTCALL allocate(byval n as uinteger) as uinteger
+
+
+
+
+ asm
+ push namespace core
+ ld b, h
+ ld c, l
+ jp __MEM_ALLOC
+ pop namespace
+ end asm
+end function
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+function FASTCALL callocate(byval n as uinteger) as uinteger
+
+
+
+
+ asm
+ push namespace core
+ ld b, h
+ ld c, l
+ jp __MEM_CALLOC
+ pop namespace
+ end asm
+end function
+
+
+
+
+
+
+
+
+sub FASTCALL deallocate(byval addr as integer)
+
+
+
+ asm
+ push namespace core
+ jp __MEM_FREE
+ pop namespace
+ end asm
+end sub
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+function FASTCALL reallocate(byval addr as uinteger, byval n as uinteger) as uinteger
+
+
+
+
+
+ asm
+ push namespace core
+ ex de, hl
+ pop hl
+ ex (sp), hl
+ ld b, h
+ ld c, l
+ ex de, hl
+ jp __REALLOC
+ pop namespace
+ end asm
+end function
+
+
+
+
+
+
+
+
+function FASTCALL memavail as uInteger
+ asm
+ push namespace core
+ PROC
+
+ LOCAL LOOP
+
+ ld hl, ZXBASIC_MEM_HEAP
+ ld de, 0
+
+LOOP:
+
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ inc hl
+
+
+ ld a, (hl)
+ inc hl
+ ld h, (hl)
+ ld l, a
+
+
+ ex de, hl
+ add hl, bc
+ ex de, hl
+
+
+ ld a, h
+ or l
+ jr nz, LOOP
+
+ ex de, hl
+
+ ENDP
+ pop namespace
+ end asm
+end function
+
+
+
+
+
+
+
+function FASTCALL maxavail as uInteger
+ asm
+ push namespace core
+ PROC
+
+ LOCAL LOOP, CONT
+
+ ld hl, ZXBASIC_MEM_HEAP
+ ld de, 0
+
+LOOP:
+
+ ld c, (hl)
+ inc hl
+ ld b, (hl)
+ inc hl
+
+
+ ld a, (hl)
+ inc hl
+ ld h, (hl)
+ ld l, a
+
+
+
+ ex de, hl
+ or a
+ sbc hl, bc
+ add hl, bc
+ ex de, hl
+
+
+ jr nc, CONT
+
+ ld d, b
+ ld e, c
+
+CONT:
+
+ ld a, h
+ or l
+ jr nz, LOOP
+
+ ex de, hl
+
+ ENDP
+ pop namespace
+ end asm
+end function
+
+
+#pragma pop(case_insensitive)
+
+#require "mem/alloc.asm"
+#require "mem/free.asm"
+#require "mem/realloc.asm"
+#require "mem/calloc.asm"
+
+#line 239 "/zxbasic/src/lib/arch/zx48k/stdlib/alloc.bas"
+#line 2 "prepro71.bi"
+
+PRINT "HOLA"
diff --git a/tests/functional/zxbpp/prepro72.out b/tests/functional/zxbpp/prepro72.out
index b21476152..6da2cd04e 100644
--- a/tests/functional/zxbpp/prepro72.out
+++ b/tests/functional/zxbpp/prepro72.out
@@ -1,13 +1,13 @@
-#line 1 "prepro72.bi"
-DIM b
-
-
-
-#line 1 "prepro70.bi"
-DIM b
-#line 10 "prepro70.bi"
-#line 6 "prepro72.bi"
-
-DIM a
-
-#line 10 "prepro72.bi"
+#line 1 "prepro72.bi"
+DIM b
+
+
+
+#line 1 "prepro70.bi"
+DIM b
+#line 10 "prepro70.bi"
+#line 6 "prepro72.bi"
+
+DIM a
+
+#line 10 "prepro72.bi"
diff --git a/tests/functional/zxbpp/prepro73.out b/tests/functional/zxbpp/prepro73.out
index e3de2597c..75a2c16aa 100644
--- a/tests/functional/zxbpp/prepro73.out
+++ b/tests/functional/zxbpp/prepro73.out
@@ -1,67 +1,67 @@
-#line 1 "prepro73.bi"
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-
-
-
-
-
-
-
-
-
-
-
-
-
-#line 15 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-
-#pragma push(case_insensitive)
-#pragma case_insensitive = true
-
-
-
-
-
-
-
-
-
-function FASTCALL pos as ubyte
- asm
- push namespace core
- PROC
-
- call __LOAD_S_POSN
- ld a, e
-
- ENDP
- pop namespace
- end asm
-end function
-
-#pragma pop(case_insensitive)
-#require "sposn.asm"
-
-
-#line 45 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-#line 2 "prepro73.bi"
-
-
-
-#line 1 "prepro73.bi"
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-
-
-
-
-
-
-
-
-
-#line 45 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-#line 2 "prepro73.bi"
-
-#line 7 "prepro73.bi"
-#line 6 "prepro73.bi"
-#line 7 "prepro73.bi"
+#line 1 "prepro73.bi"
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+
+
+
+
+
+
+
+
+
+
+
+
+
+#line 15 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = true
+
+
+
+
+
+
+
+
+
+function FASTCALL pos as ubyte
+ asm
+ push namespace core
+ PROC
+
+ call __LOAD_S_POSN
+ ld a, e
+
+ ENDP
+ pop namespace
+ end asm
+end function
+
+#pragma pop(case_insensitive)
+#require "sposn.asm"
+
+
+#line 45 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+#line 2 "prepro73.bi"
+
+
+
+#line 1 "prepro73.bi"
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+
+
+
+
+
+
+
+
+
+#line 45 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+#line 2 "prepro73.bi"
+
+#line 7 "prepro73.bi"
+#line 6 "prepro73.bi"
+#line 7 "prepro73.bi"
diff --git a/tests/functional/zxbpp/prepro74.out b/tests/functional/zxbpp/prepro74.out
index 72e9bfc05..f1b5a1fd5 100644
--- a/tests/functional/zxbpp/prepro74.out
+++ b/tests/functional/zxbpp/prepro74.out
@@ -1,60 +1,60 @@
-#line 1 "prepro74.bi"
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-
-
-
-
-
-
-
-
-
-
-
-
-
-#line 15 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-
-#pragma push(case_insensitive)
-#pragma case_insensitive = true
-
-
-
-
-
-
-
-
-
-function FASTCALL pos as ubyte
- asm
- push namespace core
- PROC
-
- call __LOAD_S_POSN
- ld a, e
-
- ENDP
- pop namespace
- end asm
-end function
-
-#pragma pop(case_insensitive)
-#require "sposn.asm"
-
-
-#line 45 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
-#line 2 "prepro74.bi"
-
-
-
-#line 1 "prepro74.bi"
-
-
-#line 7 "prepro74.bi"
-
-glibberish
-#line 6 "prepro74.bi"
-#line 7 "prepro74.bi"
-
-glibberish
+#line 1 "prepro74.bi"
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+
+
+
+
+
+
+
+
+
+
+
+
+
+#line 15 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = true
+
+
+
+
+
+
+
+
+
+function FASTCALL pos as ubyte
+ asm
+ push namespace core
+ PROC
+
+ call __LOAD_S_POSN
+ ld a, e
+
+ ENDP
+ pop namespace
+ end asm
+end function
+
+#pragma pop(case_insensitive)
+#require "sposn.asm"
+
+
+#line 45 "/zxbasic/src/lib/arch/zx48k/stdlib/pos.bas"
+#line 2 "prepro74.bi"
+
+
+
+#line 1 "prepro74.bi"
+
+
+#line 7 "prepro74.bi"
+
+glibberish
+#line 6 "prepro74.bi"
+#line 7 "prepro74.bi"
+
+glibberish
diff --git a/tests/functional/zxbpp/prepro75.out b/tests/functional/zxbpp/prepro75.out
index 3d2e89be8..60d28b684 100644
--- a/tests/functional/zxbpp/prepro75.out
+++ b/tests/functional/zxbpp/prepro75.out
@@ -1,9 +1,9 @@
-#line 1 "prepro75.bi"
-A = 0
-
-
-#line 1 "prepro75.bi"
-A = 0
-#line 6 "prepro75.bi"
-#line 5 "prepro75.bi"
-#line 6 "prepro75.bi"
+#line 1 "prepro75.bi"
+A = 0
+
+
+#line 1 "prepro75.bi"
+A = 0
+#line 6 "prepro75.bi"
+#line 5 "prepro75.bi"
+#line 6 "prepro75.bi"
diff --git a/tests/functional/zxbpp/prepro77.out b/tests/functional/zxbpp/prepro77.out
index 05c22b9ac..302836d38 100644
--- a/tests/functional/zxbpp/prepro77.out
+++ b/tests/functional/zxbpp/prepro77.out
@@ -1,2 +1,2 @@
-#line 1 "prepro77.bi"
-
+#line 1 "prepro77.bi"
+
diff --git a/tests/functional/zxbpp/prepro80.out b/tests/functional/zxbpp/prepro80.out
index c5f085d99..a543816d3 100644
--- a/tests/functional/zxbpp/prepro80.out
+++ b/tests/functional/zxbpp/prepro80.out
@@ -1,20 +1,20 @@
-#line 1 "prepro80.bi"
-
-
-
-
-
-
- asm
- halt
- halt
- end asm
-#line 11
-
- asm
- halt
- halt
- end asm
-#line 12
-
-#line 14 "prepro80.bi"
+#line 1 "prepro80.bi"
+
+
+
+
+
+
+ asm
+ halt
+ halt
+ end asm
+#line 11
+
+ asm
+ halt
+ halt
+ end asm
+#line 12
+
+#line 14 "prepro80.bi"
diff --git a/tests/functional/zxbpp/prepro81.out b/tests/functional/zxbpp/prepro81.out
index ff19e067d..b53925261 100644
--- a/tests/functional/zxbpp/prepro81.out
+++ b/tests/functional/zxbpp/prepro81.out
@@ -1,4 +1,4 @@
-#line 1 "prepro81.bi"
-
-
-#line 6 "prepro81.bi"
+#line 1 "prepro81.bi"
+
+
+#line 6 "prepro81.bi"
diff --git a/tests/functional/zxbpp/prepro82.out b/tests/functional/zxbpp/prepro82.out
index 8387751de..2ce50d3fa 100644
--- a/tests/functional/zxbpp/prepro82.out
+++ b/tests/functional/zxbpp/prepro82.out
@@ -1,7 +1,7 @@
-#line 1 "prepro82.bi"
-
-
-
-
- PRINT "test"
-#line 7 "prepro82.bi"
+#line 1 "prepro82.bi"
+
+
+
+
+ PRINT "test"
+#line 7 "prepro82.bi"
diff --git a/tests/functional/zxbpp/prepro85.out b/tests/functional/zxbpp/prepro85.out
index 2847d5075..8207f63be 100644
--- a/tests/functional/zxbpp/prepro85.out
+++ b/tests/functional/zxbpp/prepro85.out
@@ -1,132 +1,132 @@
-#line 1 "prepro85.bi"
-
-
-#line 1 "prepro00.bi"
-#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-
-
-
-
-
-
-
-
-
-
-
-#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-
-#pragma push(case_insensitive)
-#pragma case_insensitive = TRUE
-
-
-
-
-
-
-
-
-
-
-
-function attr(byval row as ubyte, byval col as ubyte) as ubyte
- asm
- push namespace core
-
- PROC
- LOCAL __ATTR_END
-
- ld e, (ix+7)
- ld d, (ix+5)
-
-
- call __IN_SCREEN
- jr nc, __ATTR_END
-
- call __ATTR_ADDR
- ld a, (hl)
-
-__ATTR_END:
- ENDP
-
- pop namespace
- end asm
-
-end function
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-sub setattr(byval row as ubyte, byval col as ubyte, byval value as ubyte)
- asm
- push namespace core
-
- PROC
- LOCAL __ATTR_END
-
- ld e, (ix+7)
- ld d, (ix+5)
-
-
- call __IN_SCREEN
- jr nc, __ATTR_END
-
- call __ATTR_ADDR
- ld a, (ix+9)
- ld (hl), a
-
-__ATTR_END:
- ENDP
-
- pop namespace
- end asm
-
-end sub
-
-
-
-
-
-
-
-
-
-
-
-function fastcall attraddr(byval row as ubyte, byval col as ubyte) as uinteger
- asm
- push namespace core
-
- pop hl
- ex (sp), hl
- ld d, a
- ld e, h
- jp __ATTR_ADDR
- pop namespace
- end asm
-end function
-
-
-
-#pragma pop(case_insensitive)
-
-
-#require "attr.asm"
-
-
-#require "in_screen.asm"
-
-#line 125 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
-#line 2 "prepro00.bi"
-PRINT "HELLO"
-#line 4 "prepro85.bi"
+#line 1 "prepro85.bi"
+
+
+#line 1 "prepro00.bi"
+#line 1 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+
+
+
+
+
+
+
+
+
+
+
+#line 13 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+
+#pragma push(case_insensitive)
+#pragma case_insensitive = TRUE
+
+
+
+
+
+
+
+
+
+
+
+function attr(byval row as ubyte, byval col as ubyte) as ubyte
+ asm
+ push namespace core
+
+ PROC
+ LOCAL __ATTR_END
+
+ ld e, (ix+7)
+ ld d, (ix+5)
+
+
+ call __IN_SCREEN
+ jr nc, __ATTR_END
+
+ call __ATTR_ADDR
+ ld a, (hl)
+
+__ATTR_END:
+ ENDP
+
+ pop namespace
+ end asm
+
+end function
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+sub setattr(byval row as ubyte, byval col as ubyte, byval value as ubyte)
+ asm
+ push namespace core
+
+ PROC
+ LOCAL __ATTR_END
+
+ ld e, (ix+7)
+ ld d, (ix+5)
+
+
+ call __IN_SCREEN
+ jr nc, __ATTR_END
+
+ call __ATTR_ADDR
+ ld a, (ix+9)
+ ld (hl), a
+
+__ATTR_END:
+ ENDP
+
+ pop namespace
+ end asm
+
+end sub
+
+
+
+
+
+
+
+
+
+
+
+function fastcall attraddr(byval row as ubyte, byval col as ubyte) as uinteger
+ asm
+ push namespace core
+
+ pop hl
+ ex (sp), hl
+ ld d, a
+ ld e, h
+ jp __ATTR_ADDR
+ pop namespace
+ end asm
+end function
+
+
+
+#pragma pop(case_insensitive)
+
+
+#require "attr.asm"
+
+
+#require "in_screen.asm"
+
+#line 125 "/zxbasic/src/lib/arch/zx48k/stdlib/attr.bas"
+#line 2 "prepro00.bi"
+PRINT "HELLO"
+#line 4 "prepro85.bi"
diff --git a/tests/functional/zxbpp/prepro90.out b/tests/functional/zxbpp/prepro90.out
index fa3b6bb6a..5ab94ed12 100644
--- a/tests/functional/zxbpp/prepro90.out
+++ b/tests/functional/zxbpp/prepro90.out
@@ -1,7 +1,7 @@
-#line 1 "prepro90.bi"
-
-#line 3 "prepro90.bi"
-
-#line 7 "prepro90.bi"
- OK
-#line 9 "prepro90.bi"
+#line 1 "prepro90.bi"
+
+#line 3 "prepro90.bi"
+
+#line 7 "prepro90.bi"
+ OK
+#line 9 "prepro90.bi"
diff --git a/tests/functional/zxbpp/prepro91.out b/tests/functional/zxbpp/prepro91.out
index 98e31fcb2..8de0031a7 100644
--- a/tests/functional/zxbpp/prepro91.out
+++ b/tests/functional/zxbpp/prepro91.out
@@ -1,5 +1,5 @@
-#line 1 "prepro91.bi"
-
-#line 7 "prepro91.bi"
- OK
-#line 9 "prepro91.bi"
+#line 1 "prepro91.bi"
+
+#line 7 "prepro91.bi"
+ OK
+#line 9 "prepro91.bi"
diff --git a/tests/functional/zxbpp/prepro92.out b/tests/functional/zxbpp/prepro92.out
index bcf57a7a8..ddb01a2ed 100644
--- a/tests/functional/zxbpp/prepro92.out
+++ b/tests/functional/zxbpp/prepro92.out
@@ -1,7 +1,7 @@
-#line 1 "prepro92.bi"
-
-
-
-#line 7 "prepro92.bi"
-OK
-#line 9 "prepro92.bi"
+#line 1 "prepro92.bi"
+
+
+
+#line 7 "prepro92.bi"
+OK
+#line 9 "prepro92.bi"
diff --git a/tests/functional/zxbpp/prepro93.out b/tests/functional/zxbpp/prepro93.out
index 5be344977..b00ab178a 100644
--- a/tests/functional/zxbpp/prepro93.out
+++ b/tests/functional/zxbpp/prepro93.out
@@ -1,7 +1,7 @@
-#line 1 "prepro93.bi"
-
-
-
-#line 9 "prepro93.bi"
-OK
-#line 11 "prepro93.bi"
+#line 1 "prepro93.bi"
+
+
+
+#line 9 "prepro93.bi"
+OK
+#line 11 "prepro93.bi"
diff --git a/tests/functional/zxbpp/prepro94.out b/tests/functional/zxbpp/prepro94.out
index f712fd64e..9a631f91b 100644
--- a/tests/functional/zxbpp/prepro94.out
+++ b/tests/functional/zxbpp/prepro94.out
@@ -1,6 +1,6 @@
-#line 1 "prepro94.bi"
-
-
-#line 8 "prepro94.bi"
- OK
-#line 10 "prepro94.bi"
+#line 1 "prepro94.bi"
+
+
+#line 8 "prepro94.bi"
+ OK
+#line 10 "prepro94.bi"
diff --git a/tests/functional/zxbpp/prepro95.out b/tests/functional/zxbpp/prepro95.out
index 233206023..5a7dd76ff 100644
--- a/tests/functional/zxbpp/prepro95.out
+++ b/tests/functional/zxbpp/prepro95.out
@@ -1,5 +1,5 @@
-#line 1 "prepro95.bi"
-
-#line 11 "prepro95.bi"
- OK
-#line 13 "prepro95.bi"
+#line 1 "prepro95.bi"
+
+#line 11 "prepro95.bi"
+ OK
+#line 13 "prepro95.bi"
diff --git a/tests/functional/zxbpp/spectrum.out b/tests/functional/zxbpp/spectrum.out
index 81886253d..752c23498 100644
--- a/tests/functional/zxbpp/spectrum.out
+++ b/tests/functional/zxbpp/spectrum.out
@@ -1,5 +1,5 @@
-#line 1 "spectrum.bi"
-
-
-
-PRINT "HELLO WORLD!"
+#line 1 "spectrum.bi"
+
+
+
+PRINT "HELLO WORLD!"
diff --git a/tests/functional/zxbpp/stringizing0.out b/tests/functional/zxbpp/stringizing0.out
index dda07c45b..a8d4304b7 100644
--- a/tests/functional/zxbpp/stringizing0.out
+++ b/tests/functional/zxbpp/stringizing0.out
@@ -1,6 +1,6 @@
-#line 1 "stringizing0.bi"
-
-
-#line 4 "stringizing0.bi"
-
-"1"
+#line 1 "stringizing0.bi"
+
+
+#line 4 "stringizing0.bi"
+
+"1"
diff --git a/tests/functional/zxbpp/stringizing1.out b/tests/functional/zxbpp/stringizing1.out
index 17de860a1..41500b96a 100644
--- a/tests/functional/zxbpp/stringizing1.out
+++ b/tests/functional/zxbpp/stringizing1.out
@@ -1,8 +1,8 @@
-#line 1 "stringizing1.bi"
-
-
-#line 4 "stringizing1.bi"
-#line 5 "stringizing1.bi"
-
-"ab"
-"5"
+#line 1 "stringizing1.bi"
+
+
+#line 4 "stringizing1.bi"
+#line 5 "stringizing1.bi"
+
+"ab"
+"5"
diff --git a/tests/functional/zxbpp/stringizing2.out b/tests/functional/zxbpp/stringizing2.out
index 84d7d3d01..d6689b993 100644
--- a/tests/functional/zxbpp/stringizing2.out
+++ b/tests/functional/zxbpp/stringizing2.out
@@ -1,6 +1,6 @@
-#line 1 "stringizing2.bi"
-
-
-#line 4 "stringizing2.bi"
-
-"""a """" string"""
+#line 1 "stringizing2.bi"
+
+
+#line 4 "stringizing2.bi"
+
+"""a """" string"""
diff --git a/tests/functional/zxbpp/token-paste0.out b/tests/functional/zxbpp/token-paste0.out
index b623d4d94..366812a66 100644
--- a/tests/functional/zxbpp/token-paste0.out
+++ b/tests/functional/zxbpp/token-paste0.out
@@ -1,9 +1,9 @@
-#line 1 "token-paste0.bi"
-
-
-#line 4 "token-paste0.bi"
-
-AB
-
-
-A ## B
+#line 1 "token-paste0.bi"
+
+
+#line 4 "token-paste0.bi"
+
+AB
+
+
+A ## B
diff --git a/tests/functional/zxbpp/token-paste1.out b/tests/functional/zxbpp/token-paste1.out
index 9ecbbc591..cce8bdbd6 100644
--- a/tests/functional/zxbpp/token-paste1.out
+++ b/tests/functional/zxbpp/token-paste1.out
@@ -1,8 +1,8 @@
-#line 1 "token-paste1.bi"
-
-
-#line 4 "token-paste1.bi"
-
-#line 6 "token-paste1.bi"
-
-(AA 1)
+#line 1 "token-paste1.bi"
+
+
+#line 4 "token-paste1.bi"
+
+#line 6 "token-paste1.bi"
+
+(AA 1)
diff --git a/tests/runtime/Makefile b/tests/runtime/Makefile
index 4808da7a1..163511909 100644
--- a/tests/runtime/Makefile
+++ b/tests/runtime/Makefile
@@ -1,7 +1,7 @@
-# vim:ts=4:noet:
-
-.PHONY: test
-
-test:
- ./test_all
-
+# vim:ts=4:noet:
+
+.PHONY: test
+
+test:
+ ./test_all
+
diff --git a/tests/runtime/run b/tests/runtime/run
index 4ea370eef..84cf8be6b 100755
--- a/tests/runtime/run
+++ b/tests/runtime/run
@@ -1,9 +1,9 @@
-# vim:et:ts=4:
-
-RUN=$(basename -s .bas $1)
-EXT=tzx
-rm -f "$RUN.$EXT"
-killall fuse 2>/dev/null
-../../zxbc.py --$EXT -aB "$@" --debug-memory || exit 1
-#fuse --auto-load --speed=100 --machine=plus2 "$RUN.$EXT" &
-speccy "$RUN.$EXT"
+# vim:et:ts=4:
+
+RUN=$(basename -s .bas $1)
+EXT=tzx
+rm -f "$RUN.$EXT"
+killall fuse 2>/dev/null
+../../zxbc.py --$EXT -aB "$@" --debug-memory || exit 1
+#fuse --auto-load --speed=100 --machine=plus2 "$RUN.$EXT" &
+speccy "$RUN.$EXT"
diff --git a/tests/runtime/test_all b/tests/runtime/test_all
index 5b2cb3c9f..c43884b2e 100755
--- a/tests/runtime/test_all
+++ b/tests/runtime/test_all
@@ -1,4 +1,4 @@
-#!/bin/bash
-
-# run tests in parallel, one per CPU
-parallel ./test_case ::: cases/*.bas
+#!/bin/bash
+
+# run tests in parallel, one per CPU
+parallel ./test_case ::: cases/*.bas
diff --git a/tests/runtime/test_case b/tests/runtime/test_case
index 5ca2603f9..792c0fa13 100755
--- a/tests/runtime/test_case
+++ b/tests/runtime/test_case
@@ -1,17 +1,17 @@
-#!/bin/bash
-# vim:et:ts=4:
-
-# Test a single case (prog.bas file)
-# A RAM dump /expected/prog.tzx.scr must exists
-
-TIMEOUT=180
-TIMEKILL=$((TIMEOUT+30))
-
-echo -n "Testing $(basename $1): "
-RUN=$(basename -s .bas $1).z80
-EXPECTED=$(basename -s .bas $1).tzx.scr
-../../zxbc.py -f z80 -aB -o "$RUN" $1 $(grep "PARAMS:" $1 |cut -d':' -f2-) --debug-memory 2>/dev/null
-timeout -k $TIMEKILL $TIMEOUT ./check_test.py "$RUN" "./expected/${EXPECTED}"
-RETVAL=$?
-rm -f "$RUN" 2>/dev/null
-exit $RETVAL
+#!/bin/bash
+# vim:et:ts=4:
+
+# Test a single case (prog.bas file)
+# A RAM dump /expected/prog.tzx.scr must exists
+
+TIMEOUT=180
+TIMEKILL=$((TIMEOUT+30))
+
+echo -n "Testing $(basename $1): "
+RUN=$(basename -s .bas $1).z80
+EXPECTED=$(basename -s .bas $1).tzx.scr
+../../zxbc.py -f z80 -aB -o "$RUN" $1 $(grep "PARAMS:" $1 |cut -d':' -f2-) --debug-memory 2>/dev/null
+timeout -k $TIMEKILL $TIMEOUT ./check_test.py "$RUN" "./expected/${EXPECTED}"
+RETVAL=$?
+rm -f "$RUN" 2>/dev/null
+exit $RETVAL
diff --git a/tests/runtime/update_test.sh b/tests/runtime/update_test.sh
index 929195ac6..8f0c116d2 100755
--- a/tests/runtime/update_test.sh
+++ b/tests/runtime/update_test.sh
@@ -1,7 +1,7 @@
-#!/bin/bash
-
-# ./run $1
-NAME=$(basename -s .bas $1).z80
-../../zxbc.py -f z80 -aB "$@" --debug-memory || exit 1
-./update_test.py $NAME
-mv $NAME.scr expected/$(basename -s .bas $1).tzx.scr
+#!/bin/bash
+
+# ./run $1
+NAME=$(basename -s .bas $1).z80
+../../zxbc.py -f z80 -aB "$@" --debug-memory || exit 1
+./update_test.py $NAME
+mv $NAME.scr expected/$(basename -s .bas $1).tzx.scr
diff --git a/tools/profile.sh b/tools/profile.sh
index e0baf1687..3ffdc6530 100755
--- a/tools/profile.sh
+++ b/tools/profile.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
-
-python -m cProfile "$@"
-
+#!/bin/bash
+
+python -m cProfile "$@"
+