Skip to content

Commit

Permalink
Enable configuration of the output format of #FOREACH(POKEname)
Browse files Browse the repository at this point in the history
  • Loading branch information
skoolkid committed Aug 28, 2024
1 parent 7c6b5ec commit df5e548
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 16 deletions.
47 changes: 32 additions & 15 deletions skoolkit/skoolmacro.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@

_cwd = ()

CFG = {
'poke': 'POKE {addr},{byte}',
'pokes': 'FOR n={start} TO {end}: POKE n,{byte}: NEXT n',
'pokes-step': 'FOR n={start} TO {end} STEP {step}: POKE n,{byte}: NEXT n',
}

FILL_UDG = Udg(66, [129, 66, 36, 24, 24, 36, 66, 128])

MACRO_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
Expand Down Expand Up @@ -954,15 +960,23 @@ def parse_foreach(writer, text, index, *cwd):
else:
indexes = (None, None)
values = []
cfg = writer.fields['cfg']
for addr, byte, length, step in writer.pokes[name][slice(*indexes)]:
if length == 1:
values.append(f'POKE {addr},{byte}')
fmt = cfg['poke']
fvals = {'addr': addr, 'byte': byte}
else:
end = addr + (length - 1) * step
addr2 = addr + (length - 1) * step
if step == 1:
values.append(f'FOR n={addr} TO {end}: POKE n,{byte}: NEXT n')
fmt = cfg['pokes']
fvals = {'start': addr, 'end': addr2, 'byte': byte}
else:
values.append(f'FOR n={addr} TO {end} STEP {step}: POKE n,{byte}: NEXT n')
fmt = cfg['pokes-step']
fvals = {'start': addr, 'end': addr2, 'step': step, 'byte': byte}
try:
values.append(fmt.format(**fvals))
except KeyError as e:
raise FormattingError(f"Unrecognised field '{e.args[0]}': {fmt}")
if not values:
return end, ''
if fsep is None:
Expand Down Expand Up @@ -1052,18 +1066,21 @@ def parse_let(writer, text, index, *cwd):
end, stmt = parse_strings(text, index, 1)
name, sep, value = stmt.partition('=')
if name and sep:
value = _format_params(writer.expand(value, *cwd), text[index:end], **writer.fields)
if name.endswith('[]'):
try:
args = parse_strings(value, 0)[1]
except NoParametersError:
raise NoParametersError(f"No values provided: '{name}={value}'")
writer.fields[name[:-2]] = _eval_map(args, value, name.endswith('$[]'))
if name.startswith('cfg[') and name.endswith(']'):
writer.fields['cfg'][name[4:-1]] = value
else:
try:
writer.fields[name] = eval_variable(name, value)
except ValueError:
raise InvalidParameterError("Cannot parse integer value '{}': {}".format(value, stmt))
value = _format_params(writer.expand(value, *cwd), text[index:end], **writer.fields)
if name.endswith('[]'):
try:
args = parse_strings(value, 0)[1]
except NoParametersError:
raise NoParametersError(f"No values provided: '{name}={value}'")
writer.fields[name[:-2]] = _eval_map(args, value, name.endswith('$[]'))
else:
try:
writer.fields[name] = eval_variable(name, value)
except ValueError:
raise InvalidParameterError("Cannot parse integer value '{}': {}".format(value, stmt))
elif name:
raise InvalidParameterError("Missing variable value: '{}'".format(stmt))
else:
Expand Down
3 changes: 2 additions & 1 deletion skoolkit/skoolparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
warn, get_int_param, parse_int, read_bin_file, open_file, z80)
from skoolkit.components import get_assembler, get_instruction_utility
from skoolkit.skool2bin import BinWriter
from skoolkit.skoolmacro import INTEGER, MacroParsingError, parse_if
from skoolkit.skoolmacro import CFG, INTEGER, MacroParsingError, parse_if
from skoolkit.skoolutils import (DIRECTIVES, Z80_ASSEMBLER, Comment, Memory, get_address, join_comments,
parse_address_comments, parse_address_range, parse_addresses, parse_asm_bank_directive,
parse_asm_bytes_directive, parse_asm_data_directive, parse_asm_keep_directive,
Expand Down Expand Up @@ -113,6 +113,7 @@ def __init__(self, skoolfile, case=0, base=0, asm_mode=0, warnings=False, fix_mo
'html': int(html)
}
self.fields.update({
'cfg': CFG.copy(),
'mode': self.fields.copy(),
'vars': defaultdict(int, variables)
})
Expand Down
43 changes: 43 additions & 0 deletions sphinx/source/skool-macros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ macros:
* ``case`` - 1 if the ``--lower`` option is used with :ref:`skool2asm.py`
or :ref:`skool2html.py`, 2 if the ``--upper`` option is used, or 0 if neither
option is used
* ``cfg`` - a dictionary of skool macro
:ref:`configuration parameters <configurationParameters>`
* ``fix`` - 1 if in :ref:`ofixMode`, 2 if in :ref:`bfixMode`, 3 if in
:ref:`rfixMode`, or 0 otherwise
* ``html`` - 1 if in HTML mode, 0 otherwise
Expand All @@ -163,6 +165,9 @@ expands to ``hl`` if in lower case mode, or ``HL`` otherwise.
Note that if a replacement field is used, the parameter string must be
enclosed in parentheses.

.. versionchanged:: 9.4
Added the ``cfg`` dictionary.

.. versionchanged:: 8.7
Added the ``sim`` dictionary.

Expand All @@ -173,6 +178,38 @@ enclosed in parentheses.
The ``asm`` replacement field indicates the exact ASM mode; added the
``fix`` and ``vars`` replacement fields.

.. _configurationParameters:

Configuration parameters
^^^^^^^^^^^^^^^^^^^^^^^^
The ``cfg`` dictionary contains skool macro configuration parameters.
Recognised parameter names are:

* ``poke`` - the format used for a single POKE by the ``POKEname`` special
variable of the :ref:`FOREACH` macro (default: ``POKE {addr},{byte}``)
* ``pokes`` - the format used for a contiguous range of POKEs by the
``POKEname`` special variable of the :ref:`FOREACH` macro (default:
``FOR n={start} TO {end}: POKE n,{byte}: NEXT n``)
* ``pokes-step`` - the format used for a range of POKEs with a STEP value by the
``POKEname`` special variable of the :ref:`FOREACH` macro (default:
``FOR n={start} TO {end} STEP {step}: POKE n,{byte}: NEXT n``)

To set the value of a configuration parameter, use the :ref:`LET` macro with
the following special syntax::

#LET(cfg[name]=value)

For example, to set the ``poke`` parameter to use hexadecimal values::

#LET(cfg[poke]=POKE ${addr:04X},${byte:02X})

Parameter values may also contain skool macros::

#LET(cfg[pokes]=#FOR({start},{end})//a/POKE a,{byte}/: //)

This would make the ``POKEname`` special variable emit a sequence of POKE
statements separated by colons instead of a FOR loop.

.. _SMPLmacros:

SMPL macros
Expand Down Expand Up @@ -481,6 +518,9 @@ example:
* ``#POKES30000,1,2`` gives 'FOR n=30000 TO 30001: POKE n,1: NEXT n'
* ``#POKES30000,1,2,3`` gives 'FOR n=30000 TO 30003 STEP 3: POKE n,1: NEXT n'

The format of these items can be changed by setting the ``poke``, ``pokes``
and ``pokes-step`` :ref:`configuration parameters <configurationParameters>`.

To specify a subset of the POKEs in a list, use either an index parameter, or
start and stop parameters separated by a colon, enclosed in square brackets and
appended to ``name``. As with Python sequences, these parameters are 0-based,
Expand Down Expand Up @@ -640,6 +680,9 @@ string value '?', and keys '1' and '2' mapping to the string values 'a' and
'b'. The values in this dictionary are accessible to other macros via the
replacement fields ``{d$[1]}`` and ``{d$[2]}``.

The ``#LET`` macro may also be used to set skool macro
:ref:`configuration parameter <configurationParameters>` values.

To define a variable that will be available for use immediately anywhere in the
skool file or ref files, consider using the :ref:`expand` directive.

Expand Down
68 changes: 68 additions & 0 deletions tests/macrotest.py
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,24 @@ def test_macro_foreach_with_poke(self):
writer.expand('#PUSHSfoo #POKES30000,2 #POPS')
self.assertEqual(writer.expand('#FOREACH(POKEfoo)(p,p)'), 'POKE 30000,2')

def test_macro_foreach_with_poke_formatted(self):
skool = """
@start
b30000 DEFB 1
"""
writer = self._get_writer(skool=skool)
writer.expand('#LET(cfg[poke]=POKE ${addr:04X},${byte:02X}) #PUSHSfoo #POKES30000,2 #POPS')
self.assertEqual(writer.expand('#FOREACH(POKEfoo)(p,p)'), 'POKE $7530,$02')

def test_macro_foreach_with_poke_formatted_using_macros(self):
skool = """
@start
b30000 DEFB 1
"""
writer = self._get_writer(skool=skool)
writer.expand('#LET(cfg[poke]=#N({addr}),#N{byte}) #PUSHSfoo #POKES30000,2 #POPS')
self.assertEqual(writer.expand('#FOREACH(POKEfoo)(p,p)'), '30000,2')

def test_macro_foreach_with_poke_length(self):
skool = """
@start
Expand All @@ -1008,6 +1026,24 @@ def test_macro_foreach_with_poke_length(self):
writer.expand('#PUSHSbar #POKES30000,3,2 #POPS')
self.assertEqual(writer.expand('#FOREACH(POKEbar)(p,p)'), 'FOR n=30000 TO 30001: POKE n,3: NEXT n')

def test_macro_foreach_with_poke_length_formatted(self):
skool = """
@start
b30000 DEFB 1,1
"""
writer = self._get_writer(skool=skool)
writer.expand('#LET(cfg[pokes]=POKE {start}-{end},{byte}) #PUSHSbar #POKES30000,3,2 #POPS')
self.assertEqual(writer.expand('#FOREACH(POKEbar)(p,p)'), 'POKE 30000-30001,3')

def test_macro_foreach_with_poke_length_formatted_using_macros(self):
skool = """
@start
b30000 DEFB 1,1
"""
writer = self._get_writer(skool=skool)
writer.expand('#LET(cfg[pokes]=#FOR({start},{end})//a/POKE a,{byte}/: //) #PUSHSbar #POKES30000,3,2 #POPS')
self.assertEqual(writer.expand('#FOREACH(POKEbar)(p,p)'), 'POKE 30000,3: POKE 30001,3')

def test_macro_foreach_with_poke_length_and_step(self):
skool = """
@start
Expand All @@ -1017,6 +1053,24 @@ def test_macro_foreach_with_poke_length_and_step(self):
writer.expand('#PUSHSbaz #POKES30000,4,2,2 #POPS')
self.assertEqual(writer.expand('#FOREACH(POKEbaz)(p,p)'), 'FOR n=30000 TO 30002 STEP 2: POKE n,4: NEXT n')

def test_macro_foreach_with_poke_length_and_step_formatted(self):
skool = """
@start
b30000 DEFB 1,0,1
"""
writer = self._get_writer(skool=skool)
writer.expand('#LET(cfg[pokes-step]=POKE {start}-{end}-{step},{byte}) #PUSHSbaz #POKES30000,4,2,2 #POPS')
self.assertEqual(writer.expand('#FOREACH(POKEbaz)(p,p)'), 'POKE 30000-30002-2,4')

def test_macro_foreach_with_poke_length_and_step_formatted_using_macros(self):
skool = """
@start
b30000 DEFB 1,0,1
"""
writer = self._get_writer(skool=skool)
writer.expand('#LET(cfg[pokes-step]=#FOR({start},{end},{step})//p/POKE p,{byte}/: //) #PUSHSbaz #POKES30000,4,2,2 #POPS')
self.assertEqual(writer.expand('#FOREACH(POKEbaz)(p,p)'), 'POKE 30000,4: POKE 30002,4')

def test_macro_foreach_with_poke_multiple(self):
skool = """
@start
Expand Down Expand Up @@ -1079,6 +1133,20 @@ def test_macro_foreach_with_poke_slicing(self):
self.assertEqual(writer.expand('#FOREACH(POKEbbb[1:])(p,[p])'), '[POKE 30001,2][POKE 30002,3]')
self.assertEqual(writer.expand('#FOREACH(POKEbbb[:])(p,[p])'), '[POKE 30000,1][POKE 30001,2][POKE 30002,3]')

def test_macro_foreach_with_poke_invalid(self):
writer = self._get_writer(skool='b16384 DEFS 49152')
writer.expand('#LET(cfg[poke]=POKE {no})')
writer.expand('#LET(cfg[pokes]=POKE {nope})')
writer.expand('#LET(cfg[pokes-step]=POKE {nah})')
writer.expand('#PUSHSsnap1 #POKES16384,255 #POPS')
writer.expand('#PUSHSsnap2 #POKES16384,255,2 #POPS')
writer.expand('#PUSHSsnap3 #POKES16384,255,2,2 #POPS')
prefix = ERROR_PREFIX.format('FOREACH')

self._assert_error(writer, '#FOREACH(POKEsnap1)(p,p)', "Unrecognised field 'no': POKE {no}", prefix)
self._assert_error(writer, '#FOREACH(POKEsnap2)(p,p)', "Unrecognised field 'nope': POKE {nope}", prefix)
self._assert_error(writer, '#FOREACH(POKEsnap3)(p,p)', "Unrecognised field 'nah': POKE {nah}", prefix)

def test_macro_foreach_invalid(self):
writer = self._get_writer()
prefix = ERROR_PREFIX.format('FOREACH')
Expand Down

0 comments on commit df5e548

Please sign in to comment.