diff --git a/parser/typerecover.py b/parser/typerecover.py index 5d4baa4..ded1806 100644 --- a/parser/typerecover.py +++ b/parser/typerecover.py @@ -184,3 +184,58 @@ def walk(o): walk(idl) return idl, fixed + + +# Strip const/struct qualifiers and pointer stars to the bare base name. +_BASE_RE = re.compile(r"\b(?:const|struct|volatile)\b|\*") + + +def _base_name(t): + return _BASE_RE.sub(" ", t or "").strip() + + +def normalize_canonical(idl): + """Re-derive each type slot's ``canonical`` from its ``cType`` typedef. + + ``canonical`` is the MEOS/PG typedef the public API exposes (``_TYPE_MAP``), + not libclang's fully-resolved platform type. The self-contained (installed) + header parse resolves ``TimestampTz`` -> ``long`` and ``Jsonb *`` -> ``struct + varlena *`` while ``cType`` keeps the faithful typedef, so re-derive + ``canonical`` from ``cType`` -- a binding generator keys on ``canonical`` and + must see the semantic type (a timestamp, a jsonb), never its platform width. + Idempotent; a no-op on the source parse (``canonical`` already equals the + typedef) and on non-typedef slots (``Temporal *``, ``int *``). Complements + ``recover_collapsed_types``: that recovers a ``cType`` the preprocessor erased + to ``int``; this trusts a faithful ``cType`` and only re-spells ``canonical``. + """ + fixed = 0 + + def want(ctype): + mapped = _TYPE_MAP.get(_base_name(ctype)) + if not mapped: + return None + const = "const " if re.search(r"\bconst\b", ctype) else "" + stars = "".join(c for c in ctype if c == "*") + return f"{const}{mapped}{(' ' + stars) if stars else ''}" + + def fix(slot): + nonlocal fixed + if not (isinstance(slot, dict) and "canonical" in slot): + return + ctype = slot.get("cType") or slot.get("c") + w = want(ctype) if ctype else None + if w and _nospace(slot["canonical"]) != _nospace(w): + slot["canonical"] = w + fixed += 1 + + def walk(o): + if isinstance(o, dict): + fix(o) + for v in o.values(): + walk(v) + elif isinstance(o, list): + for v in o: + walk(v) + + walk(idl) + return idl, fixed diff --git a/run.py b/run.py index ac607d5..d082a6b 100644 --- a/run.py +++ b/run.py @@ -7,7 +7,7 @@ from parser.parser import parse_all_headers, merge_meta from parser.portable import attach_portable_aliases, classify_backing_sqlfn from parser.covering import attach_temporal_covering -from parser.typerecover import recover_collapsed_types +from parser.typerecover import recover_collapsed_types, normalize_canonical from parser.header_types import reconcile from parser.shapeinfer import infer_shapes from parser.nullable import merge_nullable @@ -79,6 +79,17 @@ def main(): # recover_collapsed_types (H3Index/Quadbin -> uint64_t) are left intact. idl = reconcile(idl, HEADERS_DIR) + # 1d. Re-spell each slot's `canonical` as the MEOS typedef its `cType` names, + # not libclang's platform resolution. The self-contained (installed) + # header parse resolves TimestampTz -> long and Jsonb * -> varlena *; the + # source parse leaves them as the typedef. Deriving canonical from the + # faithful cType makes both parses agree, so a binding generator (which + # keys on canonical) marshals timestamps/jsonb rather than dropping them. + idl, ncanon = normalize_canonical(idl) + if ncanon: + print(f" normalized {ncanon} canonical spellings to the cType typedef", + file=sys.stderr) + # 1d. Generate the codegen `shape` from the signatures + Doxygen, replacing # the hand-maintained meta stub. outputArrays/arrayReturn come from the # parameter forms; nullable comes from the C `@param ... may be NULL` SoT. diff --git a/tests/test_typerecover.py b/tests/test_typerecover.py index 5304c88..00a0f0b 100644 --- a/tests/test_typerecover.py +++ b/tests/test_typerecover.py @@ -145,6 +145,26 @@ def test_cell_id_canonical_normalized_uniform(self): self.assertEqual(rt["c"], "uint64_t", f"{name} c") self.assertEqual(rt["canonical"], "uint64_t", f"{name} canonical") + def test_typedef_canonical_not_platform_resolved(self): + # ``canonical`` is the MEOS typedef its ``cType`` names, never libclang's + # fully-resolved platform type. On the self-contained (installed-header) + # parse ``TimestampTz`` resolves to ``long`` and ``Jsonb *`` / ``JsonPath + # *`` to ``varlena *`` while ``cType`` keeps the typedef; normalize_canonical + # re-derives ``canonical`` from the faithful ``cType`` so a binding + # generator (which keys on ``canonical``) marshals the semantic type + # instead of dropping the function — a guard on that pass. + def canon(name, pname): + self.assertIn(name, self.by_name, f"{name} missing from IDL") + p = next(p for p in self.by_name[name]["params"] if p["name"] == pname) + return (p["cType"], p["canonical"]) + self.assertEqual(canon("tint_value_at_timestamptz", "t"), + ("TimestampTz", "TimestampTz")) + if "jsonb_path_exists" in self.by_name: # JSON=ON-conditional surface + self.assertEqual(canon("jsonb_path_exists", "jb"), + ("const Jsonb *", "const Jsonb *")) + self.assertEqual(canon("jsonb_path_exists", "jp"), + ("const JsonPath *", "const JsonPath *")) + # ---- genuine-int controls (must NOT be rewritten) ---------------------- def test_genuine_int_left_untouched(self):