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
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
# carries, or a change to them is not exercised until after it merges.
# Consumers use the action; this repository owns the rules.
- name: Refuse a skip, and a suite that shrank
run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 289
run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 292

# The rules earn their place by refusing a log that carries what they
# name. Both fixtures are written here rather than tracked, and the
Expand Down
11 changes: 11 additions & 0 deletions docs/object-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,17 @@ C-symbol guessing. A function with no prefix match (operator overloads,
`datum_*` base helpers, plumbing) is recorded honestly with
`class: null` and a reason — never force-fitted.

`objectModel.classes.<Class>.cType` names what the class's instances are a
pointer to, since a binding declares every wrapper in terms of it and
working it out per binding is what makes a class the binding cannot type
until it is edited. It is read from the signatures MEOS publishes: a
receiver-role method — accessor, predicate, conversion, restriction,
output — takes the value it is called on first, so the pointee of that
parameter names the type. A class holding constructors alone takes the
answer of the subtype it is a product of (`TFloatInst` → `TInstant`), and
any other its parent's, which is the same C type by construction. All 105
classes resolve one, over the 21 C types MEOS's own signatures name.

## Dispatch metadata

For 4 of the 6 temporal-type families the per-member argument→backing
Expand Down
85 changes: 85 additions & 0 deletions parser/object_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ def find_mobilitydb_src(headers_dir: Path | None = None) -> Path | None:
_COMPANION_PREFIX_ALIASES = {"GeomSet": ["geomset", "geoset"]}


#: The roles whose first parameter is the value the method is called on. A
#: constructor builds one out of something else and an aggregate takes the
#: accumulator, so neither says what the class's instances are.
_RECEIVER_ROLES = frozenset({"accessor", "predicate", "conversion",
"restriction", "output"})

_QUALIFIER_RE = re.compile(r"\b(?:const|struct)\b")


def _pointee(c_type: str) -> str | None:
"""The type a single-pointer C declaration points at, or None."""
bare = _QUALIFIER_RE.sub("", c_type).strip()
if bare.endswith("*") and bare.count("*") == 1:
return bare[:-1].strip()
return None


def _companion_families(model: dict) -> list:
"""The companion hierarchies the model carries, in file order.

Expand Down Expand Up @@ -410,6 +427,56 @@ def derive_membership(nodes: dict, cat_src: str, basetypes: dict) -> None:
spec["cBaseType"] = basetypes[temptype]


def _class_ctypes(classes: dict, functions: dict, parents: dict,
subtype_classes: dict) -> dict:
"""The C type each class's instances are a pointer to.

A binding declares every wrapper in terms of it, so leaving it to each
binding to work out is what makes a class a binding cannot type unless it
is edited. It is read here from the signatures MEOS already publishes: a
receiver-role method takes the value it is called on first, so the pointee
of that parameter names the type, and the class's own methods answer for
it. A class whose methods build values rather than take them — the
concrete `<leaf><subtype>` classes hold constructors alone — takes the
answer of the subtype it is a product of, and any other class its parent's,
which is the same C type by construction.
"""
own = {}
for cls, spec in classes.items():
seen = {}
for method in spec["methods"]:
if method["role"] not in _RECEIVER_ROLES:
continue
fn = functions.get(method["function"])
params = fn.get("params") if fn else None
if not params:
continue
pointee = _pointee(params[0]["cType"])
if pointee:
seen[pointee] = seen.get(pointee, 0) + 1
ranked = sorted(seen.items(), key=lambda kv: -kv[1])
if ranked and (len(ranked) == 1 or ranked[0][1] > ranked[1][1]):
own[cls] = ranked[0][0]

resolved: dict = {}

def resolve(cls, walked=frozenset()):
if cls in resolved:
return resolved[cls]
if cls in own:
resolved[cls] = own[cls]
elif cls in subtype_classes:
resolved[cls] = resolve(subtype_classes[cls], walked | {cls})
else:
parent = parents.get(cls)
resolved[cls] = (resolve(parent, walked | {cls})
if parent and parent not in walked | {cls}
else None)
return resolved[cls]

return {cls: resolve(cls) for cls in classes}


def attach_object_model(idl: dict, path: Path,
mobilitydb_src: Path | None = None) -> dict:
"""Attach ``idl["objectModel"]`` from the canonical lattice file."""
Expand Down Expand Up @@ -486,6 +553,24 @@ def attach_object_model(idl: dict, path: Path,
function_to_class[name]["concreteOf"] = tgt["concreteOf"]
function_to_class[name]["subtype"] = tgt["subtype"]

# What each class's instances are a pointer to, so a binding declares its
# wrappers from the model rather than from a map of its own.
parents = {n: s.get("parent") for n, s in lat.items()}
for fam in _companion_families(model):
for n, s in model["companions"][fam]["nodes"].items():
if not n.startswith("_"):
parents[n] = s.get("parent")
subtype_classes = {}
for leaf in [n for n, s in lat.items() if s["kind"] == "leaf"]:
for _tok, suffix, subtype in _SUBTYPE_SUFFIX:
if leaf + suffix in classes:
subtype_classes[leaf + suffix] = subtype
ctypes = _class_ctypes(classes, {f["name"]: f for f in functions},
parents, subtype_classes)
for cls, ctype in ctypes.items():
if ctype:
classes[cls]["cType"] = ctype

# Error contract
errors = dict(model["errors"])
if mobilitydb_src and Path(mobilitydb_src).exists():
Expand Down
39 changes: 39 additions & 0 deletions tests/test_object_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,45 @@ def test_internal_api_methods_are_excluded(self):
self.assertNotIn("ooExclude", meths["temporal_num_instants"])
self.assertTrue(meths["temporal_inst_n"].get("ooExclude"))

def test_class_ctype_comes_from_the_receiver(self):
# A receiver-role method takes the value it is called on first, so its
# pointee names what the class's instances are — which is what a
# binding declares every wrapper in terms of.
om = attach_object_model({"functions": [
{"name": "cbuffer_srid",
"params": [{"name": "cbuf", "cType": "const Cbuffer *"}]},
{"name": "geom_to_geog",
"params": [{"name": "geo", "cType": "const GSERIALIZED *"}]},
]}, MODEL, None)["objectModel"]
self.assertEqual(om["classes"]["Cbuffer"]["cType"], "Cbuffer")
self.assertEqual(om["classes"]["Geometry"]["cType"], "GSERIALIZED")

def test_a_constructor_only_class_takes_its_subtypes_ctype(self):
# `tfloatinst_make` builds a value out of a base value and a time, so
# its first parameter says nothing about the class; the concrete class
# is the product of a leaf and a subtype, and the subtype answers.
om = attach_object_model({"functions": [
{"name": "tfloatinst_make",
"params": [{"name": "d", "cType": "double"},
{"name": "t", "cType": "TimestampTz"}]},
{"name": "tinstant_value",
"params": [{"name": "inst", "cType": "const TInstant *"}]},
]}, MODEL, None)["objectModel"]
self.assertEqual(om["classes"]["TFloatInst"]["cType"], "TInstant")

def test_a_class_with_no_receiver_takes_its_parents_ctype(self):
# A collection leaf whose only method builds a set out of geographies
# says nothing about itself either, and it is no product of a subtype;
# its parent answers, which is the same C type by construction.
om = attach_object_model({"functions": [
{"name": "geogset_make",
"params": [{"name": "values", "cType": "const GSERIALIZED **"},
{"name": "count", "cType": "int"}]},
{"name": "set_num_values",
"params": [{"name": "s", "cType": "const Set *"}]},
]}, MODEL, None)["objectModel"]
self.assertEqual(om["classes"]["GeogSet"]["cType"], "Set")

def test_tree_derived(self):
om = self._attach(["temporal_merge"])["objectModel"]
lat = om["lattice"]
Expand Down
Loading