diff --git a/parser/typerecover.py b/parser/typerecover.py index 505de8a..5d4baa4 100644 --- a/parser/typerecover.py +++ b/parser/typerecover.py @@ -50,6 +50,17 @@ "JsonPath": "JsonPath", } +# libclang renders a fixed-width integer typedef's fully-resolved `canonical` as the +# platform builtin spelling (uint64_t -> "unsigned long" on LP64). When the c-field is the +# typedef but `canonical` is that platform alias, normalize `canonical` too, so the same +# underlying type is spelled identically catalog-wide -- e.g. the Tcell cell-id accessors +# th3index_start_value (H3Index, from libh3) and tquadbin_start_value (Quadbin) BOTH read +# uint64_t, not one "unsigned long" and the other "uint64_t". +_CANON_ALIAS = { + "uint64_t": {"unsignedlong", "longunsignedint", "unsignedlonglong"}, + "int64_t": {"long", "longint", "longlong"}, +} + _NAMES = "|".join(sorted(_TYPE_MAP, key=len, reverse=True)) # optional const, a recoverable base, optional pointer stars, optional identifier _DECL_RE = re.compile( @@ -135,12 +146,20 @@ def _apply(slot, recovery): # canonical collapses (slot spells `original`, e.g. a MobilityDB typedef # such as Quadbin whose uint64 underlying type was the part that erased). recoverable = (_nospace(collapsed), _nospace(original)) - if _nospace(slot.get(key)) not in recoverable: + cur = _nospace(slot.get(key)) + # `cur in recoverable`: the base name collapsed to int, or survived as the typedef. + # `cur == recovered`: libclang already rendered the c-field as the typedef's immediate + # underlying type (e.g. H3Index -> uint64_t) while leaving `canonical` at the fully + # resolved platform spelling ("unsigned long") -> fall through to normalize canonical. + if cur not in recoverable and cur != _nospace(recovered): return 0 + rewrote = slot.get(key) != recovered slot[key] = recovered - if _nospace(slot.get("canonical")) in recoverable: + canon = _nospace(slot.get("canonical")) + if canon in recoverable or canon in _CANON_ALIAS.get(_nospace(recovered), ()): + rewrote = rewrote or slot.get("canonical") != recovered slot["canonical"] = recovered - return 1 + return 1 if rewrote else 0 def patch(fn): rec = decls.get(fn.get("name")) diff --git a/tests/test_typerecover.py b/tests/test_typerecover.py index 45c9c50..5304c88 100644 --- a/tests/test_typerecover.py +++ b/tests/test_typerecover.py @@ -133,6 +133,18 @@ def test_uint64_recovered(self): self.assertEqual(self._ret("span_hash_extended"), "uint64_t") self.assertIn("uint64_t", self._param_ctypes("set_hash_extended")) + def test_cell_id_canonical_normalized_uniform(self): + # H3Index (libh3's typedef, whose fully-resolved canonical is the platform + # "unsigned long") and Quadbin (MobilityDB's typedef, recovered to "uint64_t") + # are BOTH uint64 cell ids; as Tcell subtypes they must be spelled identically. + # The ``canonical`` field must normalize to "uint64_t" for both, not leave H3Index + # at "unsigned long" — a guard on the _CANON_ALIAS canonical-normalization pass. + for name in ("th3index_start_value", "th3index_end_value", + "tquadbin_start_value", "tquadbin_end_value", "h3index_in"): + rt = self.by_name[name]["returnType"] + self.assertEqual(rt["c"], "uint64_t", f"{name} c") + self.assertEqual(rt["canonical"], "uint64_t", f"{name} canonical") + # ---- genuine-int controls (must NOT be rewritten) ---------------------- def test_genuine_int_left_untouched(self):