From 1cdb02d80f52ec5dc5579584372937938ea4ce47 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Wed, 23 Sep 2026 15:24:54 +0200 Subject: [PATCH 1/5] Add utility functions to MatchingTimelines to help with flattening --- ymmsl/v0_2/model.py | 31 ++++++++++++++++++++++- ymmsl/v0_2/tests/test_model.py | 46 +++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/ymmsl/v0_2/model.py b/ymmsl/v0_2/model.py index 432d87b..fbcdb27 100644 --- a/ymmsl/v0_2/model.py +++ b/ymmsl/v0_2/model.py @@ -1,7 +1,7 @@ from collections import OrderedDict from copy import copy from enum import Enum -from typing import Any, List, Sequence, TypeAlias, cast +from typing import Any, List, Self, Sequence, TypeAlias, cast import yatiml @@ -331,6 +331,35 @@ def __init__( def __contains__(self, timeline: Timeline) -> bool: return timeline in self.matches + def __copy__(self) -> "MatchingTimelines": + """Shallow-copy the object. + + Since this class effectively models a container, a shallow copy should create a + new container with the same items in it, to which items can be added or removed + without affecting the original. By default, copy.copy() won't do that however, + as it will return a new MatchingTimelines containing the same self.matches. So + we override it to give more natural semantics. + """ + return MatchingTimelines(self.head, list(self.matches)) + + def __ior__(self, other: "MatchingTimelines") -> Self: + """Merge another MatchingTimelines into this one. + + This takes all of the matching timelines from other and adds them to this + object. If the head timeline of other is nested less deeply than the current + head, then it will replace the current head, otherwise the head remains + unchanged. + """ + + self.matches |= other.matches + + this_depth = max([len(ref) for ref in self.head]) + other_depth = max([len(ref) for ref in other.head]) + if other_depth < this_depth: + self.head = other.head + + return self + def _yatiml_attributes(self) -> OrderedDict: matches: str | list[str] = list(map(str, sorted(self.matches - {self.head}))) if len(matches) < 6 and sum(len(m) for m in matches) < 60: diff --git a/ymmsl/v0_2/tests/test_model.py b/ymmsl/v0_2/tests/test_model.py index 7f33fdc..2b5ab25 100644 --- a/ymmsl/v0_2/tests/test_model.py +++ b/ymmsl/v0_2/tests/test_model.py @@ -1,3 +1,4 @@ +from copy import copy from typing import Callable import pytest @@ -194,7 +195,7 @@ def test_dump_multicast_conduits() -> None: assert text == ("sender: init.out\nreceiver:\n- c1.in\n- repeat pad c2.in\n") -def test_create_matching_timeline() -> None: +def test_create_matching_timelines() -> None: mt = MatchingTimelines(Timeline("tl1"), "tl2") assert isinstance(mt.head, Timeline) assert mt.head == Timeline("tl1") @@ -225,6 +226,49 @@ def test_create_matching_timeline() -> None: } +def test_copy_matching_timelines() -> None: + tl1 = Timeline("tl1") + tl2 = Timeline("tl2") + mt1 = MatchingTimelines(tl1, [tl2]) + + mt2 = copy(mt1) + + assert mt2.head is mt1.head + assert mt2.matches is not mt1.matches + for m2 in mt2.matches: + assert len([m1 for m1 in mt1.matches if m1 is m2]) > 0 + + +def test_merge_matching_timelines() -> None: + mt1 = MatchingTimelines(Timeline("tl1"), "tl2") + mt2 = MatchingTimelines(Timeline("tl2"), "tl3") + + mt1 |= mt2 + assert mt1.head == "tl1" + assert mt1.matches == {Timeline("tl1"), Timeline("tl2"), Timeline("tl3")} + + mt3 = MatchingTimelines(Timeline("tl4"), "tl5") + mt1 |= mt3 + assert mt1.head == "tl1" + assert mt1.matches == { + Timeline("tl1"), + Timeline("tl2"), + Timeline("tl3"), + Timeline("tl4"), + Timeline("tl5"), + } + + mt4 = MatchingTimelines(Timeline("c1.c2.tl1"), "c1.c3.tl1") + mt4 |= mt3 + assert mt4.head == "tl4" + assert mt4.matches == { + Timeline("tl4"), + Timeline("tl5"), + Timeline("c1.c2.tl1"), + Timeline("c1.c3.tl1"), + } + + def test_load_matching_timelines() -> None: load = yatiml.load_function(MatchingTimelines, Timeline) From feb467915a4756a4b4df3498f3fdfade3e67419c Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Fri, 25 Sep 2026 16:44:09 +0200 Subject: [PATCH 2/5] Add small usability improvements --- ymmsl/v0_2/ports.py | 8 ++++++++ ymmsl/v0_2/timeline_resolver.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/ymmsl/v0_2/ports.py b/ymmsl/v0_2/ports.py index 04dc439..aa422e1 100644 --- a/ymmsl/v0_2/ports.py +++ b/ymmsl/v0_2/ports.py @@ -180,6 +180,14 @@ def __init__( self.operator = operator if timeline is None: timeline = Timeline("") + + lengthy = [ref for ref in timeline if len(ref) > 1] + if lengthy: + raise ValueError( + "To avoid confusion, timeline annotations must have a single" + " identifier between colons, so {timeline} is not valid. Please" + " use names without periods instead of e.g. {lengthy[0]}.") + self.timeline = timeline def __eq__(self, other: Any) -> bool: diff --git a/ymmsl/v0_2/timeline_resolver.py b/ymmsl/v0_2/timeline_resolver.py index 2b55902..9d0054c 100644 --- a/ymmsl/v0_2/timeline_resolver.py +++ b/ymmsl/v0_2/timeline_resolver.py @@ -377,6 +377,8 @@ def format_timelines(self) -> str: """Create a formatted list of determined timelines per component.""" return "\n".join( f"- Component '{comp}' has timeline '{tl}:{comp}'" + if tl + else f"- Component '{comp}' has timeline '{comp}'" for comp, tl in self._parent_timeline.items() if len(comp) > 0 # Ony print actual components ) From f987316161440314d9f889e6666e4f1eedf8bc90 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sat, 26 Sep 2026 16:14:16 +0200 Subject: [PATCH 3/5] Fix timeline for model ports with deep timeline annotation --- ymmsl/v0_2/timeline_resolver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ymmsl/v0_2/timeline_resolver.py b/ymmsl/v0_2/timeline_resolver.py index 9d0054c..5c54fe7 100644 --- a/ymmsl/v0_2/timeline_resolver.py +++ b/ymmsl/v0_2/timeline_resolver.py @@ -273,8 +273,8 @@ def timeline_for_port( This returns the name of the timeline the given port communicates on. These are as follows: - - for a model port without timeline annotation, empty - - for a model port with a timeline annotation, that annotation + - for a model port without timeline annotation, + - for a model port with a timeline annotation tl1:tl2, .tl1:.tl2 - for a component port without timeline annotation, : - for a component O_I or S port with timeline annotation "subtl1", :.subtl1 @@ -296,9 +296,9 @@ def timeline_for_port( assert isinstance(model_port, Identifier) port = self._model.ports[model_port] if port.timeline: - result = port.timeline + result = Timeline([self._model.name + tl for tl in port.timeline]) else: - result = ROOT_TIMELINE + result = Timeline([self._model.name]) else: # Component port From 2210a3805dc8298cc8cd9e533ad52796543b64d0 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sat, 26 Sep 2026 16:14:46 +0200 Subject: [PATCH 4/5] Fix formatting --- ymmsl/v0_2/ports.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ymmsl/v0_2/ports.py b/ymmsl/v0_2/ports.py index aa422e1..24487ee 100644 --- a/ymmsl/v0_2/ports.py +++ b/ymmsl/v0_2/ports.py @@ -184,9 +184,10 @@ def __init__( lengthy = [ref for ref in timeline if len(ref) > 1] if lengthy: raise ValueError( - "To avoid confusion, timeline annotations must have a single" - " identifier between colons, so {timeline} is not valid. Please" - " use names without periods instead of e.g. {lengthy[0]}.") + "To avoid confusion, timeline annotations must have a single" + " identifier between colons, so {timeline} is not valid. Please" + " use names without periods instead of e.g. {lengthy[0]}." + ) self.timeline = timeline From 5022bf57133c8c8483293ed18b0bc26f6ef12660 Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Sat, 26 Sep 2026 17:28:30 +0200 Subject: [PATCH 5/5] Fix Python 3.10 compatibility --- ymmsl/v0_2/model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ymmsl/v0_2/model.py b/ymmsl/v0_2/model.py index fbcdb27..524a495 100644 --- a/ymmsl/v0_2/model.py +++ b/ymmsl/v0_2/model.py @@ -1,9 +1,10 @@ from collections import OrderedDict from copy import copy from enum import Enum -from typing import Any, List, Self, Sequence, TypeAlias, cast +from typing import Any, List, Sequence, TypeAlias, cast import yatiml +from typing_extensions import Self from ymmsl.v0_2.component import Component from ymmsl.v0_2.identity import Identifier, Reference