From d07ed09c585ff8a117340511021789b3dd8df676 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Thu, 23 Jul 2026 16:27:02 +0200 Subject: [PATCH] Derive the base-to-collection type-relation registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A base type T parameterises four independent template classes — Temporal, Set, Span and SpanSet. Resolving the concrete collection type of a value-domain result (SpanSet is floatspanset) is a static type-system fact, needed at generation time by every binding, but it lives only in the positional catalog arrays of meos_catalog.c. Parse those arrays (MEOS_SETTYPE_CATALOG, MEOS_SPANTYPE_CATALOG, MEOS_SPANSETTYPE_CATALOG, MEOS_TEMPTYPE_CATALOG) and MEOS_TYPE_NAMES, invert them, and attach idl["typeRelations"].byBase: for each base type name, the names of its set, span, span set and temporal types (absent when the base has none, so text carries no span). A binding projects the concrete collection wrapper from this registry rather than hard-coding the mapping. The parse degrades to no attachment when the source tree is unavailable, never a fabricated map. --- parser/typerelations.py | 81 ++++++++++++++++++++++++++++++++ run.py | 6 +++ tests/test_typerelations.py | 94 +++++++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 parser/typerelations.py create mode 100644 tests/test_typerelations.py diff --git a/parser/typerelations.py b/parser/typerelations.py new file mode 100644 index 0000000..03a8376 --- /dev/null +++ b/parser/typerelations.py @@ -0,0 +1,81 @@ +"""Attach the base-to-collection type-relation registry from ``meos_catalog.c``. + +A base type ``T`` is the single parameter of four independent template classes — +``Temporal``, ``Set``, ``Span`` and ``SpanSet``. The positional +catalog arrays in ``meos_catalog.c`` pair each template instance's ``MeosType`` +with its base (a span set with its span), and ``MEOS_TYPE_NAMES`` maps a +``MeosType`` to its public name. Inverting the arrays and resolving through the +names yields, for each base type name, the names of its set, span, span set and +temporal types. + +This is the static metadata a binding generator needs to pick the concrete +collection type of a value-domain result — ``SpanSet`` is ``floatspanset`` +— with no hand-coding: every binding is a projection of the catalog, so the +mapping belongs in the catalog rather than in each generator. +""" +import re +from pathlib import Path + +_NAME_RE = re.compile(r'\[\s*(T_\w+)\s*\]\s*=\s*"([^"]+)"') +_PAIR_RE = re.compile(r'\{\s*(T_\w+)\s*,\s*(T_\w+)\s*\}') + + +def _names(text: str) -> dict: + """The ``MeosType`` enum-name to public-name map from ``MEOS_TYPE_NAMES``.""" + m = re.search(r'MEOS_TYPE_NAMES\s*\[\]\s*=\s*\{(.*?)\};', text, re.S) + return dict(_NAME_RE.findall(m.group(1))) if m else {} + + +def _pairs(text: str, array: str) -> list: + """The ``{T_A, T_B}`` rows of a positional catalog array, in order.""" + m = re.search(re.escape(array) + r'\s*\[\]\s*=\s*\{(.*?)\};', text, re.S) + return _PAIR_RE.findall(m.group(1)) if m else [] + + +def attach_type_relations(idl: dict, src_root: Path | None) -> dict: + """Attach ``idl["typeRelations"]`` from the ``meos_catalog.c`` arrays. + + Degrades to no attachment — never a fabricated map — when the source tree is + not available, mirroring the honest-signal contract of the object-model scan. + """ + if src_root is None: + return idl + catalog = Path(src_root) / "temporal" / "meos_catalog.c" + if not catalog.exists(): + return idl + + text = re.sub(r"//.*", "", catalog.read_text(errors="ignore")) + names = _names(text) + + # Each array pairs an instance type with the type it is built over: a set, + # span or temporal with its base; a span set with its span. + base_of_set = {inst: base for inst, base in _pairs(text, "MEOS_SETTYPE_CATALOG")} + base_of_span = {inst: base for inst, base in _pairs(text, "MEOS_SPANTYPE_CATALOG")} + span_of_spanset = {inst: span for inst, span in _pairs(text, "MEOS_SPANSETTYPE_CATALOG")} + base_of_temp = {inst: base for inst, base in _pairs(text, "MEOS_TEMPTYPE_CATALOG")} + + # Invert to base -> instance; a span set reaches its base through its span. + set_of_base = {base: inst for inst, base in base_of_set.items()} + span_of_base = {base: inst for inst, base in base_of_span.items()} + temp_of_base = {base: inst for inst, base in base_of_temp.items()} + spanset_of_base = {} + for spanset, span in span_of_spanset.items(): + base = base_of_span.get(span) + if base is not None: + spanset_of_base[base] = spanset + + by_base = {} + for base in set(set_of_base) | set(span_of_base) | set(temp_of_base): + base_name = names.get(base) + if base_name is None: + continue + record = {} + for role, mapping in (("temporal", temp_of_base), ("set", set_of_base), + ("span", span_of_base), ("spanset", spanset_of_base)): + inst = mapping.get(base) + if inst is not None and names.get(inst) is not None: + record[role] = names[inst] + by_base[base_name] = record + + idl["typeRelations"] = {"byBase": dict(sorted(by_base.items()))} + return idl diff --git a/run.py b/run.py index 93b370c..d18da6b 100644 --- a/run.py +++ b/run.py @@ -19,6 +19,7 @@ from parser.doxygroup import attach_groups from parser.extractors import find_unlisted_foreign_structs from parser.object_model import attach_object_model, find_mobilitydb_src +from parser.typerelations import attach_type_relations HEADERS_DIR = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("./meos/include") @@ -264,6 +265,11 @@ def main(): f"(error scan: {MOBILITYDB_SRC})...", file=sys.stderr) idl = attach_object_model(idl, OBJMODEL_PATH, MOBILITYDB_SRC) + # Attach the base-to-collection type-relation registry (a base T to its set, span, span set + # and temporal types), derived from the meos_catalog.c positional arrays. A binding projects + # the concrete collection type of a value-domain result from this, never hard-coding it. + idl = attach_type_relations(idl, MOBILITYDB_SRC) + # Stamp the MobilityDB source commit so the catalog is SELF-DESCRIBING about its freshness: # a consumer proves it is current by comparing sourceCommit to live upstream master, never by # inspecting whatever directory a vendored copy sits in. None when the source is not a git diff --git a/tests/test_typerelations.py b/tests/test_typerelations.py new file mode 100644 index 0000000..a69b5d1 --- /dev/null +++ b/tests/test_typerelations.py @@ -0,0 +1,94 @@ +"""Unit tests for the base-to-collection type-relation registry. + +Runs without libclang or pytest: python3 tests/test_typerelations.py + +A hermetic fixture exercises the parse and the inversion; a source check +asserts the canonical numeric mappings against the live MobilityDB tree when it +is available (skipped, never fabricated, when it is not). +""" +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from parser.typerelations import attach_type_relations +from parser.object_model import find_mobilitydb_src + +_FIXTURE = """ +static const char *MEOS_TYPE_NAMES[] = +{ + [T_FLOAT8] = "float8", + [T_FLOATSET] = "floatset", + [T_FLOATSPAN] = "floatspan", + [T_FLOATSPANSET] = "floatspanset", + [T_TFLOAT] = "tfloat", + [T_TEXT] = "text", + [T_TEXTSET] = "textset", + [T_TTEXT] = "ttext", +}; +static const settype_catalog_struct MEOS_SETTYPE_CATALOG[] = +{ + {T_FLOATSET, T_FLOAT8}, + {T_TEXTSET, T_TEXT}, +}; +static const spantype_catalog_struct MEOS_SPANTYPE_CATALOG[] = +{ + {T_FLOATSPAN, T_FLOAT8}, +}; +static const spansettype_catalog_struct MEOS_SPANSETTYPE_CATALOG[] = +{ + {T_FLOATSPANSET, T_FLOATSPAN}, +}; +static const temptype_catalog_struct MEOS_TEMPTYPE_CATALOG[] = +{ + {T_TFLOAT, T_FLOAT8}, + {T_TTEXT, T_TEXT}, +}; +""" + + +class TypeRelationsParseTest(unittest.TestCase): + + def _attach(self, text): + with tempfile.TemporaryDirectory() as d: + catalog = Path(d) / "temporal" + catalog.mkdir() + (catalog / "meos_catalog.c").write_text(text) + return attach_type_relations({}, Path(d))["typeRelations"]["byBase"] + + def test_full_numeric_base_resolves_all_four_templates(self): + by_base = self._attach(_FIXTURE) + self.assertEqual(by_base["float8"], { + "temporal": "tfloat", "set": "floatset", + "span": "floatspan", "spanset": "floatspanset"}) + + def test_non_orderable_base_has_set_but_no_span(self): + # text has a set and a temporal type but no span/span set. + by_base = self._attach(_FIXTURE) + self.assertEqual(by_base["text"], {"temporal": "ttext", "set": "textset"}) + + def test_absent_source_degrades_without_fabricating(self): + self.assertNotIn("typeRelations", attach_type_relations({}, None)) + self.assertNotIn("typeRelations", attach_type_relations({}, Path("/no/such/tree"))) + + +class TypeRelationsSourceTest(unittest.TestCase): + + def test_canonical_numeric_mappings(self): + src = find_mobilitydb_src() + if src is None: + self.skipTest("MobilityDB source not available") + by_base = attach_type_relations({}, src)["typeRelations"]["byBase"] + self.assertEqual(by_base["float8"]["spanset"], "floatspanset") + self.assertEqual(by_base["int4"], { + "temporal": "tint", "set": "intset", + "span": "intspan", "spanset": "intspanset"}) + self.assertEqual(by_base["int8"]["span"], "bigintspan") + self.assertNotIn("span", by_base["text"]) + + +if __name__ == "__main__": + unittest.main()