Skip to content

Commit

Permalink
Enable #LET to set individual values in a dictionary
Browse files Browse the repository at this point in the history
  • Loading branch information
skoolkid committed Sep 3, 2024
1 parent 3521d63 commit feb18c6
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 20 deletions.
43 changes: 30 additions & 13 deletions skoolkit/skoolmacro.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,21 +1066,38 @@ def parse_let(writer, text, index, *cwd):
end, stmt = parse_strings(text, index, 1)
name, sep, value = stmt.partition('=')
if name and sep:
if name.startswith('cfg[') and name.endswith(']'):
writer.fields['cfg'][name[4:-1]] = value
m = re.match(r'(.+)\[([^]]*)\]$', name)
if m:
dname = m.group(1)
key = m.group(2)
if dname == 'cfg':
writer.fields['cfg'][key] = value
else:
value = _format_params(writer.expand(value, *cwd), text[index:end], **writer.fields)
if key:
key = _format_params(writer.expand(key, *cwd), text[index:end], **writer.fields)
try:
key = evaluate(key)
except ValueError:
raise InvalidParameterError(f"Cannot parse integer value '{key}': {stmt}")
try:
writer.fields[dname][key] = eval_variable(dname, value)
except ValueError:
raise InvalidParameterError(f"Cannot parse integer value '{value}': {stmt}")
except KeyError as e:
raise InvalidParameterError(f"Unrecognised dictionary '{e.args[0]}': {stmt}")
else:
try:
args = parse_strings(value, 0)[1]
except NoParametersError:
raise NoParametersError(f"No values provided: '{name}={value}'")
writer.fields[dname] = _eval_map(args, value, dname.endswith('$'))
else:
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))
try:
writer.fields[name] = eval_variable(name, value)
except ValueError:
raise InvalidParameterError(f"Cannot parse integer value '{value}': {stmt}")
elif name:
raise InvalidParameterError("Missing variable value: '{}'".format(stmt))
else:
Expand Down
2 changes: 2 additions & 0 deletions sphinx/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Changelog
:ref:`trace.py <trace-conf>` (to specify the scale factor of the PNG image)
* Added support to the :ref:`FOREACH` macro for the ``POKEname`` special
variable
* Added support to the :ref:`LET` macro for setting individual key-value pairs
in dictionary variables
* 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
Expand Down
25 changes: 18 additions & 7 deletions sphinx/source/skool-macros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,14 @@ 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]}``.

An individual key-value pair in a dictionary can be set by using the following
syntax::

#LET(name[key]=value)

Here ``key`` is the integer key, which may be expressed using skool macros and
replacement fields.

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

Expand All @@ -690,13 +698,16 @@ See :ref:`stringParameters` for details on alternative ways to supply the
entire ``name=value`` parameter string, or the part after the equals sign when
defining a dictionary variable.

+---------+--------------------------------------------------+
| Version | Changes |
+=========+==================================================+
| 8.6 | Added the ability to define dictionary variables |
+---------+--------------------------------------------------+
| 8.2 | New |
+---------+--------------------------------------------------+
+---------+-------------------------------------------------------------------+
| Version | Changes |
+=========+===================================================================+
| 9.4 | Added the ability to set individual key-value pairs in dictionary |
| | variables |
+---------+-------------------------------------------------------------------+
| 8.6 | Added the ability to define dictionary variables |
+---------+-------------------------------------------------------------------+
| 8.2 | New |
+---------+-------------------------------------------------------------------+

.. _MAP:

Expand Down
43 changes: 43 additions & 0 deletions tests/macrotest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,24 @@ def test_macro_let_dictionary_of_strings(self):
self.assertEqual(writer.fields['g$'], {1: '5'})
self.assertEqual(writer.fields['g$'][0], ':')

def test_macro_let_dictionary_of_strings_set_item(self):
writer = self._get_writer()
self.assertEqual(writer.expand('#LET(d$[]=(?))'), '')

# Set item
self.assertEqual(writer.expand('#LET(d$[0]=foo)'), '')
self.assertEqual(writer.fields['d$'], {0: 'foo'})

# Set item using replacement fields
self.assertEqual(writer.expand('#LET(a=1)#LET(v$=bar)'), '')
self.assertEqual(writer.expand('#LET(d$[{a}]={v$})'), '')
self.assertEqual(writer.fields['d$'], {0: 'foo', 1: 'bar'})

# Set item using skool macros
self.assertEqual(writer.expand('#LET(b=2)'), '')
self.assertEqual(writer.expand('#LET(d$[#IF(0)({a},{b})]=#IF(1)(baz))'), '')
self.assertEqual(writer.fields['d$'], {0: 'foo', 1: 'bar', 2: 'baz'})

def test_macro_let_dictionary_of_integers(self):
writer = self._get_writer()

Expand Down Expand Up @@ -1453,6 +1471,24 @@ def test_macro_let_dictionary_of_integers(self):
self.assertEqual(writer.fields['e'], {1: 1, 2: 2, 3: 3, 4: 4})
self.assertEqual(writer.fields['e'][0], 256)

def test_macro_let_dictionary_of_integers_set_item(self):
writer = self._get_writer()
self.assertEqual(writer.expand('#LET(d[]=(0))'), '')

# Set item
self.assertEqual(writer.expand('#LET(d[0]=1)'), '')
self.assertEqual(writer.fields['d'], {0: 1})

# Set item using replacement fields
self.assertEqual(writer.expand('#LET(a=1)#LET(v=2)'), '')
self.assertEqual(writer.expand('#LET(d[{a}]={v})'), '')
self.assertEqual(writer.fields['d'], {0: 1, 1: 2})

# Set item using skool macros
self.assertEqual(writer.expand('#LET(b=2)'), '')
self.assertEqual(writer.expand('#LET(d[#IF(0)({a},{b})]=#IF(1)(4))'), '')
self.assertEqual(writer.fields['d'], {0: 1, 1: 2, 2: 4})

def test_macro_let_dictionary_with_alternative_delimiters(self):
writer = self._get_writer()

Expand Down Expand Up @@ -1484,6 +1520,13 @@ def test_macro_let_invalid(self):
self._assert_error(writer, '#LET(f[]=(1,x1:3))', "Invalid key (x1): (1,x1:3)", prefix)
self._assert_error(writer, '#LET(f[]=(0,1:y))', "Invalid integer value (y): (0,1:y)", prefix)
self._assert_error(writer, '#LET(f[]=1,2:2)', "No terminating delimiter: 1,2:2", prefix)
self._assert_error(writer, '#LET(g[q]=1)', "Cannot parse integer value 'q': g[q]=1", prefix)
self._assert_error(writer, '#LET(g[{no}]=1)', "Unrecognised field 'no': (g[{no}]=1)", prefix)
self._assert_error(writer, '#LET(g[{bad]=1)', 'Invalid format string: (g[{bad]=1)', prefix)
self._assert_error(writer, '#LET(g[0]=q)', "Cannot parse integer value 'q': g[0]=q", prefix)
self._assert_error(writer, '#LET(g[0]={no})', "Unrecognised field 'no': (g[0]={no})", prefix)
self._assert_error(writer, '#LET(g[0]={bad)', 'Invalid format string: (g[0]={bad)', prefix)
self._assert_error(writer, '#LET(g[0]=1)', "Unrecognised dictionary 'g': g[0]=1", prefix)

def test_macro_link_invalid(self):
writer = self._get_writer()
Expand Down

0 comments on commit feb18c6

Please sign in to comment.