From 9d537ab3c06aaff540cceadba9359bbfc3cd1219 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Tue, 1 Sep 2026 04:20:38 +0200 Subject: [PATCH] warn when %zpreserve/%zpallowed excludes fixed zeropage scratch locations --- codeCore/src/prog8/code/core/MemoryRegions.kt | 20 +++++++++++++++++++ compiler/src/prog8/compiler/Compiler.kt | 2 ++ docs/source/programming.rst | 9 +++++++++ docs/source/todo.rst | 1 + 4 files changed, 32 insertions(+) diff --git a/codeCore/src/prog8/code/core/MemoryRegions.kt b/codeCore/src/prog8/code/core/MemoryRegions.kt index f4a69bebf..4f5508990 100644 --- a/codeCore/src/prog8/code/core/MemoryRegions.kt +++ b/codeCore/src/prog8/code/core/MemoryRegions.kt @@ -33,6 +33,26 @@ abstract class Zeropage(options: CompilationOptions): MemoryAllocator(options) { val allocatedVariables = mutableMapOf() val free = mutableSetOf() // subclasses must set this to the appropriate free locations. + + /** + * Report a warning for every scratch byte that is excluded from allocation by a %zpreserved + * or %zpallowed directive. The compiler's code generators always use these fixed scratch + * locations internally (P8ZP_SCRATCH_*), so a %zpreserved/%zpallowed range that excludes one + * of them would let the compiler overwrite memory the user explicitly wants to protect. + */ + fun checkScratchConflicts(errors: IErrorReporter) { + if(options.zeropage== ZeropageType.DONTUSE) + return + + val scratchBytes = listOf(SCRATCH_B1, SCRATCH_REG, SCRATCH_W1, SCRATCH_W1 + 1u, SCRATCH_W2, SCRATCH_W2 + 1u, SCRATCH_PTR, SCRATCH_PTR + 1u) + for(byte in scratchBytes) { + val preserved = options.zpReserved.any { byte in it } + val allowed = options.zpAllowed.any { byte in it } + if(preserved || !allowed) + errors.warn("zero-page address $${byte.toString(16)} is a fixed scratch location used by the compiler (P8ZP_SCRATCH_*), but it is excluded by a %zpreserved or %zpallowed directive", Position.DUMMY) + } + } + fun removeReservedFromFreePool() { for (reserved in options.zpReserved) reserve(reserved) diff --git a/compiler/src/prog8/compiler/Compiler.kt b/compiler/src/prog8/compiler/Compiler.kt index 44f9838bc..9b75c7b3c 100644 --- a/compiler/src/prog8/compiler/Compiler.kt +++ b/compiler/src/prog8/compiler/Compiler.kt @@ -214,6 +214,8 @@ fun compileProgram(args: CompilerArguments): CompilationResult? { // re-initialize memory areas with final compilationOptions compilationOptions.compTarget.initializeMemoryAreas(compilationOptions) + if(compilationOptions.compTarget.cpu.is6502) + compilationOptions.compTarget.zeropage.checkScratchConflicts(args.errors) if (args.printAst1) { println("\n*********** COMPILER AST *************") diff --git a/docs/source/programming.rst b/docs/source/programming.rst index 60f709209..dd743a031 100644 --- a/docs/source/programming.rst +++ b/docs/source/programming.rst @@ -582,6 +582,15 @@ Directives Global setting, can occur multiple times. It allows you to reserve or 'block' a part of the zeropage so that it will not be used by the compiler. +.. note:: + The compiler itself always needs a few fixed 'scratch' locations in the zeropage + (``P8ZP_SCRATCH_B1``, ``P8ZP_SCRATCH_REG``, ``P8ZP_SCRATCH_W1``, ``P8ZP_SCRATCH_W2`` and + ``P8ZP_SCRATCH_PTR``, together with their adjacent bytes) to perform address computations and + store temporary values. These locations live at fixed, target-specific, addresses and cannot + be relocated. If a ``%zpreserved`` or ``%zpallowed`` directive excludes any of these scratch + bytes, the compiler reports a warning, because it would otherwise silently overwrite memory that + you explicitly wanted to protect. + Loops ----- diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 057e373af..139277ccc 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -27,6 +27,7 @@ Future Things and Ideas - support typed pointer arrays as struct fields, curretly requires untyped pointers arrays. - support arrays-of-struct-instances as struct fields (e.g. ``struct Outer { Inner[4] inners }``). Currently rejected with an error. - support immediate struct instance variables (e.g. ``Enemy e``) instead of only ``^^Enemy`` pointers and ``Enemy[]`` arrays. Attempted Aug 2026 and rolled back — requires value-type IR (currently ``STRUCT_INSTANCE`` is pointer-only, see ``DataTypes.kt:22`` and ``IRCodeGen.kt:2332``; field access assumes ``^^Struct`` indirection). Far-off; use ``^^Enemy`` or ``Enemy[N]``. +- consider making the fixed zero-page scratch locations (``P8ZP_SCRATCH_B1``/``_REG``/``_W1``/``_W2``/``_PTR``) dynamic rather than hardcoded per target. Right now they live at fixed per-target addresses and are always reserved; a ``%zpreserved``/``%zpallowed`` range that excludes them only produces a warning (the codegen would still overwrite that memory). Relocating scratch to an available low-address byte (or making it configurable) when not in conflict would let ``%zpreserved``/``%zpallowed`` fully protect the desired areas. See ``MemoryRegions.kt`` (``Zeropage.scratchBytes``/``checkScratchConflicts``) and the per-target ``*Zeropage`` classes. Romable (%option romable)