Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/cross-repo-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ python tools/portable_parity.py # -> output/meos-portable-parity.json (bare-nam
| `meos-idl.json#/functions[].{network,wire,api}` | per-function projectability + decode/encode/array/out-param wire model |
| `meos-idl.json#/temporalTypes` | per `Temporal<T>`: its `base`, its `bbox`, the `mfjson` type token `asMFJSON` writes, and the `number` / `spatial` / `linear` classes |
| `meos-idl.json#/typeRelations/byBase` | each base type's `set`, `span`, `spanset` and its `temporal` types, the last a list since a base carries several |
| `meos-idl.json#/typeRelations/predicates` | each type category `meos_catalog.c` states as a membership predicate (`numspan_type`, `timespan_type`, `tnumber_spantype`, `tspatial_type`, ...) → its member types in `MeosType` order, with every family the catalog describes; the `ensure_*` validators state no category and are left out |
| `meos-coverage.json#/worklist` | every non-exposable public function with a `class` + concrete upstream `suggest`, ranked by `byClass` |
| `meos-portable-parity.json` | each bare name → backing family, with the verified parity headline |
| OpenAPI / MCP / runtime server | generated from the same catalog |
Expand Down
66 changes: 65 additions & 1 deletion parser/typerelations.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,69 @@ def locate_catalog(src_root: Path | None) -> Path | None:
return locate_temporal_source(src_root, "meos_catalog.c")


_PREDICATE_RE = re.compile(r'\nbool\s*\n(\w+)\(' + MEOSTYPE_PARAM + r' type\)\s*\{(.*?)\n\}', re.S)
_TERM_RE = re.compile(r'^(?:type\s*==\s*(T_\w+)|(\w+)\(\s*type\s*\))$')


def type_predicates(text: str, names: dict) -> dict:
"""The members of every membership predicate of ``meos_catalog.c``, in MeosType order.

A membership predicate is a ``bool <name>(MeosType type)`` whose body returns a
disjunction of ``type == T_X`` tests and of calls to other membership predicates
(`numspan_type`, `tnumber_type`, `spatialset_type`, ...): it states a category of types,
the sets of numbers or the spans a temporal box carries, and the catalog publishes each
category with its members so that a binding reads which types a category holds where
MEOS states it. A predicate whose body does anything else is not a membership statement
and is left out rather than approximated, as #object_model.predicate_types reads only a
predicate it names.
"""
bodies = {}
for name, body in _PREDICATE_RE.findall(text):
body = re.sub(r'/\*.*?\*/', '', body, flags=re.S)
# A family guard (`#if POINTCLOUD` ... `#endif`) admits its members in the build with
# every family, which is the build the catalog describes; an alternative (`#else`) is
# not a membership the catalog can state.
if re.search(r'^\s*#\s*(else|elif)\b', body, re.M):
continue
body = re.sub(r'^\s*#\s*(if|ifdef|ifndef|endif)\b.*$', '', body, flags=re.M)
m = (re.fullmatch(r'\s*return\s*\(?(.*?)\)?\s*;\s*', body, re.S)
or re.fullmatch(r'\s*bool\s+(\w+)\s*=\s*\(?(.*?)\)?\s*;\s*return\s+\1\s*;\s*',
body, re.S))
if not m:
continue
terms = [t.strip() for t in re.split(r'\|\|', m.group(m.lastindex))]
parsed = [_TERM_RE.match(t) for t in terms]
if all(parsed):
bodies[name] = [(p.group(1), p.group(2)) for p in parsed]

order = {t: i for i, t in enumerate(names)}
resolved = {}

def members(name, seen=()):
if name in resolved:
return resolved[name]
if name in seen or name not in bodies:
return None
out = set()
for literal, call in bodies[name]:
if literal:
out.add(literal)
else:
sub = members(call, seen + (name,))
if sub is None:
return None
out |= sub
resolved[name] = out
return out

result = {}
for name in bodies:
types = members(name)
if types and all(t in names for t in types):
result[name] = [names[t] for t in sorted(types, key=lambda t: order[t])]
return dict(sorted(result.items()))


def attach_type_relations(idl: dict, src_root: Path | None) -> dict:
"""Attach ``idl["typeRelations"]`` from the ``MEOS_RELTYPE_CATALOG`` array.

Expand Down Expand Up @@ -203,5 +266,6 @@ def attach_type_relations(idl: dict, src_root: Path | None) -> dict:
f"{catalog}: no type relation parsed from MEOS_RELTYPE_CATALOG — the catalog's "
"shape has changed and parser/typerelations.py no longer reads it")

idl["typeRelations"] = {"byBase": dict(sorted(by_base.items()))}
idl["typeRelations"] = {"byBase": dict(sorted(by_base.items())),
"predicates": type_predicates(text, names)}
return idl
95 changes: 95 additions & 0 deletions tests/test_typerelations.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,86 @@ def test_mdb_src_root_resolves_when_object_model_root_is_absent(self):
self.assertEqual(by_base["float8"]["spanset"], "floatspanset")


_PREDICATES = _FIXTURE + """
bool
numspan_type(MeosType type)
{
return (/* Dates are represented as integers */ type == T_FLOATSPAN);
}

bool
span_type(MeosType type)
{
return (numspan_type(type) || type == T_FLOATSET);
}

bool
tspatial_type(MeosType type)
{
return (type == T_TGEOMPOINT
#if GEOMETRY
|| type == T_TGEOMETRY
#endif
);
}

bool
linear_type(MeosType type)
{
bool result = (type == T_TFLOAT || type == T_TGEOMPOINT);
return result;
}

bool
ensure_tspatial_type(MeosType type)
{
if (tspatial_type(type))
return true;
return false;
}

bool
alternative_type(MeosType type)
{
#if GEOMETRY
return type == T_TGEOMETRY;
#else
return type == T_TGEOMPOINT;
#endif
}
"""


class TypePredicatesParseTest(unittest.TestCase):

def _predicates(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"]["predicates"]

def test_a_predicate_lists_its_members_in_meostype_order(self):
preds = self._predicates(_PREDICATES)
self.assertEqual(preds["numspan_type"], ["floatspan"])
self.assertEqual(preds["linear_type"], ["tfloat", "tgeompoint"])

def test_a_predicate_calling_another_takes_its_members(self):
self.assertEqual(self._predicates(_PREDICATES)["span_type"], ["floatset", "floatspan"])

def test_a_family_guard_admits_its_members(self):
# The catalog describes the build with every family, so a member behind `#if` is one
self.assertEqual(self._predicates(_PREDICATES)["tspatial_type"],
["tgeompoint", "tgeometry"])

def test_what_states_no_membership_is_left_out(self):
# an ensure_ raises rather than states a category, and a body with an alternative
# (`#else`) holds no one membership
preds = self._predicates(_PREDICATES)
self.assertNotIn("ensure_tspatial_type", preds)
self.assertNotIn("alternative_type", preds)


class TypeRelationsSourceTest(unittest.TestCase):

def test_canonical_numeric_mappings(self):
Expand Down Expand Up @@ -182,6 +262,21 @@ def test_canonical_numeric_mappings(self):
for name in ("tpose", "tgeompoint", "tgeogpoint", "trgeometry", "tgeometry"):
self.assertIn(name, reachable)

def test_canonical_categories(self):
# The live counterpart of TypePredicatesParseTest, as #test_canonical_numeric_mappings
# is of the relation parse
src = find_mobilitydb_src()
if src is None and locate_catalog(None) is None:
self.skipTest("MobilityDB source not available")
preds = attach_type_relations({}, src)["typeRelations"]["predicates"]
# The numbers of a temporal box and the temporal numbers whose box it is
self.assertEqual(preds["tnumber_spantype"], ["floatspan", "intspan", "bigintspan"])
self.assertEqual(preds["tnumber_type"], ["tfloat", "tint", "tbigint"])
self.assertEqual(preds["timespan_type"], ["datespan", "tstzspan"])
# A member behind a family guard (`#if POINTCLOUD`)
self.assertIn("pcpatchset", preds["spatialset_type"])
self.assertFalse(any(name.startswith("ensure_") for name in preds))


if __name__ == "__main__":
unittest.main()
Loading