diff --git a/skoolkit/skoolasm.py b/skoolkit/skoolasm.py index 6ca617b8..0bd901c4 100644 --- a/skoolkit/skoolasm.py +++ b/skoolkit/skoolasm.py @@ -1,4 +1,4 @@ -# Copyright 2008-2023 Richard Dymond (rjdymond@gmail.com) +# Copyright 2008-2024 Richard Dymond (rjdymond@gmail.com) # # This file is part of SkoolKit. # @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License along with # SkoolKit. If not, see . +from collections import defaultdict import re from skoolkit import (CASE_LOWER, skoolmacro, SkoolKitError, SkoolParsingError, @@ -95,6 +96,7 @@ def __init__(self, parser, properties, templates, config): self.snapshot = self.parser.snapshot self._snapshots = [(self.snapshot, '')] + self.pokes = defaultdict(list) self.list_parser = ListParser(properties.get('bullet', '*')) @@ -227,6 +229,10 @@ def push_snapshot(self, name=''): :param name: An optional name for the snapshot. """ self._snapshots.append((self.snapshot.copy(), name)) + self.pokes[name].clear() + + def save_pokes(self, addr, byte, length, step): + self.pokes[self._snapshots[-1][1]].append((addr, byte, length, step)) def expand_audio(self, text, index): if self.handle_unsupported_macros: diff --git a/skoolkit/skoolhtml.py b/skoolkit/skoolhtml.py index c6d82248..e524a210 100644 --- a/skoolkit/skoolhtml.py +++ b/skoolkit/skoolhtml.py @@ -76,6 +76,7 @@ def __init__(self, skool_parser, ref_parser, file_info=None, code_id=MAIN_CODE_I self.snapshot = self.parser.snapshot self._snapshots = [(self.snapshot, '')] + self.pokes = defaultdict(list) self.asm_entry_dicts = {} self.map_entry_dicts = {} self.nonexistent_entry_dict = defaultdict(lambda: '', exists=0) @@ -458,6 +459,10 @@ def push_snapshot(self, name=''): :param name: An optional name for the snapshot. """ self._snapshots.append((self.snapshot.copy(), name)) + self.pokes[name].clear() + + def save_pokes(self, addr, byte, length, step): + self.pokes[self.get_snapshot_name()].append((addr, byte, length, step)) def get_page_ids(self): return self.page_ids diff --git a/skoolkit/skoolmacro.py b/skoolkit/skoolmacro.py index 8dac3a88..88d5f2c3 100644 --- a/skoolkit/skoolmacro.py +++ b/skoolkit/skoolmacro.py @@ -501,7 +501,7 @@ def get_macros(writer): '#DEF': partial(parse_def, writer), '#EVAL': partial(parse_eval, writer.fields, writer.case == CASE_LOWER), '#FOR': partial(parse_for, writer.fields), - '#FOREACH': partial(parse_foreach, writer.parser), + '#FOREACH': partial(parse_foreach, writer), '#FORMAT': partial(parse_format, writer.fields), '#IF': partial(parse_if, writer.fields), '#LET': partial(parse_let, writer), @@ -912,7 +912,7 @@ def parse_for(fields, text, index, *cwd): return end, html.escape(''.join(elements)) return end, ''.join(elements) -def parse_foreach(entry_holder, text, index, *cwd): +def parse_foreach(writer, text, index, *cwd): # #FOREACH([v1,v2,...])(var,string[,sep,fsep]) try: end, values = parse_strings(text, index) @@ -922,6 +922,7 @@ def parse_foreach(entry_holder, text, index, *cwd): end, (var, s, sep, fsep) = parse_strings(text, end, 4, ('', None)) except (NoParametersError, MissingParameterError) as e: raise MacroParsingError("No variable name: {}".format(text[index:e.args[1]])) + entry_holder = writer.parser if len(values) == 1: value = values[0] if value.startswith(('EREF', 'REF')): @@ -940,6 +941,18 @@ def parse_foreach(entry_holder, text, index, *cwd): elif value.startswith('ENTRY'): types = value[5:] values = [str(e.address) for e in entry_holder.memory_map if e.ctl != 'i' and (not types or e.ctl in types)] + elif value.startswith('POKE'): + name = value[4:] + values = [] + for addr, byte, length, step in writer.pokes[name]: + if length == 1: + values.append(f'POKE {addr},{byte}') + else: + end = addr + (length - 1) * step + if step == 1: + values.append(f'FOR n={addr} TO {end}: POKE n,{byte}: NEXT n') + else: + values.append(f'FOR n={addr} TO {end} STEP {step}: POKE n,{byte}: NEXT n') if not values: return end, '' if fsep is None: @@ -1156,6 +1169,7 @@ def parse_pokes(writer, text, index, *cwd): while end < index or (end < len(text) and text[end] == ';'): end, addr, byte, length, step = parse_ints(text, end + 1, 4, (1, 1), fields=writer.fields) writer.snapshot[addr:addr + length * step:step] = [byte] * length + writer.save_pokes(addr, byte, length, step) return end, '' def parse_pops(writer, text, index, *cwd): diff --git a/sphinx/source/changelog.rst b/sphinx/source/changelog.rst index 73347fee..b29f466e 100644 --- a/sphinx/source/changelog.rst +++ b/sphinx/source/changelog.rst @@ -10,6 +10,8 @@ Changelog configuration parameter values) * Added support to :ref:`skool2bin.py` for padding the output with zeroes (as specified by the ``PadLeft`` and ``PadRight`` configuration parameters) +* Added support to the :ref:`FOREACH` macro for the ``POKEname`` special + variable * Fixed how the 'ADC A,*', 'SBC A,*', 'ADC HL,rr' and 'SBC HL,rr' instructions affect the half-carry flag * Fixed how 'BIT n,(IX/Y+d)' affects bits 3 and 5 of the flags in the C version diff --git a/sphinx/source/skool-macros.rst b/sphinx/source/skool-macros.rst index cc1f0e6b..c8e4dbf1 100644 --- a/sphinx/source/skool-macros.rst +++ b/sphinx/source/skool-macros.rst @@ -461,6 +461,8 @@ expands to a specific sequence of strings. The special variables are: the memory map; if ``types`` is not given, every type is included * ``EREFaddr`` - the addresses of the routines that jump to or call a given instruction (at ``addr``) +* ``POKEname`` - the POKEs made by the :ref:`POKES` macro on the named snapshot + created by the :ref:`PUSHS` macro * ``REFaddr`` - the addresses of the routines that jump to or call a given routine (at ``addr``), or jump to or call any entry point within that routine @@ -471,14 +473,24 @@ For example:: This instance of the ``#FOREACH`` macro expands to a list of the addresses of the entries of type ``t`` (text). +The format of an item produced by ``POKEname`` depends on the values of the +``length`` and ``step`` parameters of the corresponding :ref:`POKES` macro. For +example: + +* ``#POKES30000,1`` gives 'POKE 30000,1' +* ``#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' + See :ref:`stringParameters` for details on alternative ways to supply the ``s1,s2,...`` and ``var,string[,sep,fsep]`` parameter strings. -+---------+---------+ -| Version | Changes | -+=========+=========+ -| 5.1 | New | -+---------+---------+ ++---------+-----------------------------------------------------+ +| Version | Changes | ++=========+=====================================================+ +| 9.4 | Added support for the ``POKEname`` special variable | ++---------+-----------------------------------------------------+ +| 5.1 | New | ++---------+-----------------------------------------------------+ .. _FORMAT: diff --git a/tests/macrotest.py b/tests/macrotest.py index 11296e3d..6ba09c9c 100644 --- a/tests/macrotest.py +++ b/tests/macrotest.py @@ -990,6 +990,70 @@ def test_macro_foreach_with_ref_invalid(self): self.assertEqual(writer.expand('#FOREACH(REFx)(n,n)'), 'REFx') self.assertEqual(writer.expand('#FOREACH[REF(x)](n,n)'), 'REF(x)') + def test_macro_foreach_with_poke(self): + skool = """ + @start + b30000 DEFB 1 + """ + writer = self._get_writer(skool=skool) + writer.expand('#PUSHSfoo #POKES30000,2 #POPS') + self.assertEqual(writer.expand('#FOREACH(POKEfoo)(p,p)'), 'POKE 30000,2') + + def test_macro_foreach_with_poke_length(self): + skool = """ + @start + b30000 DEFB 1,1 + """ + writer = self._get_writer(skool=skool) + 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_and_step(self): + skool = """ + @start + b30000 DEFB 1,0,1 + """ + writer = self._get_writer(skool=skool) + 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_multiple(self): + skool = """ + @start + b30000 DEFB 1,1,1 + """ + writer = self._get_writer(skool=skool) + writer.expand('#PUSHSqux #POKES30000,5;30001,6;30002,7 #POPS') + self.assertEqual(writer.expand('#FOREACH(POKEqux)(p,[p])'), '[POKE 30000,5][POKE 30001,6][POKE 30002,7]') + + def test_macro_foreach_with_poke_none(self): + skool = """ + @start + b30000 DEFB 1 + """ + writer = self._get_writer(skool=skool) + writer.expand('#PUSHSxyzzy #POKES30000,2 #POPS') + self.assertEqual(writer.expand('#FOREACH(POKEnope)(p,p)'), '') + + def test_macro_foreach_with_poke_nameless_snapshot(self): + skool = """ + @start + b30000 DEFB 1 + """ + writer = self._get_writer(skool=skool) + writer.expand('#PUSHS #POKES30000,8 #POPS') + self.assertEqual(writer.expand('#FOREACH(POKE)(n,n)'), 'POKE 30000,8') + + def test_macro_foreach_with_poke_two_snapshots(self): + skool = """ + @start + b30000 DEFB 1 + """ + writer = self._get_writer(skool=skool) + writer.expand('#PUSHSthud #POKES30000,9 #POPS') + writer.expand('#PUSHSthud #POKES30000,10 #POPS') + self.assertEqual(writer.expand('#FOREACH(POKEthud)(a,a)'), 'POKE 30000,10') + def test_macro_foreach_invalid(self): writer = self._get_writer() prefix = ERROR_PREFIX.format('FOREACH')