Skip to content

Commit

Permalink
Add support for not repeating an 'N' directive at the start of a loop
Browse files Browse the repository at this point in the history
  • Loading branch information
skoolkid committed Jul 9, 2023
1 parent 03e3d24 commit 183146f
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 15 deletions.
23 changes: 14 additions & 9 deletions skoolkit/ctlparser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2009-2022 Richard Dymond (rjdymond@gmail.com)
# Copyright 2009-2023 Richard Dymond (rjdymond@gmail.com)
#
# This file is part of SkoolKit.
#
Expand Down Expand Up @@ -158,13 +158,13 @@ def parse_ctls(self, ctlfiles, min_address=0, max_address=65536):
count = lengths[0][0]
if count > 1:
if len(lengths) > 1:
repeat_entries = lengths[1][0]
flags = lengths[1][0]
else:
repeat_entries = 0
flags = 0
loop_end = start + count * (end - start)
if loop_end > 65536:
warn('Loop crosses 64K boundary:\n{}'.format(s_line))
self._loops.append((start, end, count, repeat_entries))
self._loops.append((start, end, count, flags))
self._subctls[loop_end] = None
else:
self._subctls[start] = ctl.lower()
Expand Down Expand Up @@ -293,17 +293,22 @@ def _terminate_multiline_comments(self):
self._multiline_comments[address] = (max_end, text, repeat)

def _unroll_loops(self, max_address):
for start, end, count, repeat_entries in self._loops:
for directives in (self._subctls, self._mid_block_comments, self._instruction_comments, self._lengths):
for start, end, count, flags in self._loops:
for directives in (self._subctls, self._instruction_comments, self._lengths):
self._repeat_directives(directives, start, end, count, max_address)
self._repeat_directives(self._mid_block_comments, start, end, count, max_address, flags == 2)
self._repeat_multiline_comments(start, end, count, max_address)
if repeat_entries:
if flags == 1:
# Repeat entries
for directives in (self._ctls, self._titles, self._descriptions, self._registers, self._end_comments):
self._repeat_directives(directives, start, end, count, max_address)

def _repeat_directives(self, directives, start, end, count, max_address):
def _repeat_directives(self, directives, start, end, count, max_address, exclude_start=False):
interval = end - start
repeated = {k: v for k, v in directives.items() if start <= k < end}
if exclude_start:
repeated = {k: v for k, v in directives.items() if start < k < end}
else:
repeated = {k: v for k, v in directives.items() if start <= k < end}
for addr, value in repeated.items():
for i in range(1, count):
address = addr + i * interval
Expand Down
2 changes: 2 additions & 0 deletions sphinx/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Changelog
* Added the ``load`` simulated LOAD configuration parameter to
:ref:`tap2sna.py <tap2sna-sim-load>` (to specify an alternative command line
to use to load the tape)
* Added support to :ref:`control directive loops <ctlLoops>` for avoiding
repetition of an ``N`` directive at the start of a loop

8.10 (2023-06-17)
-----------------
Expand Down
41 changes: 35 additions & 6 deletions sphinx/source/control-files.rst
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ repeating pattern. For example::
Repeating patterns like this can be expressed more succinctly as a loop by
using the ``L`` directive, which has the following format::

L start,length,count[,blocks]
L start,length,count[,flags]

where:

Expand All @@ -432,8 +432,14 @@ where:
repeat)
* ``count`` is the number of times to repeat the loop (only values of 2 or more
make sense)
* ``blocks`` is ``1`` to repeat block-level elements, or ``0`` to repeat only
sub-block elements (default: ``0``)
* ``flags`` controls which elements in the loop are repeated (see below)

``flags`` may have one of the following values:

* 0 - repeat sub-block elements only (this is the default)
* 1 - repeat block-level elements and sub-block elements
* 2 - repeat sub-block elements only, except any mid-block comment at the loop
start address

So using the ``L`` directive, the body of the data block above can be expressed
in three lines instead of 20::
Expand All @@ -443,8 +449,8 @@ in three lines instead of 20::
W 30002
L 30000,4,10

The ``L`` directive can also be used to repeat entire blocks, by setting the
``blocks`` argument to ``1``. For example::
The ``L`` directive can also be used to repeat entire blocks, by setting
``flags`` to ``1``. For example::

b 40000 A block of five pairs of bytes
B 40000,10,2
Expand All @@ -459,6 +465,26 @@ is equivalent to::
b 40020 A block of five pairs of bytes
B 40020,10,2

By default, an ``N`` directive at the beginning of a loop is repeated. To avoid
that, set ``flags`` to 2. For example::

b 50000 Three groups of bytes and words
N 50000 The three groups are as follows:
B 50000 Byte
W 50001 Word
L 50000,3,3,2

is equivalent to::

b 50000 Three groups of bytes and words
N 50000 The three groups are as follows:
B 50000 Byte
W 50001 Word
B 50003 Byte
W 50004 Word
B 50006 Byte
W 50007 Word

Note that ASM directives in the address range of an ``L`` directive loop are
*not* repeated.

Expand Down Expand Up @@ -757,7 +783,7 @@ L directive
The ``L`` directive defines a control file loop that repeats a sequence of
other control directives::

L start,length,count[,blocks]
L start,length,count[,flags]

See :ref:`ctlLoops` for more details.

Expand Down Expand Up @@ -921,6 +947,9 @@ Revision history
+---------+-------------------------------------------------------------------+
| Version | Changes |
+=========+===================================================================+
| 9.0 | Added support to the ``L`` directive for avoiding repetition of |
| | an ``N`` directive at the start of the loop |
+---------+-------------------------------------------------------------------+
| 8.7 | Added support to the ``M`` directive for applying its comment to |
| | each instruction in its range |
+---------+-------------------------------------------------------------------+
Expand Down
79 changes: 79 additions & 0 deletions tests/test_ctlparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1930,6 +1930,85 @@ def test_loop_including_entries(self):
exp_directives = {start + 20: ['label=END']}
self._check_instruction_asm_directives(exp_directives, blocks)

def test_loop_excluding_mid_block_comment_at_loop_start_address(self):
start = 30000
length = 25
count = 2
end = start + length * count
ctl = """
@ 30000 start
@ 30000 org=30000
c 30000 This entry should not be repeated
D 30000 This entry description should not be repeated
R 30000 HL This register should not be repeated
N 30000 This comment should not be repeated
30000,5 Begin
B 30005,5,1,2
N 30010 A mid-block comment
M 30010,10 A multi-line comment
S 30010,6
W 30016,4,4
@ 30020 label=END
T 30020,5,4:n1 End
E 30000 This end comment should not be repeated
L {},{},{},2
""".format(start, length, count)
ctl_parser = self._get_ctl_parser(ctl)
blocks = ctl_parser.get_blocks()
self.assertEqual(len(blocks), 1)
block = blocks[0]
sub_blocks = block.blocks
sub_block_map = {b.start: b for b in sub_blocks}

# Check B, C, S, T and W sub-blocks
i = 0
exp_subctls = {}
exp_sublengths = {}
for a in range(start, end, length):
for offset, subctl, sublengths in (
(0, 'c', ((0, 'n'),)),
(5, 'b', ((1, 'n'),)),
(6, 'b', ((2, 'n'),)),
(10, 's', ((0, 'n'),)),
(16, 'w', ((4, 'n'),)),
(20, 't', ((4, 'c'), (1, 'n')),),
(25, 'c', ((0, 'n'),))
):
address = a + offset
exp_subctls[address] = subctl
exp_sublengths[address] = sublengths
i += 1
self._check_subctls(exp_subctls, blocks)
self._check_sublengths(exp_sublengths, blocks)

# Check mid-block comment at the start of the loop
self.assertEqual([['This comment should not be repeated']], sub_block_map[start].header)
for a in range(start + length, end, length):
self.assertEqual((), sub_block_map[a].header, f'Unexpected repeated mid-block comment at {a}')

# Check mid-block comments in the middle of the loop
offset = 10
for a in range(start + offset, end, length):
self.assertEqual([['A mid-block comment']], sub_block_map[a].header, f'Mid-block comment mismatch at {a}')

# Check multi-line comments
offset = 10
for a in range(start + offset, end, length):
self.assertEqual((a + offset, [(0, 'A multi-line comment')]), sub_block_map[a].multiline_comment)

# Check entry-level directives (c, D, E, R)
self._check_ctls({start: 'c'}, blocks)
self.assertEqual([['This entry description should not be repeated']], block.description)
self.assertEqual([['HL This register should not be repeated']], block.registers)
self.assertEqual([['This end comment should not be repeated']], block.end_comment)

# Check entry-level ASM directives
self.assertEqual(['start', 'org=30000'], block.asm_directives)

# Check instruction-level ASM directives
exp_directives = {start + 20: ['label=END']}
self._check_instruction_asm_directives(exp_directives, blocks)

def test_loop_crossing_64k_boundary(self):
ctl = """
u 65532
Expand Down

0 comments on commit 183146f

Please sign in to comment.