diff --git a/skoolkit/skoolmacro.py b/skoolkit/skoolmacro.py index 267270df..bc306cee 100644 --- a/skoolkit/skoolmacro.py +++ b/skoolkit/skoolmacro.py @@ -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: diff --git a/sphinx/source/changelog.rst b/sphinx/source/changelog.rst index c388494c..79f29bc9 100644 --- a/sphinx/source/changelog.rst +++ b/sphinx/source/changelog.rst @@ -21,6 +21,8 @@ Changelog :ref:`trace.py ` (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 diff --git a/sphinx/source/skool-macros.rst b/sphinx/source/skool-macros.rst index f119acc7..7ff25bee 100644 --- a/sphinx/source/skool-macros.rst +++ b/sphinx/source/skool-macros.rst @@ -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 ` values. @@ -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: diff --git a/tests/macrotest.py b/tests/macrotest.py index 489bb202..9829eb3c 100644 --- a/tests/macrotest.py +++ b/tests/macrotest.py @@ -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() @@ -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() @@ -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()