diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 272fc38..85f96e8 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -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 304 + run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 307 # The rules earn their place by refusing a log that carries what they # name. Both fixtures are written here rather than tracked, and the diff --git a/docs/object-model.md b/docs/object-model.md index a452dc1..3a20c4a 100644 --- a/docs/object-model.md +++ b/docs/object-model.md @@ -113,7 +113,7 @@ hierarchies — `Box` (`TBox`, `STBox`, `TPCBox`), `Collection` tstz/geo/… leaves) and `Value` — and `objectModel.algebra` records which companion a temporal family yields. -`Value` holds the base values themselves: `Text`, `Jsonb`, `JsonPath`, `Geo` over +`Value` holds the values a method names: `Text`, `Jsonb`, `JsonPath`, `Geo` over `Geometry` and `Geography` (both a `GSERIALIZED`, parented exactly as `TGeo` parents `TGeometry` and `TGeography`), `Cbuffer`, `Npoint`, `Nsegment`, `Pose`, `PoseChain`, `Pcpoint`, `Pcpatch` and `Raquet`. A @@ -125,7 +125,13 @@ that set from the two predicates and fails until each has a class, so a base type MEOS adds reaches the model with no edit to the file. `Nsegment`, `Raquet` and `JsonPath` are the MeosTypes no membership predicate admits; the model states them, and the same gate states that it -does. +does. `Pcschema`, `RTree`, `SPTree` and `MeosArray` go further: MEOS +registers them in no enum at all, and they say so with a null `temptype` +rather than by leaving the field out. To a binding a pointer to a struct +is one thing — an instance it holds and calls through — whether MEOS +publishes the struct's layout or forward-declares it, which is why they +sit here rather than in a hierarchy of their own; GoMEOS's own wrapper +table draws no such line either. `Box` is covered the same way, from the `type_bboxtype` column of `MEOS_RELTYPE_CATALOG`: it names the box each type stores, so its diff --git a/meta/object-model.json b/meta/object-model.json index 955cd2e..cd1c751 100644 --- a/meta/object-model.json +++ b/meta/object-model.json @@ -566,7 +566,7 @@ } }, "Value": { - "_comment": "A base value crosses the MEOS boundary as a POINTER unless basetype_byvalue() names its type, and a method taking or answering one cannot be typed without a class for it. Parallel hierarchy, as Box and Collection are. temptypes gated against the MeosType enum, and the by-reference base types gated for COVERAGE against basetype_byvalue().", + "_comment": "A value crosses the MEOS boundary as a POINTER unless basetype_byvalue() names its type, and a method taking or answering one cannot be typed without a class for it. To a binding a pointer to a struct is one thing — an instance it holds and calls through — whether MEOS publishes the struct's layout or forward-declares it, so the schema, the trees and the array sit here beside the base values rather than in a hierarchy of their own. Parallel hierarchy, as Box and Collection are. temptypes gated against the MeosType enum where MEOS registers one, and the by-reference base types gated for COVERAGE against basetype_byvalue().", "root": "Value", "nodes": { "Value": { @@ -706,6 +706,43 @@ "temptype": "T_RAQUET", "conditional": "RASTER", "doc": "A raster tile. A MeosType no membership predicate admits, as Nsegment is." + }, + "Pcschema": { + "kind": "leaf", + "parent": "Value", + "prefixes": [ + "meos_pc_schema" + ], + "temptype": null, + "conditional": "POINTCLOUD", + "doc": "The dimension schema a point cloud value is read through. Every pcpoint and pcpatch accessor takes one, and the process-global cache answers it for a pcid. MEOS registers it in no enum, so it names no temptype." + }, + "RTree": { + "kind": "leaf", + "parent": "Value", + "prefixes": [ + "rtree" + ], + "temptype": null, + "doc": "MEOS's in-memory R-tree over spans, boxes and their kin. MEOS registers it in no enum, so it names no temptype." + }, + "SPTree": { + "kind": "leaf", + "parent": "Value", + "prefixes": [ + "sptree" + ], + "temptype": null, + "doc": "MEOS's in-memory SP-tree, the R-tree's sibling over the same key types. MEOS registers it in no enum, so it names no temptype." + }, + "MeosArray": { + "kind": "leaf", + "parent": "Value", + "prefixes": [ + "meos_array" + ], + "temptype": null, + "doc": "The growable array MEOS builds its own results in. MEOS registers it in no enum, so it names no temptype." } } } diff --git a/parser/object_model.py b/parser/object_model.py index ed7698f..32e49e7 100644 --- a/parser/object_model.py +++ b/parser/object_model.py @@ -78,6 +78,17 @@ def find_mobilitydb_src(headers_dir: Path | None = None) -> Path | None: _QUALIFIER_RE = re.compile(r"\b(?:const|struct)\b") +#: What a class's instances are never a pointer to. A `char *` is a string in +#: every binding and a scalar pointer is a buffer or an out-parameter, so +#: neither says anything about the type a class stands for. +_NOT_A_CLASS_POINTEE = frozenset({ + "char", "void", "bool", "int", "int8", "int8_t", "uint8", "uint8_t", + "short", "int16", "int16_t", "uint16", "uint16_t", "int32", "int32_t", + "uint32", "uint32_t", "float", "Oid", "DateADT", "long", "int64", + "int64_t", "uint64", "uint64_t", "double", "float8", "Datum", "Timestamp", + "TimestampTz", "TimeADT", "TimeOffset", "size_t", +}) + def _pointee(c_type: str) -> str | None: """The type a single-pointer C declaration points at, or None.""" @@ -210,8 +221,13 @@ def _role(fn_name: str) -> str: # Editorial name fixes for the rare cases mechanical derivation gets wrong. # Deliberately minimal — the clean derived name is canonical, so legacy -# binding spellings are not carried forward. -_OONAME_OVERRIDES: dict[str, str] = {} +# binding spellings are not carried forward. What belongs here is a function the +# derivation cannot name at all: one whose whole name IS its class's prefix, so +# dropping the prefix leaves nothing. +_OONAME_OVERRIDES: dict[str, str] = { + # `meos_pc_schema(pcid)` answers the schema the cache holds for a pcid. + "meos_pc_schema": "get", +} def _strip_class_token(fn_name: str, cls: str, prefix: str = "") -> str: @@ -445,25 +461,31 @@ def _class_ctypes(classes: dict, functions: dict, parents: dict, 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 `` 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. + answer of the subtype it is a product of, then what its own methods RETURN, + and any other class its parent's, which is the same C type by construction. """ - own = {} + own, made = {}, {} for cls, spec in classes.items(): - seen = {} + seen, answered = {}, {} for method in spec["methods"]: + fn = functions.get(method["function"]) + if not fn: + continue + pointee = _pointee(fn.get("returnType", {}).get("c", "")) + if pointee and pointee not in _NOT_A_CLASS_POINTEE: + answered[pointee] = answered.get(pointee, 0) + 1 if method["role"] not in _RECEIVER_ROLES: continue - fn = functions.get(method["function"]) - params = fn.get("params") if fn else None + params = fn.get("params") 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] + for source, out in ((seen, own), (answered, made)): + ranked = sorted(source.items(), key=lambda kv: -kv[1]) + if ranked and (len(ranked) == 1 or ranked[0][1] > ranked[1][1]): + out[cls] = ranked[0][0] resolved: dict = {} @@ -474,6 +496,11 @@ def resolve(cls, walked=frozenset()): resolved[cls] = own[cls] elif cls in subtype_classes: resolved[cls] = resolve(subtype_classes[cls], walked | {cls}) + elif cls in made: + # No method takes one, so what the class MAKES says what it is — + # a type MEOS only ever hands over, never calls a method on, is + # reached that way and no other. + resolved[cls] = made[cls] else: parent = parents.get(cls) resolved[cls] = (resolve(parent, walked | {cls}) diff --git a/tests/test_object_model.py b/tests/test_object_model.py index f9537df..a8dbbc4 100644 --- a/tests/test_object_model.py +++ b/tests/test_object_model.py @@ -90,7 +90,12 @@ def test_companions_are_well_formed_trees(self): if s["parent"]: self.assertIn(s["parent"], nodes) if s["kind"] == "leaf": + # A leaf names the ONE type it models, and a null says MEOS + # registers it in no enum — stated rather than left out, + # since an absent field reads the same as one nobody wrote. self.assertIn("temptype", s, n) + if s["temptype"] is None: + self.assertTrue(s.get("doc"), n) def test_traits_are_not_inheritance(self): # geometry/geodetic is a TRAIT axis, never a parent (no diamond). @@ -212,6 +217,43 @@ def test_ooname_drops_the_prefix_the_classifier_matched(self): # One token is dropped, not every repetition of it. self.assertEqual(names["set_set_subspan"], "setSubspan") + def test_a_class_nothing_is_called_on_takes_what_it_makes(self): + # Nothing is called ON a point-cloud schema — every accessor takes one + # as an ARGUMENT — so what the class MAKES says what it is, and the + # `char *` among those answers says nothing, being a string. + om = attach_object_model({"functions": [ + {"name": "meos_pc_schema", "returnType": {"c": "PCSCHEMA *"}, + "params": [{"name": "pcid", "cType": "uint32_t"}]}, + {"name": "meos_pc_schema_xml", "returnType": {"c": "const char *"}, + "params": [{"name": "pcid", "cType": "uint32_t"}]}, + {"name": "meos_pc_schema_compression", + "returnType": {"c": "const char *"}, + "params": [{"name": "pcid", "cType": "uint32_t"}]}, + ]}, MODEL, None)["objectModel"] + self.assertEqual(om["classes"]["Pcschema"]["cType"], "PCSCHEMA") + + def test_a_type_meos_registers_in_no_enum_still_gets_a_class(self): + # A pointer to a struct is one thing to a binding whether MEOS + # publishes the layout or forward-declares it, so these sit in `Value` + # beside the base values and say with a null that they name no type. + nodes = _nodes(json.loads(MODEL.read_text())["companions"]["Value"] + ["nodes"]) + unregistered = {n for n, s in nodes.items() + if s["kind"] == "leaf" and s["temptype"] is None} + self.assertEqual(unregistered, + {"Pcschema", "RTree", "SPTree", "MeosArray"}) + + def test_a_function_named_as_its_own_prefix_still_has_a_member_name(self): + # `meos_pc_schema` IS its class's prefix, so dropping the prefix leaves + # nothing — the one case the override table exists for. + om = attach_object_model({"functions": [ + {"name": "meos_pc_schema"}, {"name": "meos_pc_schema_ndims"}, + ]}, MODEL, None)["objectModel"] + names = {m["function"]: m["ooName"] + for m in om["classes"]["Pcschema"]["methods"]} + self.assertEqual(names, {"meos_pc_schema": "get", + "meos_pc_schema_ndims": "ndims"}) + 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 @@ -568,7 +610,7 @@ def test_every_bbox_type_has_a_companion_class(self): for fam in _nodes(self.attached["companions"]) for spec in _nodes( self.attached["companions"][fam]["nodes"]).values() - if spec["kind"] == "leaf"} + if spec["kind"] == "leaf" and spec.get("temptype")} self.assertEqual(boxes - claimed, set(), "a box type the relation catalog names has no " "companion class, so the method answering it is " @@ -592,6 +634,11 @@ def test_every_byreference_base_type_has_a_value_class(self): if spec["kind"] != "leaf": continue tt = spec["temptype"] + if tt is None: + # A type MEOS registers in no enum: a class for it is still + # needed to type the methods naming it, and the coverage this + # gate measures is over the types MEOS DOES register. + continue self.assertNotIn(tt, seen, f"{tt} is claimed by both {seen.get(tt)} and {node}") seen[tt] = node