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
55 changes: 55 additions & 0 deletions parser/typerecover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 12 additions & 1 deletion run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions tests/test_typerecover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading