From 50eb863829613e595fb1ed60a3f5acd82701a7a2 Mon Sep 17 00:00:00 2001 From: Niels Steenbergen Date: Tue, 21 Jun 2022 05:55:53 +0200 Subject: [PATCH] Optionally add Top and Bottom types to taxonomy. See issue #94. Untested. This commit changes behaviour, because all sub- and supertypes in a lineage must now be canonical --- it is no longer possible to have a canonical root that has non-canonical supertypes. Also fixed an issue in `.uri()` that goes along with it, because it allowed unnamed operators to sometimes not throw an error when put into the taxonomy. --- tests/test_graph.py | 10 ++++++---- transformation_algebra/graph.py | 19 +++++++++++-------- transformation_algebra/lang.py | 13 +++++++++---- transformation_algebra/type.py | 30 +++++++++++++++++++----------- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/tests/test_graph.py b/tests/test_graph.py index 626d8d9..bf76946 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -581,9 +581,9 @@ def test_disabling_of_output_passthrough(self): # Source (of the general type) that takes an expression output (of the # specific type) as input. See issue #81. - A = TypeOperator() - B = TypeOperator(supertype=A) - TypeOperator(supertype=B) + A = TypeOperator('A') + B = TypeOperator('B', supertype=A) + TypeOperator('C', supertype=B) f = Operator(type=lambda x: x ** x [x <= A]) lang = Language(locals(), namespace=TEST) @@ -677,7 +677,9 @@ def test_taxonomy_with_parameterized_type_alias(self): actual.add_taxonomy() expected = make_taxonomy(lang, { A: {B}, - F: {F(A, B)}, + F: {F(A, A)}, + F(A, A): {F(A, B), F(B, A)}, F(A, B): {F(B, B)}, + F(B, A): {F(B, B)} }) self.assertIsomorphic(expected, actual) diff --git a/transformation_algebra/graph.py b/transformation_algebra/graph.py index 9eb4277..f0dd61c 100644 --- a/transformation_algebra/graph.py +++ b/transformation_algebra/graph.py @@ -6,7 +6,7 @@ from __future__ import annotations from transformation_algebra.type import Type, TypeOperation, \ - Function, TypeInstance + Function, TypeInstance, Top, Bottom from transformation_algebra.expr import \ Expr, Operation, Application, Abstraction, Source from transformation_algebra.lang import Language, TA @@ -88,23 +88,26 @@ def add_taxonomy(self) -> None: """ Add a taxonomy of types. """ + incl = self.language.include_top_and_bottom + # Add all canonical nodes and connect to their subtypes for t in self.language.canon: node = self.add_type(t) - for s in t.subtypes(): - self.add((self.add_type(s), RDFS.subClassOf, node)) + if t._operator is not Top: + for s in t.subtypes(incl): + self.add((self.add_type(s), RDFS.subClassOf, node)) - # Connect top-level type nodes (i.e. compound type operators) to the - # roots of their respective trees - for t in self.language.canon: + # Connect roots of trees to the Top supertype & to their operator if not any(s in self.language.canon for s in t.supertypes()): op = t._operator if op.arity > 0: self.add((self.language.uri(t), RDFS.subClassOf, self.language.uri(op))) - # self.add((self.language.uri(op), RDFS.subClassOf, - # self.language.uri(Top))) + if incl: + self.add((self.language.uri(op), RDFS.subClassOf, + self.language.uri(Top))) + # Transitive closure if self.with_transitive_closure: nodes = set(chain( (self.type_nodes[t] for t in self.language.canon), diff --git a/transformation_algebra/lang.py b/transformation_algebra/lang.py index 96e17b1..7b8d608 100644 --- a/transformation_algebra/lang.py +++ b/transformation_algebra/lang.py @@ -23,6 +23,7 @@ def __init__(self, scope: dict[str, Any] = {}, self.operators: dict[str, Operator] = dict() self.types: dict[str, TypeOperator] = dict() self.synonyms: dict[str, TypeAlias] = dict() + self.include_top_and_bottom: bool = False self._canon: set[TypeOperation] = set() self._closed = False @@ -56,6 +57,8 @@ def namespace(self) -> LanguageNamespace: return self._namespace def uri(self, value: Operator | TypeOperator | TypeOperation) -> URIRef: + if isinstance(value, TypeOperation) and value._operator.arity == 0: + value = value._operator if isinstance(value, (Operator, TypeOperator)): return (TA if value in builtins else self.namespace)[value.name] elif value in self.canon: @@ -65,17 +68,19 @@ def uri(self, value: Operator | TypeOperator | TypeOperation) -> URIRef: raise ValueError("non-canonical type") def generate_canon(self) -> set[TypeOperation]: + incl = self.include_top_and_bottom canon: set[TypeOperation] = set() - # canon.update((Unit(), Top(), Bottom(), Product())) - canon.update((op() for op in self.types.values() if op.arity == 0)) - # Canonicalize subtypes + # Start with base types and any compound type that has an alias stack: list[TypeOperation] = [s.instance() for s in self.synonyms.values() if s.canonical] + stack += [op() for op in self.types.values() if op.arity == 0] + + # ... and make sure that sub+supertypes are included while stack: current = stack.pop() canon.add(current) - for s in current.subtypes(): + for s in chain(current.subtypes(incl), current.supertypes(incl)): if s not in canon: stack.append(s) return canon diff --git a/transformation_algebra/type.py b/transformation_algebra/type.py index 0391471..76a0ec1 100644 --- a/transformation_algebra/type.py +++ b/transformation_algebra/type.py @@ -623,41 +623,49 @@ def __hash__(self) -> int: def basic(self) -> bool: return self._operator.arity == 0 - def subtypes(self, recursive: bool = False) -> Iterator[TypeOperation]: + def subtypes(self, inclusive: bool = False, + recursive: bool = False) -> Iterator[TypeOperation]: if recursive: raise NotImplementedError op = self._operator if op is Top: - raise NotImplementedError + raise RuntimeError("Cannot list all types.") elif op is Bottom: pass elif op.arity == 0: - # We skip the Bottom type - yield from (c() for c in op.children) + if op.children: + yield from (c() for c in op.children) + elif inclusive: + yield Bottom() else: for i, v, p in zip(count(), op.variance, self.params): if isinstance(p, TypeOperation): - for q in (p.subtypes() if Variance.CO else p.supertypes()): + for q in (p.subtypes(inclusive) if Variance.CO else + p.supertypes(inclusive)): yield op(*(q if i == j else p for j, p in enumerate(self.params))) else: raise RuntimeError - def supertypes(self, recursive: bool = False) -> Iterator[TypeOperation]: + def supertypes(self, inclusive: bool = False, + recursive: bool = False) -> Iterator[TypeOperation]: if recursive: raise NotImplementedError op = self._operator if op is Bottom: - raise NotImplementedError + raise RuntimeError("Cannot list all types.") elif op is Top: pass - elif op.arity == 0 and op.parent: - # We skip the Top type - yield op.parent() + elif op.arity == 0: + if op.parent: + yield op.parent() + elif inclusive: + yield Top() else: for i, v, p in zip(count(), op.variance, self.params): if isinstance(p, TypeOperation): - for q in (p.supertypes() if Variance.CO else p.subtypes()): + for q in (p.supertypes(inclusive) if Variance.CO else + p.subtypes(inclusive)): yield op(*(q if i == j else p for j, p in enumerate(self.params))) else: