From 8fb0aa4a7426c75e7def12b0fee6bbf0daa17fd5 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Fri, 3 Jul 2026 06:13:33 +0200 Subject: [PATCH 01/22] Initial commit for DynaMAL branch --- .gitignore | 4 +++- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index dde54e8f..d30099f5 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,6 @@ nosetests.xml !examples/resources/* # uv lock file -uv.lock \ No newline at end of file +uv.lock + +*.local.* \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3538b7ed..dbc31008 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "PyYAML", "py2neo", "networkx", - "tree-sitter-mal>=1.2.3, <2.0.0", + "tree-sitter-mal @ git+https://github.com/sandorstormen/tree-sitter-mal.git@dynamal-grammar", "tree-sitter", ] license = {text = "Apache Software License"} From 99fa77e9eb3853fff31dcd0375cfd31ba0c6494f Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Wed, 15 Jul 2026 04:33:58 +0200 Subject: [PATCH 02/22] Add a test to compile wiperLang --- tests/language/test_compiler.py | 4 ++++ tests/testdata/wiperLang.mal | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/testdata/wiperLang.mal diff --git a/tests/language/test_compiler.py b/tests/language/test_compiler.py index f4367719..75f06ae3 100644 --- a/tests/language/test_compiler.py +++ b/tests/language/test_compiler.py @@ -69,3 +69,7 @@ def test_compile_actions_effects(): assert lang_graph.assets['AssetB'].attack_steps['hack'].causal_mode == 'effect' assert lang_graph.assets['AssetB'].attack_steps['attack'].causal_mode == 'action' assert lang_graph.assets['AssetB'].attack_steps['test'].causal_mode is None + +def test_compile_wiperlang(): + """Test that we can compile the wiperlang.mal language""" + MalCompiler().compile("tests/testdata/wiperLang.mal") \ No newline at end of file diff --git a/tests/testdata/wiperLang.mal b/tests/testdata/wiperLang.mal new file mode 100644 index 00000000..7efaff04 --- /dev/null +++ b/tests/testdata/wiperLang.mal @@ -0,0 +1,40 @@ +#id "wiperlang" +#version "1.0" + +asset Internet {} +abstract asset Host {} +asset C2Server extends Host {} +asset Device extends Host { + | infect + A> self / malware[Wiper] // Add Malware vertex + -> malware.activate +} + +abstract asset Malware { + | activate + -> trigger + | trigger +} + +asset Wiper extends Malware { + | activate + +> exfiltrate, propagate + | exfiltrate + A> victim.data / // Link data + ~victim.inet.hosts[C2Server].data // to C2 + | propagate + A> victim.sendTo / malware[Wiper] + -> victim.sendTo.malware[Wiper].activate + | trigger + R> victim / malware ^ ~data + // Retain victim, then self-destruct + wipe +} + +asset Data {} + +associations { + Internet [inet] 1 <-- contains --> * [hosts] Host + Host [receiveFrom] * <-- neighbor --> * [sendTo] Host + Host [node] 1 <-- store --> * [data] Data + Host [victim] 1 <-- infect --> * [malware] Malware +} From 75f381719f88c4b96aa5343d3b424e72b56bf291 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Thu, 16 Jul 2026 16:04:29 +0200 Subject: [PATCH 03/22] Added compilation of DynaMAL grammar --- maltoolbox/language/compiler/mal_analyzer.py | 4 +- maltoolbox/language/compiler/mal_compiler.py | 54 ++++++++++++++- tests/language/test_compiler.py | 29 +++++++- tests/testdata/wiperLang.mal | 69 +++++++++++--------- 4 files changed, 122 insertions(+), 34 deletions(-) diff --git a/maltoolbox/language/compiler/mal_analyzer.py b/maltoolbox/language/compiler/mal_analyzer.py index 3c114bd2..1dbb03cb 100644 --- a/maltoolbox/language/compiler/mal_analyzer.py +++ b/maltoolbox/language/compiler/mal_analyzer.py @@ -584,12 +584,12 @@ def _read_steps(self, asset: str, parents: List) -> None: or attackStep_node_reaches.child_by_field_name( 'operator' ).text.decode() - == '->' + in ('->', 'A>', 'R>') ): # Valid step current_steps.append(attackStep) else: - # Step was defined using '+>', but there is nothing to inherit from + # Step was defined using '+>', '+A>', or '+R>', but there is nothing to inherit from error_msg = f"Cannot inherit attack step '{attackStep}' without previous definition" self._raise_analyzer_exception(error_msg) else: diff --git a/maltoolbox/language/compiler/mal_compiler.py b/maltoolbox/language/compiler/mal_compiler.py index 2ee31b6a..fc219899 100644 --- a/maltoolbox/language/compiler/mal_compiler.py +++ b/maltoolbox/language/compiler/mal_compiler.py @@ -463,9 +463,19 @@ def visit_attack_step(self, cursor: TreeCursor) -> tuple[str, dict]: go_to_sibling(cursor) reaches = None + dyn_reaches = None if assert_node(cursor.node).type == 'reaching': reaches = self.visit(cursor) go_to_sibling(cursor) + if assert_node(cursor.node).type == 'dyn_reaching': + dyn_reaches = self.visit(cursor) + go_to_sibling(cursor) + elif assert_node(cursor.node).type == 'dyn_reaching': + dyn_reaches = self.visit(cursor) + go_to_sibling(cursor) + if assert_node(cursor.node).type == 'reaching': + reaches = self.visit(cursor) + go_to_sibling(cursor) ret = { 'name': name, @@ -478,6 +488,7 @@ def visit_attack_step(self, cursor: TreeCursor) -> tuple[str, dict]: 'ttc': ttc, 'requires': requires, 'reaches': reaches, + 'dyn_reaches': dyn_reaches } return ('step', ret) @@ -797,9 +808,50 @@ def visit_reaching(self, cursor: TreeCursor): ret['stepExpressions'].append(self.visit(cursor)) return ret + + def visit_dyn_reaching(self, cursor: TreeCursor): + ################################################################ + # ( '+A>' | 'A>' | '+R>' | 'R>' ) (reaches) ( ',' (reaches) )* # + ################################################################ + + ret: dict[str, Any] = {} + + # Get type of reaches + ret['overrides'] = assert_node(cursor.node).text in (b'A>', b'R>') + go_to_sibling(cursor) + + # Visit the steps + ret['stepExpressions'] = [self.visit(cursor)] + + while go_to_sibling(cursor): # check if we have a ',' + go_to_sibling(cursor) # ignore the ',' + ret['stepExpressions'].append(self.visit(cursor)) + + return ret + + def visit_dyn_sentence(self, cursor: TreeCursor): + ######################################## + # (base) '/' (target) ( '^' (target) )* # + ######################################## + + base = self.visit(cursor) + go_to_sibling(cursor) + assert assert_node(cursor.node).type == 'dynamic_separator', 'Expected dynamic separator in dynamic sentence' + go_to_sibling(cursor) + targets = [self.visit(cursor)] + while go_to_sibling(cursor): + assert assert_node(cursor.node).type == 'hold', 'Expected hold between targets in dynamic sentence' + go_to_sibling(cursor) + targets.append(self.visit(cursor)) + + return {"base": base, "targets": targets} + + def visit_assoc_and_asset_expr(self, cursor: TreeCursor): + return {"type": "assoc_op", "operand": self.visit(cursor)} def visit_asset_expr(self, cursor: TreeCursor): - return self._visit_inline_asset_expr(cursor) + ret = self._visit_inline_asset_expr(cursor) + return ret def _visit_inline_asset_expr(self, cursor: TreeCursor): ############################################################################################################################################# diff --git a/tests/language/test_compiler.py b/tests/language/test_compiler.py index 75f06ae3..d34355a2 100644 --- a/tests/language/test_compiler.py +++ b/tests/language/test_compiler.py @@ -72,4 +72,31 @@ def test_compile_actions_effects(): def test_compile_wiperlang(): """Test that we can compile the wiperlang.mal language""" - MalCompiler().compile("tests/testdata/wiperLang.mal") \ No newline at end of file + result = MalCompiler().compile("tests/testdata/wiperLang.mal") + + try: + malware_asset = next(asset for asset in result["assets"] if asset["name"] == "Malware") + except StopIteration: + pytest.fail("Malware asset not found") + try: + activate_step = next(step for step in malware_asset["attackSteps"] if step["name"] == "activate") + except StopIteration: + pytest.fail("activate attack step not found in Device asset") + assert activate_step["reaches"]["stepExpressions"][0]["name"] == "trigger" + + try: + device_asset = next(asset for asset in result["assets"] if asset["name"] == "Device") + except StopIteration: + pytest.fail("Device asset not found") + try: + infect_step = next(step for step in device_asset["attackSteps"] if step["name"] == "infect") + except StopIteration: + pytest.fail("infect attack step not found in Device asset") + + assert infect_step["reaches"]["stepExpressions"][0]["lhs"]["name"] == "malware" + assert infect_step["reaches"]["stepExpressions"][0]["rhs"]["name"] == "activate" + + assert infect_step["dyn_reaches"]["stepExpressions"][0]["base"]["name"] == "self" + assert infect_step["dyn_reaches"]["stepExpressions"][0]["targets"][0]["operand"]["stepExpression"]["name"] == "malware" + + wiper_lang = LanguageGraph(result) \ No newline at end of file diff --git a/tests/testdata/wiperLang.mal b/tests/testdata/wiperLang.mal index 7efaff04..e0c628f8 100644 --- a/tests/testdata/wiperLang.mal +++ b/tests/testdata/wiperLang.mal @@ -1,36 +1,45 @@ -#id "wiperlang" -#version "1.0" - -asset Internet {} -abstract asset Host {} -asset C2Server extends Host {} -asset Device extends Host { - | infect - A> self / malware[Wiper] // Add Malware vertex - -> malware.activate -} -abstract asset Malware { - | activate - -> trigger - | trigger -} +#id: "org.mal-lang.wiperLang" +#version: "1.0.0" -asset Wiper extends Malware { - | activate - +> exfiltrate, propagate - | exfiltrate - A> victim.data / // Link data - ~victim.inet.hosts[C2Server].data // to C2 - | propagate - A> victim.sendTo / malware[Wiper] - -> victim.sendTo.malware[Wiper].activate - | trigger - R> victim / malware ^ ~data - // Retain victim, then self-destruct + wipe -} +category WiperLang { + asset Internet {} + abstract asset Host {} + asset C2Server extends Host {} + asset Device extends Host { + | infect + A> self / malware[Wiper] // Add Malware vertex + -> malware.activate + } + + abstract asset Malware { + | activate + -> trigger + | trigger -asset Data {} + | test + A> trigger + } + + asset Wiper extends Malware { + | activate + +> exfiltrate, propagate + | exfiltrate + A> victim.data / // Link data + ~victim.inet.hosts[C2Server].data // to C2 + | propagate + A> victim.sendTo / malware[Wiper] + -> victim.sendTo.malware[Wiper].activate + | trigger + R> victim / malware ^ ~data + // Retain victim, then self-destruct + wipe + + | test + +A> activate + } + + asset Data {} +} associations { Internet [inet] 1 <-- contains --> * [hosts] Host From f8692badf223a8f1be274d0ee6d8d1fdc74557bf Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Fri, 17 Jul 2026 08:57:30 +0200 Subject: [PATCH 04/22] Fix: New branches for dynamic reaching --- maltoolbox/language/compiler/mal_compiler.py | 66 +++++++++++++------- tests/language/test_compiler.py | 4 +- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/maltoolbox/language/compiler/mal_compiler.py b/maltoolbox/language/compiler/mal_compiler.py index fc219899..5391d308 100644 --- a/maltoolbox/language/compiler/mal_compiler.py +++ b/maltoolbox/language/compiler/mal_compiler.py @@ -462,21 +462,6 @@ def visit_attack_step(self, cursor: TreeCursor) -> tuple[str, dict]: requires = self.visit(cursor) go_to_sibling(cursor) - reaches = None - dyn_reaches = None - if assert_node(cursor.node).type == 'reaching': - reaches = self.visit(cursor) - go_to_sibling(cursor) - if assert_node(cursor.node).type == 'dyn_reaching': - dyn_reaches = self.visit(cursor) - go_to_sibling(cursor) - elif assert_node(cursor.node).type == 'dyn_reaching': - dyn_reaches = self.visit(cursor) - go_to_sibling(cursor) - if assert_node(cursor.node).type == 'reaching': - reaches = self.visit(cursor) - go_to_sibling(cursor) - ret = { 'name': name, 'meta': meta, @@ -487,10 +472,23 @@ def visit_attack_step(self, cursor: TreeCursor) -> tuple[str, dict]: 'risk': risk, 'ttc': ttc, 'requires': requires, - 'reaches': reaches, - 'dyn_reaches': dyn_reaches + 'reaches': None, + 'append_reaches': None, + 'remove_reaches': None } + reaching_types = ('reaching', 'append_reaching', 'remove_reaching') + + while assert_node(cursor.node).type in reaching_types: + if cursor.node.type == 'reaching': + ret['reaches'] = self.visit(cursor) + elif cursor.node.type == 'append_reaching': + ret['append_reaches'] = self.visit(cursor) + elif cursor.node.type == 'remove_reaching': + ret['remove_reaches'] = self.visit(cursor) + if not go_to_sibling(cursor): + break + return ('step', ret) def visit_detector(self, cursor: TreeCursor): @@ -809,15 +807,35 @@ def visit_reaching(self, cursor: TreeCursor): return ret - def visit_dyn_reaching(self, cursor: TreeCursor): - ################################################################ - # ( '+A>' | 'A>' | '+R>' | 'R>' ) (reaches) ( ',' (reaches) )* # - ################################################################ + def visit_append_reaching(self, cursor: TreeCursor): + ################################################# + # ( '+A>' | 'A>' ) (reaches) ( ',' (reaches) )* # + ################################################# + + ret: dict[str, Any] = {} + + # Get type of reaches + ret['overrides'] = assert_node(cursor.node).text == b'A>' + go_to_sibling(cursor) + + # Visit the steps + ret['stepExpressions'] = [self.visit(cursor)] + + while go_to_sibling(cursor): # check if we have a ',' + go_to_sibling(cursor) # ignore the ',' + ret['stepExpressions'].append(self.visit(cursor)) + + return ret + + def visit_remove_reaching(self, cursor: TreeCursor): + ################################################# + # ( '+R>' | 'R>' ) (reaches) ( ',' (reaches) )* # + ################################################# ret: dict[str, Any] = {} # Get type of reaches - ret['overrides'] = assert_node(cursor.node).text in (b'A>', b'R>') + ret['overrides'] = assert_node(cursor.node).text == b'R>' go_to_sibling(cursor) # Visit the steps @@ -830,9 +848,9 @@ def visit_dyn_reaching(self, cursor: TreeCursor): return ret def visit_dyn_sentence(self, cursor: TreeCursor): - ######################################## + ######################################### # (base) '/' (target) ( '^' (target) )* # - ######################################## + ######################################### base = self.visit(cursor) go_to_sibling(cursor) diff --git a/tests/language/test_compiler.py b/tests/language/test_compiler.py index d34355a2..c8280fa6 100644 --- a/tests/language/test_compiler.py +++ b/tests/language/test_compiler.py @@ -96,7 +96,7 @@ def test_compile_wiperlang(): assert infect_step["reaches"]["stepExpressions"][0]["lhs"]["name"] == "malware" assert infect_step["reaches"]["stepExpressions"][0]["rhs"]["name"] == "activate" - assert infect_step["dyn_reaches"]["stepExpressions"][0]["base"]["name"] == "self" - assert infect_step["dyn_reaches"]["stepExpressions"][0]["targets"][0]["operand"]["stepExpression"]["name"] == "malware" + assert infect_step["append_reaches"]["stepExpressions"][0]["base"]["name"] == "self" + assert infect_step["append_reaches"]["stepExpressions"][0]["targets"][0]["operand"]["stepExpression"]["name"] == "malware" wiper_lang = LanguageGraph(result) \ No newline at end of file From d17f201cb228d4bacfa53f87c23bb6690990fd8d Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Fri, 17 Jul 2026 10:11:39 +0200 Subject: [PATCH 05/22] Add type to dyn sentence --- maltoolbox/language/compiler/mal_compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maltoolbox/language/compiler/mal_compiler.py b/maltoolbox/language/compiler/mal_compiler.py index 5391d308..3fd9e9ce 100644 --- a/maltoolbox/language/compiler/mal_compiler.py +++ b/maltoolbox/language/compiler/mal_compiler.py @@ -862,7 +862,7 @@ def visit_dyn_sentence(self, cursor: TreeCursor): go_to_sibling(cursor) targets.append(self.visit(cursor)) - return {"base": base, "targets": targets} + return {"type": "dyn_sentence", "base": base, "targets": targets} def visit_assoc_and_asset_expr(self, cursor: TreeCursor): return {"type": "assoc_op", "operand": self.visit(cursor)} From 7d86652fa0b0a744e06e6af4d079fd456a8c15dc Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Sat, 18 Jul 2026 06:24:12 +0200 Subject: [PATCH 06/22] Fix assoc_and_asset_expr grammar visitation --- maltoolbox/language/compiler/mal_compiler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/maltoolbox/language/compiler/mal_compiler.py b/maltoolbox/language/compiler/mal_compiler.py index 3fd9e9ce..c256bd88 100644 --- a/maltoolbox/language/compiler/mal_compiler.py +++ b/maltoolbox/language/compiler/mal_compiler.py @@ -865,7 +865,13 @@ def visit_dyn_sentence(self, cursor: TreeCursor): return {"type": "dyn_sentence", "base": base, "targets": targets} def visit_assoc_and_asset_expr(self, cursor: TreeCursor): - return {"type": "assoc_op", "operand": self.visit(cursor)} + if assert_node(cursor.node).type == 'assoc_op': + assert cursor.goto_next_sibling(), "Expected next sibling after assoc_op" + result = {"type": "assoc_op", "operand": self.visit(cursor)} + assert result["operand"] is not None, "Expected operand in assoc_and_asset_expr" + else: + result = self.visit(cursor) + return result def visit_asset_expr(self, cursor: TreeCursor): ret = self._visit_inline_asset_expr(cursor) From 86c63689a7d4c6fb146aba4234e170436d715f3d Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Sat, 18 Jul 2026 06:35:18 +0200 Subject: [PATCH 07/22] Add credit to Viktor for wiperLang --- tests/testdata/wiperLang.mal | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/testdata/wiperLang.mal b/tests/testdata/wiperLang.mal index e0c628f8..812ec4b3 100644 --- a/tests/testdata/wiperLang.mal +++ b/tests/testdata/wiperLang.mal @@ -1,3 +1,11 @@ +/** + * @file wiperLang.mal + * @brief WiperLang example model + * @author Viktor Engström + + source: https://doi.org/10.1145/3664476.3664508 +**/ + #id: "org.mal-lang.wiperLang" #version: "1.0.0" From 4e87a6c7c1d7ae037e74dfeda6f93bd7897a86ae Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Sat, 18 Jul 2026 06:35:34 +0200 Subject: [PATCH 08/22] Add concept of model effects --- maltoolbox/language/expression_chain.py | 14 +++ .../language/language_graph_attack_step.py | 5 +- maltoolbox/language/language_graph_builder.py | 36 ++++++- .../language/language_graph_model_effect.py | 95 +++++++++++++++++++ .../language/step_expression_processor.py | 71 ++++++++++++++ tests/language/test_compiler.py | 28 +++++- 6 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 maltoolbox/language/language_graph_model_effect.py diff --git a/maltoolbox/language/expression_chain.py b/maltoolbox/language/expression_chain.py index f67b2555..163cf5ad 100644 --- a/maltoolbox/language/expression_chain.py +++ b/maltoolbox/language/expression_chain.py @@ -32,6 +32,7 @@ class ExprType(str, Enum): FIELD = "field" TRANSITIVE = "transitive" SUBTYPE = "subType" + ASSOC_OP = "assoc_op" def is_binary(self) -> bool: return self in { @@ -74,6 +75,10 @@ def _validate(self) -> None: if self.type == ExprType.SUBTYPE: if not self.sub_link or not self.subtype: raise ValueError("SUBTYPE requires sub_link and subtype") + + if self.type == ExprType.ASSOC_OP: + if not self.sub_link: + raise ValueError("ASSOC_OP requires sub_link") def to_dict(self) -> dict: if self.type.is_binary(): @@ -87,6 +92,9 @@ def to_dict(self) -> dict: if self.type == ExprType.SUBTYPE: return self._subtype_to_dict() + + if self.type == ExprType.ASSOC_OP: + return self._assoc_op_to_dict() raise LanguageGraphAssociationError( f"Unknown expressions chain element {self.type}" @@ -116,6 +124,12 @@ def _subtype_to_dict(self) -> dict: "expression": self.sub_link.to_dict(), "type": self.type.value, } + + def _assoc_op_to_dict(self) -> dict: + return { + "operand": self.sub_link.to_dict(), + "type": self.type.value + } def _field_to_dict(self) -> dict: asset_type = self._resolve_field_asset_type() diff --git a/maltoolbox/language/language_graph_attack_step.py b/maltoolbox/language/language_graph_attack_step.py index ce63dbb1..d7b6124d 100644 --- a/maltoolbox/language/language_graph_attack_step.py +++ b/maltoolbox/language/language_graph_attack_step.py @@ -6,11 +6,12 @@ from dataclasses import dataclass, field from functools import cached_property + if TYPE_CHECKING: from maltoolbox.language.expression_chain import ExpressionsChain from maltoolbox.language.language_graph_asset import LanguageGraphAsset from maltoolbox.language.language_graph_detector import LanguageGraphDetector - + from maltoolbox.language.language_graph_model_effect import LanguageGraphModelEffect @dataclass class LanguageGraphAttackStep: @@ -26,6 +27,8 @@ class LanguageGraphAttackStep: own_children: dict[ LanguageGraphAttackStep, list[ExpressionsChain | None] ] = field(default_factory=dict) + own_additive_model_effects: list[LanguageGraphModelEffect] = field(default_factory=list) + own_subtractive_model_effects: list[LanguageGraphModelEffect] = field(default_factory=list) own_parents: dict[ LanguageGraphAttackStep, list[ExpressionsChain | None] ] = field(default_factory=dict) diff --git a/maltoolbox/language/language_graph_builder.py b/maltoolbox/language/language_graph_builder.py index 7f07c035..1062e62e 100644 --- a/maltoolbox/language/language_graph_builder.py +++ b/maltoolbox/language/language_graph_builder.py @@ -9,6 +9,7 @@ from maltoolbox.language.language_graph_asset import LanguageGraphAsset from maltoolbox.language.language_graph_assoc import LanguageGraphAssociation, LanguageGraphAssociationField, link_association_to_assets from maltoolbox.language.language_graph_attack_step import LanguageGraphAttackStep +from maltoolbox.language.language_graph_model_effect import build_model_effect from maltoolbox.language.step_expression_processor import process_step_expression, resolve_variable, reverse_expr_chain @@ -200,7 +201,7 @@ def _connect_attack_steps( lang_spec: dict[str, Any], attack_step_dicts: dict[str, dict] ) -> None: - """Connect attack steps based on the 'reaches' and 'requires' expressions in the language specification.""" + """Connect attack steps based on the 'reaches', 'append_reaches', 'remove_reaches' and 'requires' expressions in the language specification.""" for asset in assets.values(): for step in asset.attack_steps.values(): @@ -244,6 +245,37 @@ def _connect_attack_steps( f'Failed to find existence step requirement for:\n{expr}' ) step.own_requires.append(chain) + + # append_reaches = attack_step_dict.get('append_reaches') or {} + # for append_expression in append_reaches.get('stepExpressions', []): + # base_asset, base_expr, base_targets = proccess_dyn_sentence(assets, step.asset, append_expression, lang_spec) + +def _connect_model_effects( + assets: dict[str, LanguageGraphAsset], + lang_spec: dict[str, Any], + attack_step_dicts: dict[str, dict] +) -> None: + """Connect model effects to attack steps""" + + for asset in assets.values(): + for step in asset.attack_steps.values(): + logger.debug('Determining children for attack step %s', step.name) + + if step.full_name not in attack_step_dicts: + # Skip steps that are not in the lang spec (inherited steps) + continue + + attack_step_dict = attack_step_dicts[step.full_name] + + append_reaches = attack_step_dict.get('append_reaches') or {} + for expr in append_reaches.get('stepExpressions', []): + model_effect = build_model_effect(assets, step.asset, expr, lang_spec) + step.own_additive_model_effects.append(model_effect) + + remove_reaches = attack_step_dict.get('remove_reaches') or {} + for expr in remove_reaches.get('stepExpressions', []): + model_effect = build_model_effect(assets, step.asset, expr, lang_spec) + step.own_subtractive_model_effects.append(model_effect) def generate_attack_steps(assets: dict[str, LanguageGraphAsset], lang_spec: dict) -> None: @@ -257,6 +289,7 @@ def generate_attack_steps(assets: dict[str, LanguageGraphAsset], lang_spec: dict 2. Inherit attack steps from super-assets, respecting overrides. 3. Link attack steps via 'reaches' and evaluate 'exist'/'notExist' requirements. + 4. Link model effects via 'append_reaches' and 'remove_reaches'. Args: assets (dict): Mapping of asset names to asset objects. @@ -271,6 +304,7 @@ def generate_attack_steps(assets: dict[str, LanguageGraphAsset], lang_spec: dict attack_step_dicts = _create_lg_attack_step_nodes(assets, lang_spec) _inherit_attack_steps(assets) _connect_attack_steps(assets, lang_spec, attack_step_dicts) + _connect_model_effects(assets, lang_spec, attack_step_dicts) def create_associations_for_assets( diff --git a/maltoolbox/language/language_graph_model_effect.py b/maltoolbox/language/language_graph_model_effect.py new file mode 100644 index 00000000..cf8e0c56 --- /dev/null +++ b/maltoolbox/language/language_graph_model_effect.py @@ -0,0 +1,95 @@ +"""LanguageGraphAttackStep functionality +- Represents a step (type) defined in a MAL language +""" +from __future__ import annotations +import copy +from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Optional +from dataclasses import dataclass, field +from functools import cached_property + +from maltoolbox.language.expression_chain import ExprType +from maltoolbox.language.step_expression_processor import process_assoc_op_step_expression, process_step_expression + +if TYPE_CHECKING: + from maltoolbox.language.expression_chain import ExpressionsChain + from maltoolbox.language.language_graph_asset import LanguageGraphAsset + from maltoolbox.language.language_graph_assoc import LanguageGraphAssociation + from maltoolbox.language.language_graph_detector import LanguageGraphDetector + + +class AssocTraversal(NamedTuple): + field_name: str + asset_filter: Optional[LanguageGraphAsset] + quantity_filter: Optional[int] + +class DynTarget(NamedTuple): + """A target in a dynamic sentence""" + assoc_op: bool # Decides if we are adding/removing assets or connections to assets + assoc_traversal: list[AssocTraversal] + +@dataclass +class LanguageGraphModelEffect: + """An effect on the model instance triggered by compromising the attack step.""" + + base: list[AssocTraversal] + targets: list[DynTarget] # Assumes ^ between targets + + +def build_model_effect( + assets: dict[str, LanguageGraphAsset], + target_asset: LanguageGraphAsset, + step_expression: dict, + lang_spec + ) -> LanguageGraphModelEffect: + """Build a model effect from a step expression""" + + assert step_expression["type"] == "dyn_sentence", "Only dynamic sentences can be used to build model effects" + + def build_assoc_traversal(expr_chain: ExpressionsChain | None) -> list[AssocTraversal]: + """Create a list of AssocTraversal from an expression chain""" + + if expr_chain is None: + return [] + elif expr_chain.type == ExprType.COLLECT: + left = build_assoc_traversal(expr_chain.left_link) + right = build_assoc_traversal(expr_chain.right_link) + return left + right + elif expr_chain.type == ExprType.DIFFERENCE: + raise NotImplementedError() + elif expr_chain.type == ExprType.FIELD: + return [AssocTraversal(field_name=expr_chain.fieldname, asset_filter=None, quantity_filter=None)] + elif expr_chain.type == ExprType.INTERSECTION: + raise NotImplementedError() + elif expr_chain.type == ExprType.SUBTYPE: + ret = build_assoc_traversal(expr_chain.sub_link) + ret[-1] = ret[-1]._replace(asset_filter=expr_chain.subtype) + return ret + elif expr_chain.type == ExprType.TRANSITIVE: + raise NotImplementedError() + elif expr_chain.type == ExprType.UNION: + raise NotImplementedError() + + base_expr = step_expression["base"] + target_exprs = step_expression["targets"] + + base_target_asset, base_expr_chain, base_step = process_step_expression(assets, target_asset, None, base_expr, lang_spec) + assert not base_step, "Base can not refer to an attack step in a dynamic sentence" + base = build_assoc_traversal(base_expr_chain) + + targets = [] + for target_expr in target_exprs: + process_function = process_assoc_op_step_expression if target_expr["type"] == "assoc_op" else process_step_expression + try: + _, target_expr_chain, target_step = process_function(assets, target_asset, None, target_expr, lang_spec) + except LookupError as e: + _, target_expr_chain, target_step = process_function(assets, base_target_asset, None, target_expr, lang_spec) + assert not target_step, "Targets can not refer to an attack step in a dynamic sentence" + if target_expr_chain.type == ExprType.ASSOC_OP: + assoc_op = True + target_expr_chain = target_expr_chain.sub_link + else: + assoc_op = False + assoc_traversal = build_assoc_traversal(target_expr_chain) + targets.append(DynTarget(assoc_op=assoc_op, assoc_traversal=assoc_traversal)) + + return LanguageGraphModelEffect(base=base, targets=targets) \ No newline at end of file diff --git a/maltoolbox/language/step_expression_processor.py b/maltoolbox/language/step_expression_processor.py index c42f6ffe..f1defbcb 100644 --- a/maltoolbox/language/step_expression_processor.py +++ b/maltoolbox/language/step_expression_processor.py @@ -19,6 +19,12 @@ Optional[str], ] +# DynStepResult = tuple[ +# LanguageGraphAsset, +# Optional[ExpressionsChain], +# list[tuple[LanguageGraphAsset, Optional[ExpressionsChain]]], +# ] + def process_attack_step_expression( target_asset: LanguageGraphAsset, step_expression: dict[str, Any] @@ -152,6 +158,15 @@ def process_field_step_expression( ), None ) + + if fieldname == 'self': + # TODO: See if this needs an expression chain of some kind + # Possibly implement another ExprType for self references + return ( + target_asset, + None, + None, + ) raise LookupError( f'Failed to find field {fieldname} on asset {target_asset.name}!', ) @@ -462,3 +477,59 @@ def resolve_variable( ) return (target_asset, expr_chain) return asset.variables[var_name] + +# def proccess_dyn_sentence( +# assets: dict[str, LanguageGraphAsset], +# target_asset: LanguageGraphAsset, +# step_expression: dict[str, Any], +# lang_spec +# ) -> DynStepResult: +# base_asset, base_expr, base_step = process_step_expression(assets, target_asset, None, step_expression["base"], lang_spec) +# assert not base_step, "A base in a dynamic sentence can not end with an attack step" + +# assoc_op_or_not = lambda step_expr: process_assoc_op_step_expression( +# assets, target_asset, None, step_expr, lang_spec +# ) if step_expr["type"] == "assoc_op" else process_step_expression( +# assets, +# target_asset, +# None, +# step_expr, +# lang_spec +# ) + +# target_results = [assoc_op_or_not(target) for target in step_expression["targets"]] +# assert all(not target_result[2] for target_result in target_results) +# # for target in step_expression["targets"]: +# # base2target_asset, base2target_expr, base2target_step = assoc_op_or_not(target) +# # assert not base2target_step, "A target in a dynamic sentence can not end with an attack step" + +# return (base_asset, base_expr, target_results) + +def process_assoc_op_step_expression( + assets: dict[str, LanguageGraphAsset], + target_asset: LanguageGraphAsset, + expr_chain: ExpressionsChain | None, + step_expression: dict[str, Any], + lang_spec +) -> StepResult: + + result_target_asset, result_expr_chain, _result_step_name = ( + process_step_expression( + assets, + target_asset, + expr_chain, + step_expression['operand'], + lang_spec + ) + ) + assert not _result_step_name, "ASSOC_OP can not have an attack step as operand" + + new_expr_chain = ExpressionsChain( + type=ExprType.ASSOC_OP, + sub_link=result_expr_chain, + ) + return ( + result_target_asset, + new_expr_chain, + None + ) \ No newline at end of file diff --git a/tests/language/test_compiler.py b/tests/language/test_compiler.py index c8280fa6..320365b7 100644 --- a/tests/language/test_compiler.py +++ b/tests/language/test_compiler.py @@ -97,6 +97,30 @@ def test_compile_wiperlang(): assert infect_step["reaches"]["stepExpressions"][0]["rhs"]["name"] == "activate" assert infect_step["append_reaches"]["stepExpressions"][0]["base"]["name"] == "self" - assert infect_step["append_reaches"]["stepExpressions"][0]["targets"][0]["operand"]["stepExpression"]["name"] == "malware" + assert infect_step["append_reaches"]["stepExpressions"][0]["targets"][0]["stepExpression"]["name"] == "malware" + assert infect_step["append_reaches"]["stepExpressions"][0]["targets"][0]["subType"] == "Wiper" - wiper_lang = LanguageGraph(result) \ No newline at end of file + wiper_lang = LanguageGraph(result) + + device = wiper_lang.assets["Device"] + wiper = wiper_lang.assets["Wiper"] + assert wiper.attack_steps["exfiltrate"] in wiper.attack_steps["activate"].own_children + assert wiper.attack_steps["propagate"] in wiper.attack_steps["activate"].own_children + assert wiper.super_assets[1].attack_steps["trigger"] in wiper.super_assets[1].attack_steps["activate"].own_children + assert device.attack_steps["infect"] in wiper.super_assets[1].attack_steps["activate"].own_parents + + device_infect = device.attack_steps["infect"] + assert device_infect.own_additive_model_effects[0].base == [], "Device:infect A> base should be self" + assert device_infect.own_additive_model_effects[0].targets[0].assoc_op == False, "Device:infect A> target should not be assoc_op" + assert device_infect.own_additive_model_effects[0].targets[0].assoc_traversal[0].field_name == "malware" + assert device_infect.own_additive_model_effects[0].targets[0].assoc_traversal[0].asset_filter == wiper_lang.assets["Wiper"] + + wiper_exfiltrate = wiper.attack_steps["exfiltrate"] + assert wiper_exfiltrate.own_additive_model_effects[0].base[0].field_name == "victim" + assert wiper_exfiltrate.own_additive_model_effects[0].base[1].field_name == "data" + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_op == True + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[0].field_name == "victim" + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[1].field_name == "inet" + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[2].field_name == "hosts" + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[2].asset_filter == wiper_lang.assets["C2Server"] + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[3].field_name == "data" From 721e43389207a7fb17c71e88baeb94788732dd4d Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Wed, 22 Jul 2026 06:20:08 +0200 Subject: [PATCH 09/22] Add sandorstormen to CODEOWNERS for relevant additions --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 57811ab6..e411c25c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,3 +3,5 @@ # Owners for a specific folder maltoolbox/language/compiler/ @nkakouros +maltoolbox/language/compiler/mal_compiler.py @nkakouros @sandorstormen +maltoolbox/language/language_graph_model_effect.py @sandorstormen From c25ef64b61344e3a13d8167f6a73d705e36ab9b4 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Wed, 22 Jul 2026 06:25:56 +0200 Subject: [PATCH 10/22] Linting --- maltoolbox/language/language_graph_model_effect.py | 10 +++------- tests/language/test_compiler.py | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/maltoolbox/language/language_graph_model_effect.py b/maltoolbox/language/language_graph_model_effect.py index cf8e0c56..f86fd961 100644 --- a/maltoolbox/language/language_graph_model_effect.py +++ b/maltoolbox/language/language_graph_model_effect.py @@ -2,10 +2,8 @@ - Represents a step (type) defined in a MAL language """ from __future__ import annotations -import copy -from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Optional -from dataclasses import dataclass, field -from functools import cached_property +from typing import TYPE_CHECKING, NamedTuple, Optional +from dataclasses import dataclass from maltoolbox.language.expression_chain import ExprType from maltoolbox.language.step_expression_processor import process_assoc_op_step_expression, process_step_expression @@ -13,8 +11,6 @@ if TYPE_CHECKING: from maltoolbox.language.expression_chain import ExpressionsChain from maltoolbox.language.language_graph_asset import LanguageGraphAsset - from maltoolbox.language.language_graph_assoc import LanguageGraphAssociation - from maltoolbox.language.language_graph_detector import LanguageGraphDetector class AssocTraversal(NamedTuple): @@ -81,7 +77,7 @@ def build_assoc_traversal(expr_chain: ExpressionsChain | None) -> list[AssocTrav process_function = process_assoc_op_step_expression if target_expr["type"] == "assoc_op" else process_step_expression try: _, target_expr_chain, target_step = process_function(assets, target_asset, None, target_expr, lang_spec) - except LookupError as e: + except LookupError: _, target_expr_chain, target_step = process_function(assets, base_target_asset, None, target_expr, lang_spec) assert not target_step, "Targets can not refer to an attack step in a dynamic sentence" if target_expr_chain.type == ExprType.ASSOC_OP: diff --git a/tests/language/test_compiler.py b/tests/language/test_compiler.py index 320365b7..fcb0fc80 100644 --- a/tests/language/test_compiler.py +++ b/tests/language/test_compiler.py @@ -111,14 +111,14 @@ def test_compile_wiperlang(): device_infect = device.attack_steps["infect"] assert device_infect.own_additive_model_effects[0].base == [], "Device:infect A> base should be self" - assert device_infect.own_additive_model_effects[0].targets[0].assoc_op == False, "Device:infect A> target should not be assoc_op" + assert not device_infect.own_additive_model_effects[0].targets[0].assoc_op, "Device:infect A> target should not be assoc_op" assert device_infect.own_additive_model_effects[0].targets[0].assoc_traversal[0].field_name == "malware" assert device_infect.own_additive_model_effects[0].targets[0].assoc_traversal[0].asset_filter == wiper_lang.assets["Wiper"] wiper_exfiltrate = wiper.attack_steps["exfiltrate"] assert wiper_exfiltrate.own_additive_model_effects[0].base[0].field_name == "victim" assert wiper_exfiltrate.own_additive_model_effects[0].base[1].field_name == "data" - assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_op == True + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_op assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[0].field_name == "victim" assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[1].field_name == "inet" assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[2].field_name == "hosts" From a4ec68870b154bec0460dfccc20d910832cb2790 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Wed, 22 Jul 2026 06:27:38 +0200 Subject: [PATCH 11/22] Add missing dev packages to pyproject.toml --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index dbc31008..abb8b6dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,9 @@ classifiers = [ [project.optional-dependencies] dev = [ "pytest", + "ruff", + "mypy", + "types-PyYAML" ] [project.scripts] From 8baa70f2f6a749f3036a2efea843b8c0957e62df Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Wed, 22 Jul 2026 06:32:06 +0200 Subject: [PATCH 12/22] Fix types --- maltoolbox/language/compiler/mal_compiler.py | 6 +++--- maltoolbox/language/expression_chain.py | 1 + maltoolbox/language/language_graph_model_effect.py | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/maltoolbox/language/compiler/mal_compiler.py b/maltoolbox/language/compiler/mal_compiler.py index c256bd88..48458591 100644 --- a/maltoolbox/language/compiler/mal_compiler.py +++ b/maltoolbox/language/compiler/mal_compiler.py @@ -480,11 +480,11 @@ def visit_attack_step(self, cursor: TreeCursor) -> tuple[str, dict]: reaching_types = ('reaching', 'append_reaching', 'remove_reaching') while assert_node(cursor.node).type in reaching_types: - if cursor.node.type == 'reaching': + if assert_node(cursor.node).type == 'reaching': ret['reaches'] = self.visit(cursor) - elif cursor.node.type == 'append_reaching': + elif assert_node(cursor.node).type == 'append_reaching': ret['append_reaches'] = self.visit(cursor) - elif cursor.node.type == 'remove_reaching': + elif assert_node(cursor.node).type == 'remove_reaching': ret['remove_reaches'] = self.visit(cursor) if not go_to_sibling(cursor): break diff --git a/maltoolbox/language/expression_chain.py b/maltoolbox/language/expression_chain.py index 163cf5ad..676846ce 100644 --- a/maltoolbox/language/expression_chain.py +++ b/maltoolbox/language/expression_chain.py @@ -126,6 +126,7 @@ def _subtype_to_dict(self) -> dict: } def _assoc_op_to_dict(self) -> dict: + assert self.sub_link, "ASSOC_OP expression requires sub_link" return { "operand": self.sub_link.to_dict(), "type": self.type.value diff --git a/maltoolbox/language/language_graph_model_effect.py b/maltoolbox/language/language_graph_model_effect.py index f86fd961..1f04ad9c 100644 --- a/maltoolbox/language/language_graph_model_effect.py +++ b/maltoolbox/language/language_graph_model_effect.py @@ -53,6 +53,7 @@ def build_assoc_traversal(expr_chain: ExpressionsChain | None) -> list[AssocTrav elif expr_chain.type == ExprType.DIFFERENCE: raise NotImplementedError() elif expr_chain.type == ExprType.FIELD: + assert expr_chain.fieldname is not None, "Fieldname must be set for FIELD expression" return [AssocTraversal(field_name=expr_chain.fieldname, asset_filter=None, quantity_filter=None)] elif expr_chain.type == ExprType.INTERSECTION: raise NotImplementedError() @@ -65,6 +66,8 @@ def build_assoc_traversal(expr_chain: ExpressionsChain | None) -> list[AssocTrav elif expr_chain.type == ExprType.UNION: raise NotImplementedError() + raise ValueError(f"Unexpected expression chain type: {expr_chain.type}") + base_expr = step_expression["base"] target_exprs = step_expression["targets"] @@ -80,6 +83,7 @@ def build_assoc_traversal(expr_chain: ExpressionsChain | None) -> list[AssocTrav except LookupError: _, target_expr_chain, target_step = process_function(assets, base_target_asset, None, target_expr, lang_spec) assert not target_step, "Targets can not refer to an attack step in a dynamic sentence" + assert target_expr_chain, "Target expression in dynamic sentence does not resolve to an expression chain" if target_expr_chain.type == ExprType.ASSOC_OP: assoc_op = True target_expr_chain = target_expr_chain.sub_link From 454ffe6e9b6c582db5377f812bfb742490ec16de Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Fri, 24 Jul 2026 08:07:20 +0200 Subject: [PATCH 13/22] Add download script and tests for DynaMAL test langs --- .gitignore | 2 + tests/language/test_compiler.py | 26 ++++++ tests/testdata/get_dynamal_test_langs.py | 103 +++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 tests/testdata/get_dynamal_test_langs.py diff --git a/.gitignore b/.gitignore index d30099f5..0e66111c 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,6 @@ nosetests.xml # uv lock file uv.lock +# Claude +.claude *.local.* \ No newline at end of file diff --git a/tests/language/test_compiler.py b/tests/language/test_compiler.py index fcb0fc80..9d0a21e9 100644 --- a/tests/language/test_compiler.py +++ b/tests/language/test_compiler.py @@ -1,3 +1,5 @@ +from pathlib import Path + from maltoolbox.language.compiler import MalCompiler from maltoolbox.language import LanguageGraph import pytest @@ -124,3 +126,27 @@ def test_compile_wiperlang(): assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[2].field_name == "hosts" assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[2].asset_filter == wiper_lang.assets["C2Server"] assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[3].field_name == "data" + +def test_compile_basic_dynamal_languages(): + dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/basic") + for lang_file in dynamal_test_langs_dir.glob("*.mal"): + try: + lang = LanguageGraph.from_mal_spec(lang_file) + except Exception as e: + pytest.fail(f"Failed to compile {lang_file}: {e}") + +def test_compile_intermediate_dynamal_languages(): + dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/intermediate") + for lang_file in dynamal_test_langs_dir.glob("*.mal"): + try: + lang = LanguageGraph.from_mal_spec(lang_file) + except Exception as e: + pytest.fail(f"Failed to compile {lang_file}: {e}") + +def test_compile_advanced_dynamal_languages(): + dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/advanced") + for lang_file in dynamal_test_langs_dir.glob("*.mal"): + try: + lang = LanguageGraph.from_mal_spec(lang_file) + except Exception as e: + pytest.fail(f"Failed to compile {lang_file}: {e}") \ No newline at end of file diff --git a/tests/testdata/get_dynamal_test_langs.py b/tests/testdata/get_dynamal_test_langs.py new file mode 100644 index 00000000..d529c856 --- /dev/null +++ b/tests/testdata/get_dynamal_test_langs.py @@ -0,0 +1,103 @@ +"""Fetch the DynaMAL test language specifications into tests/testdata. + +Clones https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs and copies +every ``*.dmal`` file it contains into this directory (flattened by +filename), so they can be committed as test fixtures. + +Usage: + python tests/testdata/get_dynamal_test_langs.py +""" +import re +import shutil +from pathlib import Path + +from maltoolbox.file_utils import download_git_repo +from maltoolbox.language.languagegraph import LanguageGraph + +DYNAMAL_TEST_LANGS_URL = ( + "https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git" +) +TESTDATA_DIR = Path(__file__).resolve().parent +test_lang_dir = TESTDATA_DIR / "dynamal_test_langs" +test_lang_dir.mkdir(exist_ok=True) + +version_pattern = re.compile(r"#version:\s*") +id_pattern = re.compile(r"#id:\s*") +def fix_version_id(lang_path: Path): + lang_text = lang_path.read_text() + prepend_text = "" + id_match = bool(id_pattern.search(lang_text)) + version_match = bool(version_pattern.search(lang_text)) + if not id_match: + prepend_text += "#id: \"" + lang_path.stem + "\"\n" + + if not version_match: + prepend_text += "#version: \"0.1.0\"\n" + + if len(prepend_text) > 0: + if id_match and version_match: + prepend_text += "\n" + lang_path.write_text(prepend_text + lang_text) + + +def add_credit_disclaimer(lang_path: Path, source_path: Path) -> None: + lang_text = lang_path.read_text() + disclaimer = ( + "/**\n" + f" * Created by Viktor Engström\n" + f" * Repo: {DYNAMAL_TEST_LANGS_URL}\n" + f" * Converted by Sandor Berglund\n" + " */\n\n" + ) + lang_path.write_text(disclaimer + lang_text) + + +def fix_category(lang_path: Path): + lang_text = lang_path.read_text() + has_category = "category" in lang_text + if not has_category: + lines = lang_text.splitlines(keepends=True) + + insert_idx = 0 + while insert_idx < len(lines) and lines[insert_idx].startswith("#"): + insert_idx += 1 + lines.insert(insert_idx, f"category {lang_path.stem} {{\n") + + assoc_idx = next( + (i for i, line in enumerate(lines) + if line.lstrip().startswith("associations")), + None, + ) + body_end = assoc_idx if assoc_idx is not None else len(lines) + for i in range(insert_idx + 1, body_end): + if lines[i].strip(): + lines[i] = "\t" + lines[i] + + if assoc_idx is not None: + lines.insert(assoc_idx, "}\n\n") + else: + lines.append("}\n") + + lang_path.write_text("".join(lines)) + + +def copy_test_langs(dir_name: str, repo_dir: Path) -> None: + sub_dir = test_lang_dir / dir_name + sub_dir.mkdir(exist_ok=True) + for copy_path in (repo_dir / dir_name).rglob("*.dmal"): + lang_path = (sub_dir / copy_path.name).with_suffix(".mal") + shutil.copy(copy_path, lang_path) + fix_version_id(lang_path) + fix_category(lang_path) + add_credit_disclaimer(lang_path, copy_path.relative_to(repo_dir)) + +def main() -> None: + repo_dir = download_git_repo(DYNAMAL_TEST_LANGS_URL) + + copy_test_langs("basic", repo_dir) + copy_test_langs("intermediate", repo_dir) + copy_test_langs("advanced", repo_dir) + + +if __name__ == "__main__": + main() From 6e80b8c8ff5a2c5a03742799c4e0b311e1a84a55 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Fri, 24 Jul 2026 08:07:35 +0200 Subject: [PATCH 14/22] Add the test langs --- .../dynamal_test_langs/advanced/diceLang.mal | 190 ++++++++ .../basic/baseDynamicTestLang1.mal | 25 ++ .../basic/baseDynamicTestLang10.mal | 29 ++ .../basic/baseDynamicTestLang2.mal | 24 + .../basic/baseDynamicTestLang3.mal | 28 ++ .../basic/baseDynamicTestLang4.mal | 26 ++ .../basic/baseDynamicTestLang5.mal | 29 ++ .../basic/baseDynamicTestLang6.mal | 27 ++ .../basic/baseDynamicTestLang7.mal | 34 ++ .../basic/baseDynamicTestLang8.mal | 34 ++ .../basic/baseDynamicTestLang9.mal | 28 ++ .../basic/doorProblemLang.mal | 25 ++ .../basic/easyRansomwareLang.mal | 33 ++ .../dynamal_test_langs/basic/glider.mal | 21 + .../dynamal_test_langs/basic/looper.mal | 20 + .../dynamal_test_langs/basic/posterLang.mal | 28 ++ .../intermediate/intDynamicTestLang1.mal | 32 ++ .../intermediate/intDynamicTestLang10.mal | 24 + .../intermediate/intDynamicTestLang11.mal | 58 +++ .../intermediate/intDynamicTestLang12.mal | 425 ++++++++++++++++++ .../intermediate/intDynamicTestLang13.mal | 263 +++++++++++ .../intermediate/intDynamicTestLang2.mal | 36 ++ .../intermediate/intDynamicTestLang3.mal | 36 ++ .../intermediate/intDynamicTestLang4.mal | 51 +++ .../intermediate/intDynamicTestLang5.mal | 45 ++ .../intermediate/intDynamicTestLang6.mal | 30 ++ .../intermediate/intDynamicTestLang7.mal | 32 ++ .../intermediate/intDynamicTestLang8.mal | 25 ++ .../intermediate/intDynamicTestLang9.mal | 26 ++ 29 files changed, 1684 insertions(+) create mode 100644 tests/testdata/dynamal_test_langs/advanced/diceLang.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/glider.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/looper.mal create mode 100644 tests/testdata/dynamal_test_langs/basic/posterLang.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal create mode 100644 tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal diff --git a/tests/testdata/dynamal_test_langs/advanced/diceLang.mal b/tests/testdata/dynamal_test_langs/advanced/diceLang.mal new file mode 100644 index 00000000..a62bc008 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/advanced/diceLang.mal @@ -0,0 +1,190 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "diceLang" +#version: "0.1.0" +category diceLang { + // A contrived language for checking various dice configurations. + + abstract asset Die { + | even + | odd + | all + | prime + | betterPrime + | evenPrime + | altEvenPrime + | notPrime + | noPrime + | noEven + | noOdd + | onlyPrime + } + + asset One extends Die {} + asset Two extends Die {} + asset Three extends Die {} + asset Four extends Die {} + asset Five extends Die {} + asset Six extends Die {} + + asset Dice { + let even = ((die[Two] \/ die[Four]) \/ die[Six]) + let odd = (die[One] \/ (die[Three] \/ die[Five])) + let all = (even() \/ odd()) + let primes = (all() /\ (die[Two] \/ die[Three] \/ die[Five])) + let betterPrimes = (die[Two] \/ die[Three] \/ die[Five]) + let evenPrimes = (all() /\ die[Two]) + let altEvenPrimes = (die[Two] /\ all()) + let notPrimes = (die[Four] \/ die[Six] \/ die[One]) + let noPrimes = (all() - primes()) + let noEven = (all() - even()) + let noOdd = (all() - odd()) + let onlyPrimes = (all() - primes()) + + let noOnes = (die[Two] \/ die[Three] \/ die[Four] \/ die[Five] \/ die[Six]) + let noTwos = (die[One] \/ die[Three] \/ die[Four] \/ die[Five] \/ die[Six]) + let noThrees = (die[Two] \/ die[One] \/ die[Four] \/ die[Five] \/ die[Six]) + let noFours = (die[Two] \/ die[Three] \/ die[One] \/ die[Five] \/ die[Six]) + let noFives = (die[Two] \/ die[Three] \/ die[Four] \/ die[One] \/ die[Six]) + let noSixes = (die[Two] \/ die[Three] \/ die[Four] \/ die[Five] \/ die[One]) + + | startingPoint + modeler info: "Start your agents here." + -> allOnes, + ((die[Two] \/ die[Four]) \/ die[Six]).even, // Testing syntax + (die[One] \/ (die[Three] \/ die[Five])).odd, // Testing syntax + allTwos, + allThrees, + allFours, + allFives, + allSixes, + oneExists, + twoExists, + threeExists, + fourExists, + fiveExists, + sixExists, + even().even, + odd().odd, + all().all, + primes().prime, + betterPrimes().betterPrime, + evenPrimes().evenPrime, + altEvenPrimes().altEvenPrime, + notPrimes().notPrime, + noPrimes().noPrime, + noEven().noEven, + noOdd().noOdd, + onlyPrimes().onlyPrime + + + & allOnes + -> yahtzee + + & allTwos + -> yahtzee + + & allThrees + -> yahtzee + + & allFours + -> yahtzee + + & allFives + -> yahtzee + + & allSixes + -> yahtzee + + | yahtzee + user info: "All show the same number, usually six dice." + + & oneExists + -> simpleSequence, + oneTwoThree, + allOnes + + & twoExists + -> simpleSequence, + oneTwoThree, + allTwos + + & threeExists + -> simpleSequence, + oneTwoThree, + allThrees + + & fourExists + -> simpleSequence, + allFours + + & fiveExists + -> simpleSequence, + allFives + + & sixExists + -> simpleSequence, + allSixes + + & oneTwoThree + & simpleSequence + + E ifOneExists + <- die[One] + -> oneExists + + E ifTwoExists + <- die[Two] + -> twoExists + + E ifThreeExists + <- die[Three] + -> threeExists + + E ifFourExists + <- die[Four] + -> fourExists + + E ifFiveExists + <- die[Five] + -> fiveExists + + E ifSixExists + <- die[Six] + -> sixExists + + !E oneComplement + <- noOnes() + -> allOnes + + !E twoComplement + <- noTwos() + -> allTwos + + !E threeComplement + <- noThrees() + -> allThrees + + !E fourComplement + <- noFours() + -> allFours + + !E fiveComplement + <- noFives() + -> allFives + + !E sixComplement + <- noSixes() + -> allSixes + + } + +} + +associations{ + Die [die] * <-- collection --> 1 [collection] Dice +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal new file mode 100644 index 00000000..24367ae4 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal @@ -0,0 +1,25 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang1" +#version: "0.1.0" +category baseDynamicTestLang1 { + asset Bowl { + | access + -> addApple + + | addApple > + A> self / apples + } + + asset Apple {} + +} + +associations { + Bowl [container] 1 <-- contains --> * [apples] Apple + +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal new file mode 100644 index 00000000..d5d967fe --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal @@ -0,0 +1,29 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang10" +#version: "0.0.1" + + +category Business { + asset Plantation { + | access + -> pilferBananas + + | pilferBananas > + developer info: "Burn bridge, eat bananas." + R> trees / ~plant ^ bananas + + } + + asset BananaTree{} + asset Banana {} +} + +associations { + Plantation [plant] 1 <-- Cultivate --> * [trees] BananaTree + BananaTree [tree] 1 <-- Contains --> * [bananas] Banana +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal new file mode 100644 index 00000000..b7c278d1 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal @@ -0,0 +1,24 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang2" +#version: "0.1.0" +category baseDynamicTestLang2 { + asset Bowl { + | access + -> eatApple + + | eatApple > + R> self / apples + } + + asset Apple {} + +} + +associations { + Bowl [container] 1 <-- contains --> * [apples] Apple +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal new file mode 100644 index 00000000..9127a3f8 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal @@ -0,0 +1,28 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang3" +#version: "0.1.0" +category baseDynamicTestLang3 { + asset Bowl { + | access + -> addApple, removeApple + + | addApple > + A> self / apples + + | removeApple > + R> self / apples + } + + asset Apple {} + +} + +associations { + Bowl [container] 1 <-- contains --> * [apples] Apple + +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal new file mode 100644 index 00000000..7923aa1a --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal @@ -0,0 +1,26 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang4" +#version: "0.1.0" +category baseDynamicTestLang4 { + asset Bowl { + | access + -> tamper + + | tamper > + A> self / apples + R> self / apples + } + + asset Apple {} + +} + +associations { + Bowl [container] 1 <-- contains --> * [apples] Apple + +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal new file mode 100644 index 00000000..dd2299cd --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal @@ -0,0 +1,29 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang5" +#version: "0.0.1" + +category Business { + asset Monkey {} + asset Typewriter {} + + asset Storage { + | access + -> giveTypewriter + + | giveTypewriter > + user info: "With enough monkeys and typewriters, we can create the complete works of Shakespeare." + A> monkeys / ~typewriters.writer + } +} + +associations { + Storage [storage] 1 <-- contains --> * [typewriters] Typewriter + Storage [storage] * <-- provision --> * [monkeys] Monkey + Monkey [writer] * <-- instrument --> * [instrument] Typewriter + +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal new file mode 100644 index 00000000..dc91f69b --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal @@ -0,0 +1,27 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang6" +#version: "0.0.1" + +category Business { + asset Monkey {} + + asset Typewriter { + | access + -> hideTypewriter + + | hideTypewriter > + user info: "Monkeys without typewriters cannot work." + developer info: "Only removes the association. The typewriter still exists in the model." + R> self / ~writer + } +} + +associations { + Typewriter [typewriter] 0..1 <-- operatedBy --> 1 [writer] Monkey + +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal new file mode 100644 index 00000000..e3b03987 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal @@ -0,0 +1,34 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang7" +#version: "0.0.1" + + +category Business { + asset Plantation { + | access + -> plantTree + + | plantTree > + A> self / trees + + } + + asset MagicPlantation extends Plantation + { + | plantTree > + +A> trees / bananas + } + + asset BananaTree{} + asset Banana {} +} + +associations { + Plantation [plant] 1 <-- cultivate --> * [trees] BananaTree + BananaTree [tree] 1 <-- contains --> * [bananas] Banana +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal new file mode 100644 index 00000000..bb700eef --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal @@ -0,0 +1,34 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang8" +#version: "0.0.1" + + +category Business { + asset Plantation { + | access + -> pilferBananas + + | pilferBananas > + R> trees / bananas + + } + + asset PlantationWithSmallTrees extends Plantation + { + | pilferBananas > + +R> trees / self + } + + asset BananaTree{} + asset Banana {} +} + +associations { + Plantation [plant] 1 <-- Cultivate --> * [trees] BananaTree + BananaTree [tree] 1 <-- Contains --> * [bananas] Banana +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal new file mode 100644 index 00000000..09049e4d --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal @@ -0,0 +1,28 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang9" +#version: "0.0.1" + + +category Business { + asset Plantation { + | access + -> plantTree + + | plantTree > + A> self / trees ^ trees.bananas + + } + + asset BananaTree{} + asset Banana {} +} + +associations { + Plantation [plant] 1 <-- cultivate --> * [trees] BananaTree + BananaTree [tree] 1 <-- contains --> * [bananas] Banana +} diff --git a/tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal b/tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal new file mode 100644 index 00000000..64cc0e36 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal @@ -0,0 +1,25 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "doorProblemLang" +#version: "0.1.0" +category doorProblemLang { + asset Room { + | leave + -> entrance.leave // Leave the room + R> self / ~entrance // Remove link between two rooms, i.e., close the door. + -> entrance.leave // You cannot leave through closed doors now. + + # deathLasers + -> leave // Prevent attackers in rooms. + + } + +} + +associations { + Room [exit] * <-- Connection --> * [entrance] Room +} diff --git a/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal b/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal new file mode 100644 index 00000000..3d9deddf --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal @@ -0,0 +1,33 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "easyRansomwareLang" +#version: "0.1.0" +category easyRansomwareLang { + asset Data {} + + asset LockedData extends Data {} + + asset Host { + | connect + -> ransomware.attack + } + + asset Ransomware { + | attack + A> host / data.locked + A> host / ~host.data.locked.victim + R> host / data + } + +} + +associations { + Host [host] * <-- containment --> * [data] Data + Host [host] 1 <-- infection --> * [ransomware] Ransomware + Data [plain] 1 <-- encryption --> 0..1 [locked] LockedData + Host [victim] 1 <-- victim --> * [infected] LockedData +} diff --git a/tests/testdata/dynamal_test_langs/basic/glider.mal b/tests/testdata/dynamal_test_langs/basic/glider.mal new file mode 100644 index 00000000..2f084ae6 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/glider.mal @@ -0,0 +1,21 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "glider" +#version: "0.1.0" +category glider { + asset Glider { + | glide > + A> self / next + R> self / previous + -> next.glide + } + +} + +associations { + Glider [previous] 0..1 <-- link --> 0..1 [next] Glider +} diff --git a/tests/testdata/dynamal_test_langs/basic/looper.mal b/tests/testdata/dynamal_test_langs/basic/looper.mal new file mode 100644 index 00000000..ee59657b --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/looper.mal @@ -0,0 +1,20 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "looper" +#version: "0.1.0" +category looper { + asset Looper { + | loop > + A> self / next + -> next.loop + } + +} + +associations { + Looper [previous] 0..1 <-- link --> 0..1 [next] Looper +} diff --git a/tests/testdata/dynamal_test_langs/basic/posterLang.mal b/tests/testdata/dynamal_test_langs/basic/posterLang.mal new file mode 100644 index 00000000..cd80f9c3 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/posterLang.mal @@ -0,0 +1,28 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "posterLang" +#version: "0.1.0" +category posterLang { + asset Host { + | infect // Start by infecting a host. + A> self / malware // Add the data wiping malware. + -> malware.execute // Prepare to execute the malware. + } + asset WiperMalware { + | execute // Awaken the malware. + -> victim.sendTo.infect // Propagate then... + R> victim / data // ... wipe system data. + } + asset Data {} + +} + +associations { + Host [receiveFrom] * <-- communicate --> * [sendTo] Host + Host [host] 1 <-- contains --> * [data] Data + Host [victim] 1 <-- infects --> * [malware] WiperMalware +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal new file mode 100644 index 00000000..429926d1 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal @@ -0,0 +1,32 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang1" +#version: "0.0.1" + +category Business { + asset Apples { + | access + -> plantBadApple + + | plantBadApple > + A> self / badApple + -> badApple.apples.spoil + + | spoil + } + + asset BadApple + user info: "One bad apple spoils the bunch!" + { + + } +} + +associations { + Apples [apples] 1 <-- spoiledBy --> 0..1 [badApple] BadApple + +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal new file mode 100644 index 00000000..35c01b1a --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal @@ -0,0 +1,24 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang10" +#version: "0.1.0" +category intDynamicTestLang10 { + asset Table { + | delegateFruit > + A> bowls / apples + A> bowls.apples / ~bowls.apples + R> bowls / ~apples + } + asset Bowl {} + asset Apple {} + +} + +associations { + Bowl [bowls] * <-- placement --> 1 [table] Table + Bowl [container] * <-- contains --> * [apples] Apple +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal new file mode 100644 index 00000000..3d7f478c --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal @@ -0,0 +1,58 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang11" +#version: "0.1.0" +category intDynamicTestLang11 { + asset Table { + | access + -> testLeftUnionRemove, + testRightUnionRemove, + testDoubleUnionRemove, + testLeftUnionAdd, + testRightUnionAdd, + testDoubleUnionAdd + + | testLeftUnionRemove > + R> (bowls \/ baskets) / apples + + | testRightUnionRemove > + R> bowls / (apples \/ oranges) + + | testDoubleUnionRemove > + R> (bowls \/ baskets) / (apples \/ oranges) + + | testLeftUnionAdd > + A> (bowls \/ baskets) / apples + + | testRightUnionAdd > + A> bowls / (apples \/ oranges) + + | testDoubleUnionAdd > + A> (bowls \/ baskets) / (apples \/ oranges) + } + + abstract asset Container {} + + abstract asset Fruit {} + + asset Bowl extends Container {} + + asset Basket extends Container {} + + asset Apple extends Fruit {} + + asset Orange extends Fruit {} + + +} + +associations{ + Table [table] 1 <-- BowlPlacement --> * [bowls] Bowl + Table [table] 1 <-- BasketPlacement --> * [baskets] Basket + Container [container] 1 <-- AppleContain --> * [apples] Apple + Container [container] 1 <-- OrangeContain --> * [oranges] Orange +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal new file mode 100644 index 00000000..5a728a37 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal @@ -0,0 +1,425 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang12" +#version: "0.1.0" +category intDynamicTestLang12 { + asset Table { + | access + -> testNodeAdditions, + testNodeRemovals, + testLinkAdditions, + testLinkRemovals, + testCaretOperations, + testEmptySetCases + + | testNodeAdditions + -> testAddLeftUnionTwo, + testAddLeftUnionThree, + testLeftUnionAddFour, + testLeftUnionAddThreeLeftNested, + testLeftUnionAddFourLeftNested, + testLeftUnionAddThreeRightNested, + testLeftUnionAddFourRightNested, + testRightUnionAddTwo, + testRightUnionAddThree, + testRightUnionAddFour, + testRightUnionAddThreeLeftNested, + testRightUnionAddFourLeftNested, + testDoubleUnionAddTwo, + testDoubleUnionAddThree, + testDoubleUnionAddFour + + | testLinkAdditions + -> testLinkLeftUnionTwo, + testLinkLeftUnionThree, + testLinkLeftUnionFour, + testLinkLeftUnionThreeLeftNested, + testLinkLeftUnionFourLeftNested, + testLinkRightUnionTwo, + testLinkRightUnionThree, + testLinkRightUnionFour, + testLinkRightUnionThreeLeftNested, + testLinkRightUnionFourLeftNested, + testLinkRightUnionThreeRightNested, + testLinkRightUnionFourRightNested, + testLinkDoubleUnionTwo, + testLinkDoubleUnionThree, + testLinkDoubleUnionFour + + | testNodeRemovals + -> testLeftUnionRemoveTwo, + testLeftUnionRemoveTwoSelf, + testLeftUnionRemoveThree, + testLeftUnionRemoveFour, + testLeftUnionRemoveThreeLeftNested, + testLeftUnionRemoveFourLeftNested, + testLeftUnionRemoveThreeRightNested, + testLeftUnionRemoveFourRightNested, + testRightUnionRemoveTwo, + testRightUnionRemoveThree, + testRightUnionRemoveFour, + testRightUnionRemoveThreeLeftNested, + testRightUnionRemoveFourLeftNested, + testDoubleUnionRemoveTwo, + testDoubleUnionRemoveThree, + testDoubleUnionRemoveFour + + | testLinkRemovals + -> testUnlinkLeftUnionRemoveTwo, + testUnlinkLeftUnionRemoveThree, + testUnlinkLeftUnionRemoveFour, + testUnlinkLeftUnionRemoveThreeLeftNested, + testUnlinkLeftUnionRemoveFourLeftNested, + testUnlinkLeftUnionRemoveThreeRightNested, + testUnlinkLeftUnionRemoveFourRightNested, + testUnlinkRightUnionRemoveTwo, + testUnlinkRightUnionRemoveThree, + testUnlinkRightUnionRemoveFour, + testUnlinkRightUnionRemoveThreeLeftNested, + testUnlinkRightUnionRemoveFourLeftNested, + testUnlinkDoubleUnionRemoveTwo, + testUnlinkDoubleUnionRemoveThree, + testUnlinkDoubleUnionRemoveFour + + | testCaretOperations + -> testAddLeftUnionAddWithCaret, + testAddRightUnionAddWithCaret, + testAddDoubleUnionWithCaret, + testAddDoubleUnionNestedWithCaret, + testAddWithBackAndForthInCaret, + testAddToTwoOthersFromBowlInCaret, + testRemoveLeftUnionRemoveWithCaret, + testRemoveRightUnionAddWithCaret, + testRemoveDoubleUnionWithCaret, + testRemoveDoubleUnionNestedWithCaret, + testRemoveWithBackAndForthInCaret, + testRemoveFromTwoOthersFromBowlInCaret + + | testEmptySetCases + -> testAddEmptyBaseIntersectOne, + testAddEmptyBaseIntersectTwo, + testAddEmptyBaseDifferenceOne, + testAddEmptyBaseDifferenceTwo, + testAddEmptyTarget, + testAddEmptyTargetInCaretOne, + testAddEmptyTargetInCaretTwo, + testRemoveEmptyBaseIntersectOne, + testRemoveEmptyBaseIntersectTwo, + testRemoveEmptyBaseDifferenceOne, + testRemoveEmptyBaseDifferenceTwo, + testRemoveEmptyTargetInCaretOne, + testRemoveEmptyTargetInCaretTwo + + + // Add with union combinations + | testAddLeftUnionTwo > + A> (bowls \/ baskets) / apples + + | testAddLeftUnionThree > + A> (bowls \/ baskets \/ plates) / apples + + | testLeftUnionAddFour > + A> (bowls \/ baskets \/ plates \/ buckets) / apples + + | testLeftUnionAddThreeLeftNested > + A> ((bowls \/ baskets) \/ plates) / apples + + | testLeftUnionAddFourLeftNested > + A> (((bowls \/ baskets) \/ plates) \/ buckets) / apples + + | testLeftUnionAddThreeRightNested > + A> (bowls \/ (baskets \/ plates)) / apples + + | testLeftUnionAddFourRightNested > + A> (bowls \/ (baskets \/ (plates \/ buckets))) / apples + + | testRightUnionAddTwo > + A> bowls / (apples \/ oranges) + + | testRightUnionAddThree > + A> bowls / (apples \/ oranges \/ bananas) + + | testRightUnionAddFour > + A> bowls / (apples \/ oranges \/ bananas \/ durians) + + | testRightUnionAddThreeLeftNested > + A> bowls / ((apples \/ oranges) \/ bananas) + + | testRightUnionAddFourLeftNested > + A> bowls / (((apples \/ oranges) \/ bananas) \/ durians) + + | testDoubleUnionAddTwo > + A> (bowls \/ baskets) / (apples \/ oranges) + + | testDoubleUnionAddThree > + A> (bowls \/ baskets \/ plates) / (apples \/ oranges \/ bananas) + + | testDoubleUnionAddFour > + A> (bowls \/ baskets \/ plates \/ buckets) / (apples \/ oranges \/ bananas \/ durians) + + + + // Link add with union combinations + | testLinkLeftUnionTwo > + A> (bowls \/ baskets) / ~plates.apples.container + + | testLinkLeftUnionThree > + A> (bowls \/ baskets \/ plates) / ~buckets.apples.container + + | testLinkLeftUnionFour > + A> (bowls \/ baskets \/ plates \/ buckets) / ~bowls.apples.container + + | testLinkLeftUnionThreeLeftNested > + A> ((bowls \/ baskets) \/ plates) / ~buckets.apples.container + + | testLinkLeftUnionFourLeftNested > + A> (((bowls \/ baskets) \/ plates) \/ buckets) / ~bowls.apples.container + + | testLinkRightUnionTwo > + A> bowls / ~((bowls \/ baskets).apples).container + + | testLinkRightUnionThree > + A> bowls / ~((bowls \/ baskets \/ plates).apples).container + + | testLinkRightUnionFour > + A> bowls / ~((bowls \/ baskets \/ plates \/ buckets).apples).container + + | testLinkRightUnionThreeLeftNested > + A> bowls / ~(((bowls \/ baskets) \/ plates).apples).container + + | testLinkRightUnionFourLeftNested > + A> bowls / ~((((bowls \/ baskets) \/ plates) \/ buckets).apples).container + + | testLinkRightUnionThreeRightNested > + A> bowls / ~(bowls \/ (baskets \/ plates)).apples.container + + | testLinkRightUnionFourRightNested > + A> bowls / ~(bowls \/ (baskets \/ (plates \/ buckets))).apples.container + + | testLinkDoubleUnionTwo > + A> (bowls \/ baskets) / ~(bowls \/ baskets).apples.container + + | testLinkDoubleUnionThree > + A> (bowls \/ baskets \/ plates) / ~(bowls \/ baskets \/ plates).apples.container + + | testLinkDoubleUnionFour > + A> (bowls \/ baskets \/ plates \/ buckets) / ~(bowls \/ baskets \/ plates \/ buckets).apples.container + + + + // Remove with union combinations + | testLeftUnionRemoveTwo > + R> (bowls \/ baskets) / apples + + | testLeftUnionRemoveTwoSelf > + R> (bowls \/ baskets).apples / self + + | testLeftUnionRemoveThree > + R> (bowls \/ baskets \/ plates) / apples + + | testLeftUnionRemoveFour > + R> (bowls \/ baskets \/ plates \/ buckets) / apples + + | testLeftUnionRemoveThreeLeftNested > + R> ((bowls \/ baskets) \/ plates) / apples + + | testLeftUnionRemoveFourLeftNested > + R> (((bowls \/ baskets) \/ plates) \/ buckets) / apples + + | testLeftUnionRemoveThreeRightNested > + R> (bowls \/ (baskets \/ plates)) / apples + + | testLeftUnionRemoveFourRightNested > + R> (bowls \/ (baskets \/ (plates \/ buckets))) / apples + + | testRightUnionRemoveTwo > + R> bowls / (apples \/ oranges) + + | testRightUnionRemoveThree > + R> bowls / (apples \/ oranges \/ bananas) + + | testRightUnionRemoveFour > + R> bowls / (apples \/ oranges \/ bananas \/ durians) + + | testRightUnionRemoveThreeLeftNested > + R> bowls / ((apples \/ oranges) \/ bananas) + + | testRightUnionRemoveFourLeftNested > + R> bowls / (((apples \/ oranges) \/ bananas) \/ durians) + + | testDoubleUnionRemoveTwo > + R> (bowls \/ baskets) / (apples \/ oranges) + + | testDoubleUnionRemoveThree > + R> (bowls \/ baskets \/ plates) / (apples \/ oranges \/ bananas) + + | testDoubleUnionRemoveFour > + R> (bowls \/ baskets \/ plates \/ buckets) / (apples \/ oranges \/ bananas \/ durians) + + + + // Link remove with union combinations + | testUnlinkLeftUnionRemoveTwo > + R> (bowls \/ baskets) / ~apples + + | testUnlinkLeftUnionRemoveThree > + R> (bowls \/ baskets \/ plates) / ~apples + + | testUnlinkLeftUnionRemoveFour > + R> (bowls \/ baskets \/ plates \/ buckets) / ~apples + + | testUnlinkLeftUnionRemoveThreeLeftNested > + R> ((bowls \/ baskets) \/ plates) / ~apples + + | testUnlinkLeftUnionRemoveFourLeftNested > + R> (((bowls \/ baskets) \/ plates) \/ buckets) / ~apples + + | testUnlinkLeftUnionRemoveThreeRightNested > + R> (bowls \/ (baskets \/ plates)) / ~apples + + | testUnlinkLeftUnionRemoveFourRightNested > + R> (bowls \/ (baskets \/ (plates \/ buckets))) / ~apples + + | testUnlinkRightUnionRemoveTwo > + R> bowls / ~(apples \/ oranges) + + | testUnlinkRightUnionRemoveThree > + R> bowls / ~(apples \/ oranges \/ bananas) + + | testUnlinkRightUnionRemoveFour > + R> bowls / ~(apples \/ oranges \/ bananas \/ durians) + + | testUnlinkRightUnionRemoveThreeLeftNested > + R> bowls / ~((apples \/ oranges) \/ bananas) + + | testUnlinkRightUnionRemoveFourLeftNested > + R> bowls / ~(((apples \/ oranges) \/ bananas) \/ durians) + + | testUnlinkDoubleUnionRemoveTwo > + R> (bowls \/ baskets) / ~(apples \/ oranges) + + | testUnlinkDoubleUnionRemoveThree > + R> (bowls \/ baskets \/ plates) / ~(apples \/ oranges \/ bananas) + + | testUnlinkDoubleUnionRemoveFour > + R> (bowls \/ baskets \/ plates \/ buckets) / ~(apples \/ oranges \/ bananas \/ durians) + + + + // Tests with caret + | testAddLeftUnionAddWithCaret > + A> bowls / (apples \/ oranges) ^ bananas + + | testAddRightUnionAddWithCaret > + A> bowls / apples ^ (oranges \/ bananas) + + | testAddDoubleUnionWithCaret > + A> bowls / (apples \/ oranges) ^ (bananas \/ durians) + + | testAddDoubleUnionNestedWithCaret > + A> bowls / ((apples \/ oranges) \/ bananas) ^ (oranges \/ (bananas \/ durians)) + + | testAddWithBackAndForthInCaret > + A> bowls / table.bowls.apples ^ table.bowls.oranges + + | testAddToTwoOthersFromBowlInCaret > + A> bowls / (table.baskets \/ table.plates).apples ^ (table.baskets \/ table.plates).bananas + + | testRemoveLeftUnionRemoveWithCaret > + R> bowls / (apples \/ oranges) ^ bananas + + | testRemoveRightUnionAddWithCaret > + R> bowls / apples ^ (oranges \/ bananas) + + | testRemoveDoubleUnionWithCaret > + R> bowls / (apples \/ oranges) ^ (bananas \/ durians) + + | testRemoveDoubleUnionNestedWithCaret > + R> bowls / ((apples \/ oranges) \/ bananas) ^ (oranges \/ (bananas \/ durians)) + + | testRemoveWithBackAndForthInCaret > + R> bowls / table.bowls.apples ^ table.bowls.oranges + + | testRemoveFromTwoOthersFromBowlInCaret > + R> bowls / (table.baskets \/ table.plates).apples ^ (table.baskets \/ table.plates).bananas + + + // Empty set cases + | testAddEmptyBaseIntersectOne > + A> (bowls /\ baskets) / apples + + | testAddEmptyBaseIntersectTwo > + A> (bowls \/ baskets) /\ (plates \/ buckets) / apples + + | testAddEmptyBaseDifferenceOne > + A> (bowls - bowls) / apples + + | testAddEmptyBaseDifferenceTwo > + A> (bowls \/ baskets) - (bowls \/ baskets) / apples + + | testAddEmptyTarget > + A> bowls / (apples /\ oranges) + + | testAddEmptyTargetInCaretOne > + A> bowls / (apples /\ oranges) ^ apples + + | testAddEmptyTargetInCaretTwo > + A> bowls / apples ^ (apples /\ oranges) + + + | testRemoveEmptyBaseIntersectOne > + R> (bowls /\ baskets) / apples + + | testRemoveEmptyBaseIntersectTwo > + R> (bowls \/ baskets) /\ (plates \/ buckets) / apples + + | testRemoveEmptyBaseDifferenceOne > + R> (bowls - bowls) / apples + + | testRemoveEmptyBaseDifferenceTwo > + R> (bowls \/ baskets) - (bowls \/ baskets) / apples + + | testRemoveEmptyTargetInCaretOne > + R> bowls / (apples /\ oranges) ^ apples + + | testRemoveEmptyTargetInCaretTwo > + R> bowls / apples ^ (apples /\ oranges) + } + + abstract asset Container {} + + abstract asset Fruit {} + + asset Bowl extends Container {} + + asset Basket extends Container {} + + asset Plate extends Container {} + + asset Bucket extends Container {} + + asset Apple extends Fruit {} + + asset Orange extends Fruit {} + + asset Banana extends Fruit {} + + asset Durian extends Fruit {} + + +} + +associations{ + Table [table] 1 <-- BowlPlacement --> * [bowls] Bowl + Table [table] 1 <-- BasketPlacement --> * [baskets] Basket + Table [table] 1 <-- PlatePlacement --> * [plates] Plate + Table [table] 1 <-- BucketPlacement --> * [buckets] Bucket + Container [container] 1 <-- AppleContain --> * [apples] Apple + Container [container] 1 <-- OrangeContain --> * [oranges] Orange + Container [container] 1 <-- BananaContain --> * [bananas] Banana + Container [container] 1 <-- DurianContain --> * [durians] Durian +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal new file mode 100644 index 00000000..2b7791e1 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal @@ -0,0 +1,263 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang13" +#version: "0.1.0" +category intDynamicTestLang13 { + asset Directory { + // Use at own risk + | testAll + -> moveTopToBottom + -> moveTopToNothing + -> moveTopToAll + -> addTopToBottomLeft + -> removeTopToBottomLeft + -> addTopToBottomRight + -> removeTopToBottomRight + -> moveBackAndForth + -> moveMiddleToBottom + -> moveTopThenBottom + -> moveMiddleToAll + -> moveMiddleToAllAlternate + -> addMiddleToTopLeft + -> addMiddleToBottomLeft + -> addMiddleToTopRight + -> addMiddleToBottomRight + -> removeMiddleToTopLeft + -> removeMiddleToBottomLeft + -> removeMiddleToTopRight + -> removeMiddleToBottomRight + -> addMiddleToAll + -> removeMiddleToAll + -> moveBottomToTop + -> moveTopThenBottom + -> moveBottomToAll + -> moveBottomToNothing + -> addBottomToNothing + -> addBottomToTopLeft + -> addBottomToTopRight + -> addBottomToTopRight + -> addBottomToAll + -> removeBottomToNothingLeft + -> removeBottomToNothingRight + -> removeBottomToTopLeft + -> removeBottomToTopRight + -> removeBottomToAll + + + // Use on top directories + | testTop + -> moveTopToBottom + -> moveTopToNothing + -> moveTopToAll + -> addTopToBottomLeft + -> removeTopToBottomLeft + -> addTopToBottomRight + -> removeTopToBottomRight + -> moveBackAndForth + + + // Use on middle directories + | testMiddle + -> addMiddleToTopLeft + -> addMiddleToBottomLeft + -> addMiddleToTopRight + -> addMiddleToBottomRight + -> removeMiddleToTopLeft + -> removeMiddleToBottomLeft + -> removeMiddleToTopRight + -> removeMiddleToBottomRight + -> addMiddleToAll + -> removeMiddleToAll + + + // Use on bottom directories + | testBottom + -> moveBottomToTop + -> moveTopThenBottom + -> moveBottomToAll + -> moveBottomToNothing + -> addBottomToNothing + -> addBottomToTopLeft + -> addBottomToTopRight + -> addBottomToTopRight + -> addBottomToAll + -> removeBottomToNothingLeft + -> removeBottomToNothingRight + -> removeBottomToTopLeft + -> removeBottomToTopRight + -> removeBottomToAll + + + + //********************************************************** + + // Tests from the top + + // {middle, bottom} -> reached + | moveTopToBottom + -> *sub.files.reached + + // Empty set + | moveTopToNothing + -> *super.files.reached + + // Joins {Top} and {middle, bottom} + | moveTopToAll + -> (files \/ *sub.files).reached + + // Add from {middle, bottom}, LH + | addTopToBottomLeft + A> *sub / files + + // Remove from {middle, bottom}, LH + | removeTopToBottomLeft + R> *sub.files / self + + // Add to {middle, bottom}, RH + | addTopToBottomRight + A> self / *sub.files + + // Remove from {middle, bottom}, RH + | removeTopToBottomRight + R> self / *sub.files + + // {middle, bottom} -> {middle, top} -> {middle, bottom} -> reached + | moveBackAndForth + -> *sub*super*sub.reached + + + // Tests from the middle + + // {bottom} -> reached + | moveMiddleToBottom + -> *sub.files.reached + + // {top} -> {middle, bottom} -> reached + | moveTopThenBottom + -> super*sub.files.reached + + // Joins {middle}, {top}, and {bottom} + | moveMiddleToAll + -> (files \/ *super.files \/ *sub.files).reached + + // Joins {top} and {middle, bottom} + | moveMiddleToAllAlternate + -> (*super \/ *super*sub).files.reached + + // {top} + | addMiddleToTopLeft + A> *super / files + + // {bottom} + | addMiddleToBottomLeft + A> *sub / files + + // {top} + | addMiddleToTopRight + A> self / *super.files + + // {bottom} + | addMiddleToBottomRight + A> self / *super.files + + // {top} + | removeMiddleToTopLeft + R> *super.files / self + + // {bottom} + | removeMiddleToBottomLeft + R> *sub.files / self + + // {top} + | removeMiddleToTopRight + R> self / *super.files + + // {top} + | removeMiddleToBottomRight + R> self / *sub.files + + // {top} and {middle, bottom} + | addMiddleToAll + A> (*super \/ *super*sub) / files + + // {top} and {middle, bottom} + | removeMiddleToAll + R> (*super \/ *super*sub) / files + + + + // Test from the bottom + + // {middle, top} -> reached + | moveBottomToTop + -> *super.files.reached + + // {middle, top} -> {middle, bottom} + | moveTopThenBottom + -> *super*sub.files.reached + + // Joins {bottom}, {middle, top}, and {middle, bottom} + | moveBottomToAll + -> (files \/ *super.files \/ *super*sub.files).reached + + // Empty set + | moveBottomToNothing + -> *sub.files.reached + + // Empty set + | addBottomToNothing + A> *sub / files + + // {middle, top} + | addBottomToTopLeft + A> *super / files + + // {middle, top} + | addBottomToTopRight + A> self / *super.files + + // {middle, top} + | addBottomToTopRight + A> self / *super.files + + // {middle, top} and {middle, bottom} + | addBottomToAll + A> (*super \/ *super*sub) / files + + // Empty set + | removeBottomToNothingLeft + R> *sub.files / self + + // Empty set + | removeBottomToNothingRight + R> self / *sub.files + + // {middle, top} + | removeBottomToTopLeft + R> *super.files / self + + // {middle, top} + | removeBottomToTopRight + R> self / *super.files + + // {middle, top} and {middle, bottom} + | removeBottomToAll + R> (*super \/ *super*sub) / files + } + + asset File { + | reached + + } + + +} + +associations { + Directory [super] 1 <-- nesting --> * [sub] Directory + Directory [dirs] * <-- contain --> * [files] File +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal new file mode 100644 index 00000000..0175434e --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal @@ -0,0 +1,36 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang2" +#version: "0.0.1" + +category Business { + asset AccessPolicy {} + + asset Banana {} + + asset BananaStorage { + | access > + -> pilfer + | pilfer > + R> self / goods + } + + asset Monkey { + let allowedAccess = (storage /\ allowPolicies.facilities) + | attemptTheft > + -> allowedAccess().access + | rewritePolicy > + A> self / ~storage.policy.allowedEmployees + } +} + +associations { + Monkey [agent] * <-- Access --> * [storage] BananaStorage + BananaStorage [facilities] * <-- Govern --> 1 [policy] AccessPolicy + AccessPolicy [allowPolicies] * <-- Control --> * [allowedEmployees] Monkey + BananaStorage [container] 1 <-- Contain --> * [goods] Banana +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal new file mode 100644 index 00000000..49cd44d7 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal @@ -0,0 +1,36 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang3" +#version: "0.0.1" + +category Business { + asset Alarm {} + asset Poison {} + + asset BananaTree { + | access + -> contaminateBananas + + | contaminateBananas > + user info: "Deactivate alarms, then poison reachable bananas." + developer info: "Order of operations!" + R> self / ~alarm + A> bananas / poison + -> bananas.poison.banana.poisoned + } + + asset Banana { + | poisoned + user info: "Flag this banana as poisoned!" + } +} + +associations { + Alarm [alarm] 1 <-- protects --> * [trees] BananaTree + BananaTree [tree] 1 <-- contains --> * [bananas] Banana + Banana [banana] 1 <-- contamination --> 0..1 [poison] Poison +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal new file mode 100644 index 00000000..78be39b4 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal @@ -0,0 +1,51 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang4" +#version: "0.0.1" + +category Business { + asset Apples { + | access + -> plantBadApple, plantInfestedApple + + | plantBadApple > + A> self / badApple + -> badApple.spoil + + | plantInfestedApple > + A> self / badApple[InfestedApple] + -> badApple.spoil + + | spoil +} + + asset BadApple + user info: "One bad apple spoils the bunch!" + { + | spoil + -> apples.spoil + + } + + asset InfestedApple extends BadApple + user info: "Ew!" + { + | spoil + +A> apples / pest + } + + asset Worm + user info: "Wiggly." + { + } +} + +associations { + Apples [apples] 1 <-- spoiledBy --> 0..1 [badApple] BadApple + Apples [apple] 1 <-- infestedBy --> 0..1 [pest] Worm + +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal new file mode 100644 index 00000000..38b54322 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal @@ -0,0 +1,45 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang5" +#version: "0.1.0" +category intDynamicTestLang5 { + asset Business {} + + abstract asset BusinessResource {} + + abstract asset Perishable extends BusinessResource { + | consume > + R> self / self + } + + abstract asset NonPerishable extends BusinessResource {} + + asset Typewriter extends NonPerishable {} + asset Apple extends Perishable {} + asset Banana extends Perishable {} + + asset Monkey { + | eatPerishables > + -> business.goodies[Perishable].consume + + | eatApples > + -> business.goodies[Apple].consume + + | eatBananas > + -> business.goodies[Banana].consume + + | breakTypewriters > + R> business.goodies[Typewriter] / self + + } + +} + +associations { + Business [organization] 1 <-- trades --> * [goodies] BusinessResource + Business [business] 1 <-- employs --> * [monkeys] Monkey +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal new file mode 100644 index 00000000..947fb0b6 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal @@ -0,0 +1,30 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang6" +#version: "0.1.0" +category intDynamicTestLang6 { + asset Table { + | access + -> bowls.addApple, bowls.transferApple + } + asset Bowl { + | addApple > + A> self / apples + + | transferApple > + A> apples / ~table.bowls.apples + R> self / ~apples + } + + asset Apple {} + +} + +associations { + Bowl [bowls] * <-- placement --> 1 [table] Table + Bowl [container] * <-- contains --> * [apples] Apple +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal new file mode 100644 index 00000000..8c24e26f --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal @@ -0,0 +1,32 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang7" +#version: "0.1.0" +category intDynamicTestLang7 { + asset Table { + | access + -> bowls.appleToOrange, bowls.orangeToApple + } + asset Bowl { + | appleToOrange > + R> self / fruit[Apple] + A> self / fruit[Orange] + | orangeToApple > + R> self / fruit[Orange] + A> self / fruit[Apple] + } + + abstract asset Fruit {} + asset Apple extends Fruit {} + asset Orange extends Fruit {} + +} + +associations { + Bowl [bowls] * <-- placement --> 1 [table] Table + Bowl [container] * <-- acontains --> * [fruit] Fruit +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal new file mode 100644 index 00000000..4922a4b8 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal @@ -0,0 +1,25 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang8" +#version: "0.1.0" +category intDynamicTestLang8 { + asset Table { + | assembleFruitBowl > + A> self / bowls + A> bowls / fruit[Apple] ^ fruit[Orange] + } + asset Bowl {} + abstract asset Fruit {} + asset Apple extends Fruit {} + asset Orange extends Fruit {} + +} + +associations { + Bowl [bowls] * <-- placement --> 1 [table] Table + Bowl [container] * <-- contains --> * [fruit] Fruit +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal new file mode 100644 index 00000000..abaf7b2d --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal @@ -0,0 +1,26 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang9" +#version: "0.1.0" +category intDynamicTestLang9 { + asset Room { + | addChairs > + A> self / chairs + | installTable > + A> self / tables + A> tables / ~chairs.table + } + asset Table {} + asset Chair {} + +} + +associations { + Room [room] 1 <-- tables --> 0..* [tables] Table + Room [room] 1 <-- chairs --> 0..* [chairs] Chair + Table [table] 1 <-- arrange --> 0..* [chair] Chair +} From 8234799ff0da0b71638ec46a6f93a4ae04c6a678 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Fri, 24 Jul 2026 16:01:07 +0200 Subject: [PATCH 15/22] WIP DynaMAL test langs --- tests/language/test_compiler.py | 32 +++++++++++++++++-- .../basic/baseDynamicTestLang10.mal | 8 +---- tests/testdata/get_dynamal_test_langs.py | 11 +++++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/tests/language/test_compiler.py b/tests/language/test_compiler.py index 9d0a21e9..41d7af6e 100644 --- a/tests/language/test_compiler.py +++ b/tests/language/test_compiler.py @@ -1,10 +1,30 @@ from pathlib import Path from maltoolbox.language.compiler import MalCompiler +from maltoolbox.language.compiler.lang import PARSER from maltoolbox.language import LanguageGraph import pytest +def format_ast(lang_file: Path) -> str: + """Return a pretty-printed tree-sitter AST for a .mal file""" + tree = PARSER.parse(lang_file.read_bytes()) + + lines = [] + + def visit(node, depth): + prefix = " " * depth + if node.child_count == 0: + lines.append(f"{prefix}{node.type} {node.text!r}") + else: + lines.append(f"{prefix}{node.type}") + for child in node.children: + visit(child, depth + 1) + + visit(tree.root_node, 0) + return "\n".join(lines) + + def compile_lang(tmp_path, src: str): lang_file = tmp_path / "test.mal" lang_file.write_text(src) @@ -133,7 +153,9 @@ def test_compile_basic_dynamal_languages(): try: lang = LanguageGraph.from_mal_spec(lang_file) except Exception as e: - pytest.fail(f"Failed to compile {lang_file}: {e}") + pytest.fail( + f"Failed to compile {lang_file}: {e}\n\nAST:\n{format_ast(lang_file)}" + ) def test_compile_intermediate_dynamal_languages(): dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/intermediate") @@ -141,7 +163,9 @@ def test_compile_intermediate_dynamal_languages(): try: lang = LanguageGraph.from_mal_spec(lang_file) except Exception as e: - pytest.fail(f"Failed to compile {lang_file}: {e}") + pytest.fail( + f"Failed to compile {lang_file}: {e}\n\nAST:\n{format_ast(lang_file)}" + ) def test_compile_advanced_dynamal_languages(): dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/advanced") @@ -149,4 +173,6 @@ def test_compile_advanced_dynamal_languages(): try: lang = LanguageGraph.from_mal_spec(lang_file) except Exception as e: - pytest.fail(f"Failed to compile {lang_file}: {e}") \ No newline at end of file + pytest.fail( + f"Failed to compile {lang_file}: {e}\n\nAST:\n{format_ast(lang_file)}" + ) \ No newline at end of file diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal index d5d967fe..15e33179 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal @@ -1,9 +1,3 @@ -/** - * Created by Viktor Engström - * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git - * Converted by Sandor Berglund - */ - #id: "baseDynamicTestLang10" #version: "0.0.1" @@ -13,7 +7,7 @@ category Business { | access -> pilferBananas - | pilferBananas > + | pilferBananas developer info: "Burn bridge, eat bananas." R> trees / ~plant ^ bananas diff --git a/tests/testdata/get_dynamal_test_langs.py b/tests/testdata/get_dynamal_test_langs.py index d529c856..7e01bf58 100644 --- a/tests/testdata/get_dynamal_test_langs.py +++ b/tests/testdata/get_dynamal_test_langs.py @@ -80,6 +80,16 @@ def fix_category(lang_path: Path): lang_path.write_text("".join(lines)) +asset_pattern = re.compile(r"asset\s*\w*\s*{(?P(\n|.)*?)}") +step_pattern = re.compile(r"([|&](\n|.)*?(?=[|&]|\Z))", flags=re.DOTALL) +def fix_double_reaching(lang_path: Path): + lang_text = lang_path.read_text() + + for asset_match in asset_pattern.finditer(lang_text): + asset_def = asset_match.group("asset_def") + for step_match in step_pattern.finditer(asset_def): + step_text = step_match.group(0) + def copy_test_langs(dir_name: str, repo_dir: Path) -> None: sub_dir = test_lang_dir / dir_name @@ -89,6 +99,7 @@ def copy_test_langs(dir_name: str, repo_dir: Path) -> None: shutil.copy(copy_path, lang_path) fix_version_id(lang_path) fix_category(lang_path) + fix_double_reaching(lang_path) add_credit_disclaimer(lang_path, copy_path.relative_to(repo_dir)) def main() -> None: From b135743a3d8d881aaa2dad8778239d0791c00da4 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Thu, 30 Jul 2026 06:15:03 +0200 Subject: [PATCH 16/22] WIP DynaMAL test langs --- .../dynamal_test_langs/basic/baseDynamicTestLang10.mal | 8 +++++++- .../dynamal_test_langs/basic/baseDynamicTestLang5.mal | 8 +------- .../dynamal_test_langs/basic/easyRansomwareLang.mal | 6 ------ tests/testdata/get_dynamal_test_langs.py | 5 +++++ 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal index 15e33179..d5d967fe 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal @@ -1,3 +1,9 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + #id: "baseDynamicTestLang10" #version: "0.0.1" @@ -7,7 +13,7 @@ category Business { | access -> pilferBananas - | pilferBananas + | pilferBananas > developer info: "Burn bridge, eat bananas." R> trees / ~plant ^ bananas diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal index dd2299cd..312b0025 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal @@ -1,9 +1,3 @@ -/** - * Created by Viktor Engström - * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git - * Converted by Sandor Berglund - */ - #id: "baseDynamicTestLang5" #version: "0.0.1" @@ -15,7 +9,7 @@ category Business { | access -> giveTypewriter - | giveTypewriter > + | giveTypewriter user info: "With enough monkeys and typewriters, we can create the complete works of Shakespeare." A> monkeys / ~typewriters.writer } diff --git a/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal b/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal index 3d9deddf..3424cde5 100644 --- a/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal +++ b/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal @@ -1,9 +1,3 @@ -/** - * Created by Viktor Engström - * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git - * Converted by Sandor Berglund - */ - #id: "easyRansomwareLang" #version: "0.1.0" category easyRansomwareLang { diff --git a/tests/testdata/get_dynamal_test_langs.py b/tests/testdata/get_dynamal_test_langs.py index 7e01bf58..73cf27cc 100644 --- a/tests/testdata/get_dynamal_test_langs.py +++ b/tests/testdata/get_dynamal_test_langs.py @@ -82,6 +82,7 @@ def fix_category(lang_path: Path): asset_pattern = re.compile(r"asset\s*\w*\s*{(?P(\n|.)*?)}") step_pattern = re.compile(r"([|&](\n|.)*?(?=[|&]|\Z))", flags=re.DOTALL) +dynamic_reaching_pattern = re.compile(r"(\+?[AR]>).*") def fix_double_reaching(lang_path: Path): lang_text = lang_path.read_text() @@ -89,6 +90,10 @@ def fix_double_reaching(lang_path: Path): asset_def = asset_match.group("asset_def") for step_match in step_pattern.finditer(asset_def): step_text = step_match.group(0) + reaching_matches = dynamic_reaching_pattern.search(step_text) + if reaching_matches: + print(reaching_matches) + def copy_test_langs(dir_name: str, repo_dir: Path) -> None: From 9e04e4bf975af084b3c99319f47a395b2407899a Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Thu, 30 Jul 2026 06:40:05 +0200 Subject: [PATCH 17/22] Fix remove identical reaching clauses in test langs --- .../basic/baseDynamicTestLang5.mal | 8 ++++- .../basic/doorProblemLang.mal | 1 - .../basic/easyRansomwareLang.mal | 6 ++++ .../intermediate/intDynamicTestLang13.mal | 3 -- tests/testdata/get_dynamal_test_langs.py | 33 ++++++++++++------- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal index 312b0025..dd2299cd 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal @@ -1,3 +1,9 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + #id: "baseDynamicTestLang5" #version: "0.0.1" @@ -9,7 +15,7 @@ category Business { | access -> giveTypewriter - | giveTypewriter + | giveTypewriter > user info: "With enough monkeys and typewriters, we can create the complete works of Shakespeare." A> monkeys / ~typewriters.writer } diff --git a/tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal b/tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal index 64cc0e36..97a691fe 100644 --- a/tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal +++ b/tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal @@ -11,7 +11,6 @@ category doorProblemLang { | leave -> entrance.leave // Leave the room R> self / ~entrance // Remove link between two rooms, i.e., close the door. - -> entrance.leave // You cannot leave through closed doors now. # deathLasers -> leave // Prevent attackers in rooms. diff --git a/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal b/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal index 3424cde5..3d9deddf 100644 --- a/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal +++ b/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal @@ -1,3 +1,9 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + #id: "easyRansomwareLang" #version: "0.1.0" category easyRansomwareLang { diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal index 2b7791e1..b4aeed79 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal @@ -33,13 +33,11 @@ category intDynamicTestLang13 { -> addMiddleToAll -> removeMiddleToAll -> moveBottomToTop - -> moveTopThenBottom -> moveBottomToAll -> moveBottomToNothing -> addBottomToNothing -> addBottomToTopLeft -> addBottomToTopRight - -> addBottomToTopRight -> addBottomToAll -> removeBottomToNothingLeft -> removeBottomToNothingRight @@ -83,7 +81,6 @@ category intDynamicTestLang13 { -> addBottomToNothing -> addBottomToTopLeft -> addBottomToTopRight - -> addBottomToTopRight -> addBottomToAll -> removeBottomToNothingLeft -> removeBottomToNothingRight diff --git a/tests/testdata/get_dynamal_test_langs.py b/tests/testdata/get_dynamal_test_langs.py index 73cf27cc..12a63a78 100644 --- a/tests/testdata/get_dynamal_test_langs.py +++ b/tests/testdata/get_dynamal_test_langs.py @@ -80,20 +80,29 @@ def fix_category(lang_path: Path): lang_path.write_text("".join(lines)) -asset_pattern = re.compile(r"asset\s*\w*\s*{(?P(\n|.)*?)}") -step_pattern = re.compile(r"([|&](\n|.)*?(?=[|&]|\Z))", flags=re.DOTALL) -dynamic_reaching_pattern = re.compile(r"(\+?[AR]>).*") +step_start_pattern = re.compile(r"^\s*[|&#]") +reaches_pattern = re.compile(r"^\s*->\s*(.*)") def fix_double_reaching(lang_path: Path): - lang_text = lang_path.read_text() + """Drop repeated `-> target` lines within a single attack step, + keeping only the first occurrence of each target.""" + lines = lang_path.read_text().splitlines(keepends=True) + + seen_targets: set[str] = set() + kept_lines = [] + for line in lines: + if step_start_pattern.match(line): + seen_targets = set() + reaches_match = reaches_pattern.match(line) + if reaches_match: + target = re.sub(r"//.*", "", reaches_match.group(1)).strip().rstrip(",") + if target in seen_targets: + continue + seen_targets.add(target) + kept_lines.append(line) + + lang_path.write_text("".join(kept_lines)) + - for asset_match in asset_pattern.finditer(lang_text): - asset_def = asset_match.group("asset_def") - for step_match in step_pattern.finditer(asset_def): - step_text = step_match.group(0) - reaching_matches = dynamic_reaching_pattern.search(step_text) - if reaching_matches: - print(reaching_matches) - def copy_test_langs(dir_name: str, repo_dir: Path) -> None: From 980f1219e66207dcbeb2e678d86283d78cbf5b77 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Thu, 30 Jul 2026 06:47:31 +0200 Subject: [PATCH 18/22] Fix: Remove double dynamic sentence --- .../basic/easyRansomwareLang.mal | 4 +-- .../intermediate/intDynamicTestLang10.mal | 4 +-- .../intermediate/intDynamicTestLang8.mal | 4 +-- .../intermediate/intDynamicTestLang9.mal | 4 +-- tests/testdata/get_dynamal_test_langs.py | 33 +++++++++++++++++++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal b/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal index 3d9deddf..cccc3601 100644 --- a/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal +++ b/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal @@ -18,8 +18,8 @@ category easyRansomwareLang { asset Ransomware { | attack - A> host / data.locked - A> host / ~host.data.locked.victim + A> host / data.locked, + host / ~host.data.locked.victim R> host / data } diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal index 35c01b1a..d4b5c40d 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal @@ -9,8 +9,8 @@ category intDynamicTestLang10 { asset Table { | delegateFruit > - A> bowls / apples - A> bowls.apples / ~bowls.apples + A> bowls / apples, + bowls.apples / ~bowls.apples R> bowls / ~apples } asset Bowl {} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal index 4922a4b8..8886364e 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal @@ -9,8 +9,8 @@ category intDynamicTestLang8 { asset Table { | assembleFruitBowl > - A> self / bowls - A> bowls / fruit[Apple] ^ fruit[Orange] + A> self / bowls, + bowls / fruit[Apple] ^ fruit[Orange] } asset Bowl {} abstract asset Fruit {} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal index abaf7b2d..1ca55530 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal @@ -11,8 +11,8 @@ category intDynamicTestLang9 { | addChairs > A> self / chairs | installTable > - A> self / tables - A> tables / ~chairs.table + A> self / tables, + tables / ~chairs.table } asset Table {} asset Chair {} diff --git a/tests/testdata/get_dynamal_test_langs.py b/tests/testdata/get_dynamal_test_langs.py index 12a63a78..ea832337 100644 --- a/tests/testdata/get_dynamal_test_langs.py +++ b/tests/testdata/get_dynamal_test_langs.py @@ -103,6 +103,38 @@ def fix_double_reaching(lang_path: Path): lang_path.write_text("".join(kept_lines)) +dynamic_op_pattern = re.compile(r"^(\s*)(\+?[AR]>)\s*(.*)$") +def fix_double_dynamic_operations(lang_path: Path): + """Merge repeated `A>`/`R>`/`+A>`/`+R>` clauses within a single attack + step into one comma-separated clause, keeping only the first operator.""" + lines = lang_path.read_text().splitlines(keepends=True) + + last_line_idx: dict[str, int] = {} + kept_lines: list[str] = [] + for line in lines: + if step_start_pattern.match(line): + last_line_idx = {} + + eol = "\n" if line.endswith("\n") else "" + content = line[:-1] if eol else line + match = dynamic_op_pattern.match(content) + if match: + indent, op, rest = match.groups() + if op in last_line_idx: + prev_idx = last_line_idx[op] + prev_content = kept_lines[prev_idx].rstrip("\n") + if not prev_content.endswith(","): + prev_content += "," + kept_lines[prev_idx] = prev_content + "\n" + kept_lines.append(f"{indent}{rest}{eol}") + else: + kept_lines.append(line) + last_line_idx[op] = len(kept_lines) - 1 + continue + + kept_lines.append(line) + + lang_path.write_text("".join(kept_lines)) def copy_test_langs(dir_name: str, repo_dir: Path) -> None: @@ -114,6 +146,7 @@ def copy_test_langs(dir_name: str, repo_dir: Path) -> None: fix_version_id(lang_path) fix_category(lang_path) fix_double_reaching(lang_path) + fix_double_dynamic_operations(lang_path) add_credit_disclaimer(lang_path, copy_path.relative_to(repo_dir)) def main() -> None: From 314e245e88132f23f69253bf824cf9427fefcd09 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Thu, 30 Jul 2026 07:52:24 +0200 Subject: [PATCH 19/22] Fix: Remove stray trailing arrows from step declarations in test languages --- .../basic/baseDynamicTestLang1.mal | 2 +- .../basic/baseDynamicTestLang10.mal | 2 +- .../basic/baseDynamicTestLang2.mal | 2 +- .../basic/baseDynamicTestLang3.mal | 4 +- .../basic/baseDynamicTestLang4.mal | 2 +- .../basic/baseDynamicTestLang5.mal | 2 +- .../basic/baseDynamicTestLang6.mal | 2 +- .../basic/baseDynamicTestLang7.mal | 4 +- .../basic/baseDynamicTestLang8.mal | 4 +- .../basic/baseDynamicTestLang9.mal | 2 +- .../dynamal_test_langs/basic/glider.mal | 2 +- .../dynamal_test_langs/basic/looper.mal | 2 +- .../intermediate/intDynamicTestLang1.mal | 2 +- .../intermediate/intDynamicTestLang10.mal | 2 +- .../intermediate/intDynamicTestLang11.mal | 12 +- .../intermediate/intDynamicTestLang12.mal | 172 +++++++++--------- .../intermediate/intDynamicTestLang2.mal | 8 +- .../intermediate/intDynamicTestLang3.mal | 2 +- .../intermediate/intDynamicTestLang4.mal | 4 +- .../intermediate/intDynamicTestLang5.mal | 10 +- .../intermediate/intDynamicTestLang6.mal | 4 +- .../intermediate/intDynamicTestLang7.mal | 4 +- .../intermediate/intDynamicTestLang8.mal | 2 +- .../intermediate/intDynamicTestLang9.mal | 4 +- tests/testdata/get_dynamal_test_langs.py | 22 +++ 25 files changed, 150 insertions(+), 128 deletions(-) diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal index 24367ae4..6853af9c 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal @@ -11,7 +11,7 @@ category baseDynamicTestLang1 { | access -> addApple - | addApple > + | addApple A> self / apples } diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal index d5d967fe..93fd1165 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal @@ -13,7 +13,7 @@ category Business { | access -> pilferBananas - | pilferBananas > + | pilferBananas developer info: "Burn bridge, eat bananas." R> trees / ~plant ^ bananas diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal index b7c278d1..a9936804 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal @@ -11,7 +11,7 @@ category baseDynamicTestLang2 { | access -> eatApple - | eatApple > + | eatApple R> self / apples } diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal index 9127a3f8..fc832b40 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal @@ -11,10 +11,10 @@ category baseDynamicTestLang3 { | access -> addApple, removeApple - | addApple > + | addApple A> self / apples - | removeApple > + | removeApple R> self / apples } diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal index 7923aa1a..633e2d45 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal @@ -11,7 +11,7 @@ category baseDynamicTestLang4 { | access -> tamper - | tamper > + | tamper A> self / apples R> self / apples } diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal index dd2299cd..6268b30f 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal @@ -15,7 +15,7 @@ category Business { | access -> giveTypewriter - | giveTypewriter > + | giveTypewriter user info: "With enough monkeys and typewriters, we can create the complete works of Shakespeare." A> monkeys / ~typewriters.writer } diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal index dc91f69b..e2748c40 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal @@ -14,7 +14,7 @@ category Business { | access -> hideTypewriter - | hideTypewriter > + | hideTypewriter user info: "Monkeys without typewriters cannot work." developer info: "Only removes the association. The typewriter still exists in the model." R> self / ~writer diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal index e3b03987..03d6b068 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal @@ -13,14 +13,14 @@ category Business { | access -> plantTree - | plantTree > + | plantTree A> self / trees } asset MagicPlantation extends Plantation { - | plantTree > + | plantTree +A> trees / bananas } diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal index bb700eef..43281859 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal @@ -13,14 +13,14 @@ category Business { | access -> pilferBananas - | pilferBananas > + | pilferBananas R> trees / bananas } asset PlantationWithSmallTrees extends Plantation { - | pilferBananas > + | pilferBananas +R> trees / self } diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal index 09049e4d..042c7ca9 100644 --- a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal @@ -13,7 +13,7 @@ category Business { | access -> plantTree - | plantTree > + | plantTree A> self / trees ^ trees.bananas } diff --git a/tests/testdata/dynamal_test_langs/basic/glider.mal b/tests/testdata/dynamal_test_langs/basic/glider.mal index 2f084ae6..22e3758c 100644 --- a/tests/testdata/dynamal_test_langs/basic/glider.mal +++ b/tests/testdata/dynamal_test_langs/basic/glider.mal @@ -8,7 +8,7 @@ #version: "0.1.0" category glider { asset Glider { - | glide > + | glide A> self / next R> self / previous -> next.glide diff --git a/tests/testdata/dynamal_test_langs/basic/looper.mal b/tests/testdata/dynamal_test_langs/basic/looper.mal index ee59657b..1b8057c4 100644 --- a/tests/testdata/dynamal_test_langs/basic/looper.mal +++ b/tests/testdata/dynamal_test_langs/basic/looper.mal @@ -8,7 +8,7 @@ #version: "0.1.0" category looper { asset Looper { - | loop > + | loop A> self / next -> next.loop } diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal index 429926d1..b0436147 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal @@ -12,7 +12,7 @@ category Business { | access -> plantBadApple - | plantBadApple > + | plantBadApple A> self / badApple -> badApple.apples.spoil diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal index d4b5c40d..cad226a6 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal @@ -8,7 +8,7 @@ #version: "0.1.0" category intDynamicTestLang10 { asset Table { - | delegateFruit > + | delegateFruit A> bowls / apples, bowls.apples / ~bowls.apples R> bowls / ~apples diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal index 3d7f478c..5b665f49 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal @@ -16,22 +16,22 @@ category intDynamicTestLang11 { testRightUnionAdd, testDoubleUnionAdd - | testLeftUnionRemove > + | testLeftUnionRemove R> (bowls \/ baskets) / apples - | testRightUnionRemove > + | testRightUnionRemove R> bowls / (apples \/ oranges) - | testDoubleUnionRemove > + | testDoubleUnionRemove R> (bowls \/ baskets) / (apples \/ oranges) - | testLeftUnionAdd > + | testLeftUnionAdd A> (bowls \/ baskets) / apples - | testRightUnionAdd > + | testRightUnionAdd A> bowls / (apples \/ oranges) - | testDoubleUnionAdd > + | testDoubleUnionAdd A> (bowls \/ baskets) / (apples \/ oranges) } diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal index 5a728a37..55c2d37c 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal @@ -116,277 +116,277 @@ category intDynamicTestLang12 { // Add with union combinations - | testAddLeftUnionTwo > + | testAddLeftUnionTwo A> (bowls \/ baskets) / apples - | testAddLeftUnionThree > + | testAddLeftUnionThree A> (bowls \/ baskets \/ plates) / apples - | testLeftUnionAddFour > + | testLeftUnionAddFour A> (bowls \/ baskets \/ plates \/ buckets) / apples - | testLeftUnionAddThreeLeftNested > + | testLeftUnionAddThreeLeftNested A> ((bowls \/ baskets) \/ plates) / apples - | testLeftUnionAddFourLeftNested > + | testLeftUnionAddFourLeftNested A> (((bowls \/ baskets) \/ plates) \/ buckets) / apples - | testLeftUnionAddThreeRightNested > + | testLeftUnionAddThreeRightNested A> (bowls \/ (baskets \/ plates)) / apples - | testLeftUnionAddFourRightNested > + | testLeftUnionAddFourRightNested A> (bowls \/ (baskets \/ (plates \/ buckets))) / apples - | testRightUnionAddTwo > + | testRightUnionAddTwo A> bowls / (apples \/ oranges) - | testRightUnionAddThree > + | testRightUnionAddThree A> bowls / (apples \/ oranges \/ bananas) - | testRightUnionAddFour > + | testRightUnionAddFour A> bowls / (apples \/ oranges \/ bananas \/ durians) - | testRightUnionAddThreeLeftNested > + | testRightUnionAddThreeLeftNested A> bowls / ((apples \/ oranges) \/ bananas) - | testRightUnionAddFourLeftNested > + | testRightUnionAddFourLeftNested A> bowls / (((apples \/ oranges) \/ bananas) \/ durians) - | testDoubleUnionAddTwo > + | testDoubleUnionAddTwo A> (bowls \/ baskets) / (apples \/ oranges) - | testDoubleUnionAddThree > + | testDoubleUnionAddThree A> (bowls \/ baskets \/ plates) / (apples \/ oranges \/ bananas) - | testDoubleUnionAddFour > + | testDoubleUnionAddFour A> (bowls \/ baskets \/ plates \/ buckets) / (apples \/ oranges \/ bananas \/ durians) // Link add with union combinations - | testLinkLeftUnionTwo > + | testLinkLeftUnionTwo A> (bowls \/ baskets) / ~plates.apples.container - | testLinkLeftUnionThree > + | testLinkLeftUnionThree A> (bowls \/ baskets \/ plates) / ~buckets.apples.container - | testLinkLeftUnionFour > + | testLinkLeftUnionFour A> (bowls \/ baskets \/ plates \/ buckets) / ~bowls.apples.container - | testLinkLeftUnionThreeLeftNested > + | testLinkLeftUnionThreeLeftNested A> ((bowls \/ baskets) \/ plates) / ~buckets.apples.container - | testLinkLeftUnionFourLeftNested > + | testLinkLeftUnionFourLeftNested A> (((bowls \/ baskets) \/ plates) \/ buckets) / ~bowls.apples.container - | testLinkRightUnionTwo > + | testLinkRightUnionTwo A> bowls / ~((bowls \/ baskets).apples).container - | testLinkRightUnionThree > + | testLinkRightUnionThree A> bowls / ~((bowls \/ baskets \/ plates).apples).container - | testLinkRightUnionFour > + | testLinkRightUnionFour A> bowls / ~((bowls \/ baskets \/ plates \/ buckets).apples).container - | testLinkRightUnionThreeLeftNested > + | testLinkRightUnionThreeLeftNested A> bowls / ~(((bowls \/ baskets) \/ plates).apples).container - | testLinkRightUnionFourLeftNested > + | testLinkRightUnionFourLeftNested A> bowls / ~((((bowls \/ baskets) \/ plates) \/ buckets).apples).container - | testLinkRightUnionThreeRightNested > + | testLinkRightUnionThreeRightNested A> bowls / ~(bowls \/ (baskets \/ plates)).apples.container - | testLinkRightUnionFourRightNested > + | testLinkRightUnionFourRightNested A> bowls / ~(bowls \/ (baskets \/ (plates \/ buckets))).apples.container - | testLinkDoubleUnionTwo > + | testLinkDoubleUnionTwo A> (bowls \/ baskets) / ~(bowls \/ baskets).apples.container - | testLinkDoubleUnionThree > + | testLinkDoubleUnionThree A> (bowls \/ baskets \/ plates) / ~(bowls \/ baskets \/ plates).apples.container - | testLinkDoubleUnionFour > + | testLinkDoubleUnionFour A> (bowls \/ baskets \/ plates \/ buckets) / ~(bowls \/ baskets \/ plates \/ buckets).apples.container // Remove with union combinations - | testLeftUnionRemoveTwo > + | testLeftUnionRemoveTwo R> (bowls \/ baskets) / apples - | testLeftUnionRemoveTwoSelf > + | testLeftUnionRemoveTwoSelf R> (bowls \/ baskets).apples / self - | testLeftUnionRemoveThree > + | testLeftUnionRemoveThree R> (bowls \/ baskets \/ plates) / apples - | testLeftUnionRemoveFour > + | testLeftUnionRemoveFour R> (bowls \/ baskets \/ plates \/ buckets) / apples - | testLeftUnionRemoveThreeLeftNested > + | testLeftUnionRemoveThreeLeftNested R> ((bowls \/ baskets) \/ plates) / apples - | testLeftUnionRemoveFourLeftNested > + | testLeftUnionRemoveFourLeftNested R> (((bowls \/ baskets) \/ plates) \/ buckets) / apples - | testLeftUnionRemoveThreeRightNested > + | testLeftUnionRemoveThreeRightNested R> (bowls \/ (baskets \/ plates)) / apples - | testLeftUnionRemoveFourRightNested > + | testLeftUnionRemoveFourRightNested R> (bowls \/ (baskets \/ (plates \/ buckets))) / apples - | testRightUnionRemoveTwo > + | testRightUnionRemoveTwo R> bowls / (apples \/ oranges) - | testRightUnionRemoveThree > + | testRightUnionRemoveThree R> bowls / (apples \/ oranges \/ bananas) - | testRightUnionRemoveFour > + | testRightUnionRemoveFour R> bowls / (apples \/ oranges \/ bananas \/ durians) - | testRightUnionRemoveThreeLeftNested > + | testRightUnionRemoveThreeLeftNested R> bowls / ((apples \/ oranges) \/ bananas) - | testRightUnionRemoveFourLeftNested > + | testRightUnionRemoveFourLeftNested R> bowls / (((apples \/ oranges) \/ bananas) \/ durians) - | testDoubleUnionRemoveTwo > + | testDoubleUnionRemoveTwo R> (bowls \/ baskets) / (apples \/ oranges) - | testDoubleUnionRemoveThree > + | testDoubleUnionRemoveThree R> (bowls \/ baskets \/ plates) / (apples \/ oranges \/ bananas) - | testDoubleUnionRemoveFour > + | testDoubleUnionRemoveFour R> (bowls \/ baskets \/ plates \/ buckets) / (apples \/ oranges \/ bananas \/ durians) // Link remove with union combinations - | testUnlinkLeftUnionRemoveTwo > + | testUnlinkLeftUnionRemoveTwo R> (bowls \/ baskets) / ~apples - | testUnlinkLeftUnionRemoveThree > + | testUnlinkLeftUnionRemoveThree R> (bowls \/ baskets \/ plates) / ~apples - | testUnlinkLeftUnionRemoveFour > + | testUnlinkLeftUnionRemoveFour R> (bowls \/ baskets \/ plates \/ buckets) / ~apples - | testUnlinkLeftUnionRemoveThreeLeftNested > + | testUnlinkLeftUnionRemoveThreeLeftNested R> ((bowls \/ baskets) \/ plates) / ~apples - | testUnlinkLeftUnionRemoveFourLeftNested > + | testUnlinkLeftUnionRemoveFourLeftNested R> (((bowls \/ baskets) \/ plates) \/ buckets) / ~apples - | testUnlinkLeftUnionRemoveThreeRightNested > + | testUnlinkLeftUnionRemoveThreeRightNested R> (bowls \/ (baskets \/ plates)) / ~apples - | testUnlinkLeftUnionRemoveFourRightNested > + | testUnlinkLeftUnionRemoveFourRightNested R> (bowls \/ (baskets \/ (plates \/ buckets))) / ~apples - | testUnlinkRightUnionRemoveTwo > + | testUnlinkRightUnionRemoveTwo R> bowls / ~(apples \/ oranges) - | testUnlinkRightUnionRemoveThree > + | testUnlinkRightUnionRemoveThree R> bowls / ~(apples \/ oranges \/ bananas) - | testUnlinkRightUnionRemoveFour > + | testUnlinkRightUnionRemoveFour R> bowls / ~(apples \/ oranges \/ bananas \/ durians) - | testUnlinkRightUnionRemoveThreeLeftNested > + | testUnlinkRightUnionRemoveThreeLeftNested R> bowls / ~((apples \/ oranges) \/ bananas) - | testUnlinkRightUnionRemoveFourLeftNested > + | testUnlinkRightUnionRemoveFourLeftNested R> bowls / ~(((apples \/ oranges) \/ bananas) \/ durians) - | testUnlinkDoubleUnionRemoveTwo > + | testUnlinkDoubleUnionRemoveTwo R> (bowls \/ baskets) / ~(apples \/ oranges) - | testUnlinkDoubleUnionRemoveThree > + | testUnlinkDoubleUnionRemoveThree R> (bowls \/ baskets \/ plates) / ~(apples \/ oranges \/ bananas) - | testUnlinkDoubleUnionRemoveFour > + | testUnlinkDoubleUnionRemoveFour R> (bowls \/ baskets \/ plates \/ buckets) / ~(apples \/ oranges \/ bananas \/ durians) // Tests with caret - | testAddLeftUnionAddWithCaret > + | testAddLeftUnionAddWithCaret A> bowls / (apples \/ oranges) ^ bananas - | testAddRightUnionAddWithCaret > + | testAddRightUnionAddWithCaret A> bowls / apples ^ (oranges \/ bananas) - | testAddDoubleUnionWithCaret > + | testAddDoubleUnionWithCaret A> bowls / (apples \/ oranges) ^ (bananas \/ durians) - | testAddDoubleUnionNestedWithCaret > + | testAddDoubleUnionNestedWithCaret A> bowls / ((apples \/ oranges) \/ bananas) ^ (oranges \/ (bananas \/ durians)) - | testAddWithBackAndForthInCaret > + | testAddWithBackAndForthInCaret A> bowls / table.bowls.apples ^ table.bowls.oranges - | testAddToTwoOthersFromBowlInCaret > + | testAddToTwoOthersFromBowlInCaret A> bowls / (table.baskets \/ table.plates).apples ^ (table.baskets \/ table.plates).bananas - | testRemoveLeftUnionRemoveWithCaret > + | testRemoveLeftUnionRemoveWithCaret R> bowls / (apples \/ oranges) ^ bananas - | testRemoveRightUnionAddWithCaret > + | testRemoveRightUnionAddWithCaret R> bowls / apples ^ (oranges \/ bananas) - | testRemoveDoubleUnionWithCaret > + | testRemoveDoubleUnionWithCaret R> bowls / (apples \/ oranges) ^ (bananas \/ durians) - | testRemoveDoubleUnionNestedWithCaret > + | testRemoveDoubleUnionNestedWithCaret R> bowls / ((apples \/ oranges) \/ bananas) ^ (oranges \/ (bananas \/ durians)) - | testRemoveWithBackAndForthInCaret > + | testRemoveWithBackAndForthInCaret R> bowls / table.bowls.apples ^ table.bowls.oranges - | testRemoveFromTwoOthersFromBowlInCaret > + | testRemoveFromTwoOthersFromBowlInCaret R> bowls / (table.baskets \/ table.plates).apples ^ (table.baskets \/ table.plates).bananas // Empty set cases - | testAddEmptyBaseIntersectOne > + | testAddEmptyBaseIntersectOne A> (bowls /\ baskets) / apples - | testAddEmptyBaseIntersectTwo > + | testAddEmptyBaseIntersectTwo A> (bowls \/ baskets) /\ (plates \/ buckets) / apples - | testAddEmptyBaseDifferenceOne > + | testAddEmptyBaseDifferenceOne A> (bowls - bowls) / apples - | testAddEmptyBaseDifferenceTwo > + | testAddEmptyBaseDifferenceTwo A> (bowls \/ baskets) - (bowls \/ baskets) / apples - | testAddEmptyTarget > + | testAddEmptyTarget A> bowls / (apples /\ oranges) - | testAddEmptyTargetInCaretOne > + | testAddEmptyTargetInCaretOne A> bowls / (apples /\ oranges) ^ apples - | testAddEmptyTargetInCaretTwo > + | testAddEmptyTargetInCaretTwo A> bowls / apples ^ (apples /\ oranges) - | testRemoveEmptyBaseIntersectOne > + | testRemoveEmptyBaseIntersectOne R> (bowls /\ baskets) / apples - | testRemoveEmptyBaseIntersectTwo > + | testRemoveEmptyBaseIntersectTwo R> (bowls \/ baskets) /\ (plates \/ buckets) / apples - | testRemoveEmptyBaseDifferenceOne > + | testRemoveEmptyBaseDifferenceOne R> (bowls - bowls) / apples - | testRemoveEmptyBaseDifferenceTwo > + | testRemoveEmptyBaseDifferenceTwo R> (bowls \/ baskets) - (bowls \/ baskets) / apples - | testRemoveEmptyTargetInCaretOne > + | testRemoveEmptyTargetInCaretOne R> bowls / (apples /\ oranges) ^ apples - | testRemoveEmptyTargetInCaretTwo > + | testRemoveEmptyTargetInCaretTwo R> bowls / apples ^ (apples /\ oranges) } diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal index 0175434e..1a5c6d0a 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal @@ -13,17 +13,17 @@ category Business { asset Banana {} asset BananaStorage { - | access > + | access -> pilfer - | pilfer > + | pilfer R> self / goods } asset Monkey { let allowedAccess = (storage /\ allowPolicies.facilities) - | attemptTheft > + | attemptTheft -> allowedAccess().access - | rewritePolicy > + | rewritePolicy A> self / ~storage.policy.allowedEmployees } } diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal index 49cd44d7..49a8ce24 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal @@ -15,7 +15,7 @@ category Business { | access -> contaminateBananas - | contaminateBananas > + | contaminateBananas user info: "Deactivate alarms, then poison reachable bananas." developer info: "Order of operations!" R> self / ~alarm diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal index 78be39b4..6bda4089 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal @@ -12,11 +12,11 @@ category Business { | access -> plantBadApple, plantInfestedApple - | plantBadApple > + | plantBadApple A> self / badApple -> badApple.spoil - | plantInfestedApple > + | plantInfestedApple A> self / badApple[InfestedApple] -> badApple.spoil diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal index 38b54322..c57b25a8 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal @@ -12,7 +12,7 @@ category intDynamicTestLang5 { abstract asset BusinessResource {} abstract asset Perishable extends BusinessResource { - | consume > + | consume R> self / self } @@ -23,16 +23,16 @@ category intDynamicTestLang5 { asset Banana extends Perishable {} asset Monkey { - | eatPerishables > + | eatPerishables -> business.goodies[Perishable].consume - | eatApples > + | eatApples -> business.goodies[Apple].consume - | eatBananas > + | eatBananas -> business.goodies[Banana].consume - | breakTypewriters > + | breakTypewriters R> business.goodies[Typewriter] / self } diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal index 947fb0b6..84335dd3 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal @@ -12,10 +12,10 @@ category intDynamicTestLang6 { -> bowls.addApple, bowls.transferApple } asset Bowl { - | addApple > + | addApple A> self / apples - | transferApple > + | transferApple A> apples / ~table.bowls.apples R> self / ~apples } diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal index 8c24e26f..999921bc 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal @@ -12,10 +12,10 @@ category intDynamicTestLang7 { -> bowls.appleToOrange, bowls.orangeToApple } asset Bowl { - | appleToOrange > + | appleToOrange R> self / fruit[Apple] A> self / fruit[Orange] - | orangeToApple > + | orangeToApple R> self / fruit[Orange] A> self / fruit[Apple] } diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal index 8886364e..24cdbd9e 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal @@ -8,7 +8,7 @@ #version: "0.1.0" category intDynamicTestLang8 { asset Table { - | assembleFruitBowl > + | assembleFruitBowl A> self / bowls, bowls / fruit[Apple] ^ fruit[Orange] } diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal index 1ca55530..79893e33 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal @@ -8,9 +8,9 @@ #version: "0.1.0" category intDynamicTestLang9 { asset Room { - | addChairs > + | addChairs A> self / chairs - | installTable > + | installTable A> self / tables, tables / ~chairs.table } diff --git a/tests/testdata/get_dynamal_test_langs.py b/tests/testdata/get_dynamal_test_langs.py index ea832337..346e6894 100644 --- a/tests/testdata/get_dynamal_test_langs.py +++ b/tests/testdata/get_dynamal_test_langs.py @@ -81,6 +81,27 @@ def fix_category(lang_path: Path): lang_path.write_text("".join(lines)) step_start_pattern = re.compile(r"^\s*[|&#]") +stray_step_arrow_pattern = re.compile( + r"^(\s*[|&#]\s*[A-Za-z_][A-Za-z0-9_]*)\s*>(\s*(//.*)?)$" +) +def fix_stray_step_arrow(lang_path: Path): + """Drop the bare trailing `>` some step declarations have (e.g. + `| plantTree >`) — it isn't part of the current attack_step grammar + and causes a syntax error.""" + lines = lang_path.read_text().splitlines(keepends=True) + + fixed_lines = [] + for line in lines: + eol = "\n" if line.endswith("\n") else "" + content = line[:-1] if eol else line + match = stray_step_arrow_pattern.match(content) + if match: + line = f"{match.group(1)}{match.group(2)}{eol}" + fixed_lines.append(line) + + lang_path.write_text("".join(fixed_lines)) + + reaches_pattern = re.compile(r"^\s*->\s*(.*)") def fix_double_reaching(lang_path: Path): """Drop repeated `-> target` lines within a single attack step, @@ -147,6 +168,7 @@ def copy_test_langs(dir_name: str, repo_dir: Path) -> None: fix_category(lang_path) fix_double_reaching(lang_path) fix_double_dynamic_operations(lang_path) + fix_stray_step_arrow(lang_path) add_credit_disclaimer(lang_path, copy_path.relative_to(repo_dir)) def main() -> None: From 12aaaddef3de731334d4e2c98fa6401e97279585 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Thu, 30 Jul 2026 08:05:00 +0200 Subject: [PATCH 20/22] Add option for dyn targets to reference self --- .../language/language_graph_model_effect.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/maltoolbox/language/language_graph_model_effect.py b/maltoolbox/language/language_graph_model_effect.py index 1f04ad9c..33ea24c9 100644 --- a/maltoolbox/language/language_graph_model_effect.py +++ b/maltoolbox/language/language_graph_model_effect.py @@ -2,6 +2,7 @@ - Represents a step (type) defined in a MAL language """ from __future__ import annotations +from copy import deepcopy from typing import TYPE_CHECKING, NamedTuple, Optional from dataclasses import dataclass @@ -18,6 +19,8 @@ class AssocTraversal(NamedTuple): asset_filter: Optional[LanguageGraphAsset] quantity_filter: Optional[int] +SELF_TRAVERSAL = AssocTraversal(field_name="self", asset_filter=None, quantity_filter=None) + class DynTarget(NamedTuple): """A target in a dynamic sentence""" assoc_op: bool # Decides if we are adding/removing assets or connections to assets @@ -83,13 +86,16 @@ def build_assoc_traversal(expr_chain: ExpressionsChain | None) -> list[AssocTrav except LookupError: _, target_expr_chain, target_step = process_function(assets, base_target_asset, None, target_expr, lang_spec) assert not target_step, "Targets can not refer to an attack step in a dynamic sentence" - assert target_expr_chain, "Target expression in dynamic sentence does not resolve to an expression chain" - if target_expr_chain.type == ExprType.ASSOC_OP: - assoc_op = True - target_expr_chain = target_expr_chain.sub_link + if target_expr_chain: + if target_expr_chain.type == ExprType.ASSOC_OP: + assoc_op = True + target_expr_chain = target_expr_chain.sub_link + else: + assoc_op = False + assoc_traversal = build_assoc_traversal(target_expr_chain) else: assoc_op = False - assoc_traversal = build_assoc_traversal(target_expr_chain) + assoc_traversal = [deepcopy(SELF_TRAVERSAL)] targets.append(DynTarget(assoc_op=assoc_op, assoc_traversal=assoc_traversal)) return LanguageGraphModelEffect(base=base, targets=targets) \ No newline at end of file From e1a1152f2055a54ebc3e51e7cf1aec1e38bc1ff8 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Thu, 30 Jul 2026 08:22:56 +0200 Subject: [PATCH 21/22] Assoc traversals can split due to union --- maltoolbox/language/language_graph_model_effect.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/maltoolbox/language/language_graph_model_effect.py b/maltoolbox/language/language_graph_model_effect.py index 33ea24c9..19f3233a 100644 --- a/maltoolbox/language/language_graph_model_effect.py +++ b/maltoolbox/language/language_graph_model_effect.py @@ -24,13 +24,13 @@ class AssocTraversal(NamedTuple): class DynTarget(NamedTuple): """A target in a dynamic sentence""" assoc_op: bool # Decides if we are adding/removing assets or connections to assets - assoc_traversal: list[AssocTraversal] + assoc_traversal: list[AssocTraversal | tuple[AssocTraversal, AssocTraversal]] @dataclass class LanguageGraphModelEffect: """An effect on the model instance triggered by compromising the attack step.""" - base: list[AssocTraversal] + base: list[AssocTraversal | tuple[AssocTraversal, AssocTraversal]] targets: list[DynTarget] # Assumes ^ between targets @@ -67,7 +67,9 @@ def build_assoc_traversal(expr_chain: ExpressionsChain | None) -> list[AssocTrav elif expr_chain.type == ExprType.TRANSITIVE: raise NotImplementedError() elif expr_chain.type == ExprType.UNION: - raise NotImplementedError() + left = build_assoc_traversal(expr_chain.left_link) + right = build_assoc_traversal(expr_chain.right_link) + return (left, right) raise ValueError(f"Unexpected expression chain type: {expr_chain.type}") From ee8f6304517f61bcf629dae5490c17a127790571 Mon Sep 17 00:00:00 2001 From: sandorstormen Date: Thu, 30 Jul 2026 08:38:24 +0200 Subject: [PATCH 22/22] Refactor: Simplify error handling in dynamal language tests and improve step declaration formatting --- tests/language/test_compiler.py | 14 +- .../intermediate/intDynamicTestLang13.mal | 130 +++++++++--------- tests/testdata/get_dynamal_test_langs.py | 35 +++-- 3 files changed, 94 insertions(+), 85 deletions(-) diff --git a/tests/language/test_compiler.py b/tests/language/test_compiler.py index 41d7af6e..cbfa2f7b 100644 --- a/tests/language/test_compiler.py +++ b/tests/language/test_compiler.py @@ -150,22 +150,12 @@ def test_compile_wiperlang(): def test_compile_basic_dynamal_languages(): dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/basic") for lang_file in dynamal_test_langs_dir.glob("*.mal"): - try: - lang = LanguageGraph.from_mal_spec(lang_file) - except Exception as e: - pytest.fail( - f"Failed to compile {lang_file}: {e}\n\nAST:\n{format_ast(lang_file)}" - ) + lang = LanguageGraph.from_mal_spec(lang_file) def test_compile_intermediate_dynamal_languages(): dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/intermediate") for lang_file in dynamal_test_langs_dir.glob("*.mal"): - try: - lang = LanguageGraph.from_mal_spec(lang_file) - except Exception as e: - pytest.fail( - f"Failed to compile {lang_file}: {e}\n\nAST:\n{format_ast(lang_file)}" - ) + lang = LanguageGraph.from_mal_spec(lang_file) def test_compile_advanced_dynamal_languages(): dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/advanced") diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal index b4aeed79..39f3acc7 100644 --- a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal @@ -10,83 +10,83 @@ category intDynamicTestLang13 { asset Directory { // Use at own risk | testAll - -> moveTopToBottom - -> moveTopToNothing - -> moveTopToAll - -> addTopToBottomLeft - -> removeTopToBottomLeft - -> addTopToBottomRight - -> removeTopToBottomRight - -> moveBackAndForth - -> moveMiddleToBottom - -> moveTopThenBottom - -> moveMiddleToAll - -> moveMiddleToAllAlternate - -> addMiddleToTopLeft - -> addMiddleToBottomLeft - -> addMiddleToTopRight - -> addMiddleToBottomRight - -> removeMiddleToTopLeft - -> removeMiddleToBottomLeft - -> removeMiddleToTopRight - -> removeMiddleToBottomRight - -> addMiddleToAll - -> removeMiddleToAll - -> moveBottomToTop - -> moveBottomToAll - -> moveBottomToNothing - -> addBottomToNothing - -> addBottomToTopLeft - -> addBottomToTopRight - -> addBottomToAll - -> removeBottomToNothingLeft - -> removeBottomToNothingRight - -> removeBottomToTopLeft - -> removeBottomToTopRight - -> removeBottomToAll + -> moveTopToBottom, + moveTopToNothing, + moveTopToAll, + addTopToBottomLeft, + removeTopToBottomLeft, + addTopToBottomRight, + removeTopToBottomRight, + moveBackAndForth, + moveMiddleToBottom, + moveTopThenBottom, + moveMiddleToAll, + moveMiddleToAllAlternate, + addMiddleToTopLeft, + addMiddleToBottomLeft, + addMiddleToTopRight, + addMiddleToBottomRight, + removeMiddleToTopLeft, + removeMiddleToBottomLeft, + removeMiddleToTopRight, + removeMiddleToBottomRight, + addMiddleToAll, + removeMiddleToAll, + moveBottomToTop, + moveBottomToAll, + moveBottomToNothing, + addBottomToNothing, + addBottomToTopLeft, + addBottomToTopRight, + addBottomToAll, + removeBottomToNothingLeft, + removeBottomToNothingRight, + removeBottomToTopLeft, + removeBottomToTopRight, + removeBottomToAll // Use on top directories | testTop - -> moveTopToBottom - -> moveTopToNothing - -> moveTopToAll - -> addTopToBottomLeft - -> removeTopToBottomLeft - -> addTopToBottomRight - -> removeTopToBottomRight - -> moveBackAndForth + -> moveTopToBottom, + moveTopToNothing, + moveTopToAll, + addTopToBottomLeft, + removeTopToBottomLeft, + addTopToBottomRight, + removeTopToBottomRight, + moveBackAndForth // Use on middle directories | testMiddle - -> addMiddleToTopLeft - -> addMiddleToBottomLeft - -> addMiddleToTopRight - -> addMiddleToBottomRight - -> removeMiddleToTopLeft - -> removeMiddleToBottomLeft - -> removeMiddleToTopRight - -> removeMiddleToBottomRight - -> addMiddleToAll - -> removeMiddleToAll + -> addMiddleToTopLeft, + addMiddleToBottomLeft, + addMiddleToTopRight, + addMiddleToBottomRight, + removeMiddleToTopLeft, + removeMiddleToBottomLeft, + removeMiddleToTopRight, + removeMiddleToBottomRight, + addMiddleToAll, + removeMiddleToAll // Use on bottom directories | testBottom - -> moveBottomToTop - -> moveTopThenBottom - -> moveBottomToAll - -> moveBottomToNothing - -> addBottomToNothing - -> addBottomToTopLeft - -> addBottomToTopRight - -> addBottomToAll - -> removeBottomToNothingLeft - -> removeBottomToNothingRight - -> removeBottomToTopLeft - -> removeBottomToTopRight - -> removeBottomToAll + -> moveBottomToTop, + moveTopThenBottom, + moveBottomToAll, + moveBottomToNothing, + addBottomToNothing, + addBottomToTopLeft, + addBottomToTopRight, + addBottomToAll, + removeBottomToNothingLeft, + removeBottomToNothingRight, + removeBottomToTopLeft, + removeBottomToTopRight, + removeBottomToAll diff --git a/tests/testdata/get_dynamal_test_langs.py b/tests/testdata/get_dynamal_test_langs.py index 346e6894..65fbc8e5 100644 --- a/tests/testdata/get_dynamal_test_langs.py +++ b/tests/testdata/get_dynamal_test_langs.py @@ -80,7 +80,7 @@ def fix_category(lang_path: Path): lang_path.write_text("".join(lines)) -step_start_pattern = re.compile(r"^\s*[|&#]") +step_start_pattern = re.compile(r"^\s*(\||&|#|!E(?![A-Za-z0-9_])|E(?![A-Za-z0-9_]))") stray_step_arrow_pattern = re.compile( r"^(\s*[|&#]\s*[A-Za-z_][A-Za-z0-9_]*)\s*>(\s*(//.*)?)$" ) @@ -102,23 +102,42 @@ def fix_stray_step_arrow(lang_path: Path): lang_path.write_text("".join(fixed_lines)) -reaches_pattern = re.compile(r"^\s*->\s*(.*)") +reaches_pattern = re.compile(r"^(\s*)->\s*(.*)$") def fix_double_reaching(lang_path: Path): - """Drop repeated `-> target` lines within a single attack step, - keeping only the first occurrence of each target.""" + """Merge repeated `-> target` lines within a single attack step into + one comma-separated clause, keeping only the first `->` and dropping + any exact-duplicate targets.""" lines = lang_path.read_text().splitlines(keepends=True) seen_targets: set[str] = set() - kept_lines = [] + last_line_idx: int | None = None + kept_lines: list[str] = [] for line in lines: if step_start_pattern.match(line): seen_targets = set() - reaches_match = reaches_pattern.match(line) - if reaches_match: - target = re.sub(r"//.*", "", reaches_match.group(1)).strip().rstrip(",") + last_line_idx = None + + eol = "\n" if line.endswith("\n") else "" + content = line[:-1] if eol else line + match = reaches_pattern.match(content) + if match: + indent, rest = match.groups() + target = re.sub(r"//.*", "", rest).strip().rstrip(",") if target in seen_targets: continue seen_targets.add(target) + + if last_line_idx is None: + kept_lines.append(line) + else: + prev_content = kept_lines[last_line_idx].rstrip("\n") + if not prev_content.endswith(","): + prev_content += "," + kept_lines[last_line_idx] = prev_content + "\n" + kept_lines.append(f"{indent}{rest}{eol}") + last_line_idx = len(kept_lines) - 1 + continue + kept_lines.append(line) lang_path.write_text("".join(kept_lines))