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
34 changes: 33 additions & 1 deletion parser/extractors.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,36 @@ def extract_enum(node) -> dict:
for v in node.get_children()
if v.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL
],
}
}


def extract_macro(node) -> dict | None:
"""Extract a public object-like integer ``#define`` macro.

The MEOS public headers expose a handful of integer constants as
``#define`` object-like macros rather than ``enum`` values — the WKB / WKT
output-variant flags (``WKB_NDR``, ``WKB_EXTENDED`` …), the ``MEOS_FLAG_*``
bit masks, and similar. They are part of the public API surface (a binding's
FFI must carry them: ``meos-rs`` uses ``WKB_EXTENDED`` / ``WKB_NDR`` /
``WKB_XDR`` to select a WKB variant), but ``enum`` extraction never sees them
because they are preprocessor definitions.

Only object-like macros whose body is a single integer literal are
extracted (matching the constants a binding can project as a typed
constant); function-like macros and expression / string macros are skipped.
"""
tokens = [t.spelling for t in node.get_tokens()]
# tokens[0] is the macro name; a function-like macro has "(" immediately after.
if len(tokens) < 2 or tokens[1] == "(":
return None
body = "".join(tokens[1:])
try:
value = int(body, 0)
except ValueError:
return None
return {
"name": node.spelling,
"file": Path(node.location.file.name).name,
"family": _family_of(node.location.file.name),
"value": value,
}
25 changes: 21 additions & 4 deletions parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import os
from pathlib import Path

from parser.extractors import extract_function, extract_struct, extract_enum
from parser.extractors import (
extract_function,
extract_struct,
extract_enum,
extract_macro,
)
from parser.type_resolver import resolve_idl_types


Expand Down Expand Up @@ -105,7 +110,11 @@ def parse_meos(entry: Path, include_dir: Path) -> dict:
# it, and an undefined ``UNUSED`` makes clang error on the declarator and
# silently drop the remaining parameters of that prototype.
"-DUNUSED=__attribute__((unused))",
] + _clang_extra_args())
] + _clang_extra_args(),
# Record ``#define`` macro definitions as cursors so the public
# object-like integer macros (WKB / WKT variant flags, ``MEOS_FLAG_*``)
# can be extracted into the catalog — an ``enum`` walk never sees them.
options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD)

# Collect all .h files belonging to the project
own_files = {str(p.resolve()) for p in include_dir.glob("**/*.h")}
Expand All @@ -122,13 +131,19 @@ def parse_meos(entry: Path, include_dir: Path) -> dict:
if m:
typedef_map[m.group(1)] = node.spelling

functions, structs, enums = [], [], []
functions, structs, enums, macros = [], [], [], []

for node in tu.cursor.walk_preorder():
loc = node.location.file
if not loc or str(Path(loc.name).resolve()) not in own_files:
continue # skip stdlib, system headers, etc.

if node.kind == clang.cindex.CursorKind.MACRO_DEFINITION:
macro = extract_macro(node)
if macro:
macros.append(macro)
continue

if node.kind == clang.cindex.CursorKind.FUNCTION_DECL:
functions.append(extract_function(node))

Expand Down Expand Up @@ -161,8 +176,10 @@ def _dedup(items: list) -> list:
functions = _dedup(functions)
structs = _dedup(structs)
enums = _dedup(enums)
macros = _dedup(macros)

idl = {"functions": functions, "structs": structs, "enums": enums}
idl = {"functions": functions, "structs": structs, "enums": enums,
"macros": macros}

# Resolve types if the mappings file exists
mappings_path = Path("./meta/type-mappings.json")
Expand Down
1 change: 1 addition & 0 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def main():
f"({exposable} stateless-exposable), "
f"{len(idl['structs'])} structs, "
f"{len(idl['enums'])} enums, "
f"{len(idl.get('macros', []))} macros, "
f"{pa} portable bare-name aliases, "
f"{cov} temporal covering types", file=sys.stderr)
if om:
Expand Down
53 changes: 53 additions & 0 deletions tests/test_macros.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Regression tests for public ``#define`` macro extraction into the IDL.

A handful of MEOS public constants are object-like ``#define`` macros, not
``enum`` values — the WKB / WKT output-variant flags and the ``MEOS_FLAG_*``
bit masks. An ``enum`` walk never sees them, so the parser records the
preprocessor definitions separately (``options`` enables the detailed
preprocessing record). A binding's FFI needs them: ``meos-rs`` selects a WKB
variant with ``WKB_EXTENDED`` / ``WKB_NDR`` / ``WKB_XDR``.

The IDL is generated, not committed; run ``python run.py`` first.
"""
import json
import unittest
from pathlib import Path

IDL = Path(__file__).resolve().parents[1] / "output" / "meos-idl.json"


class MacroTests(unittest.TestCase):
def setUp(self):
if not IDL.exists():
self.skipTest(f"{IDL} not generated; run `python run.py` first")
idl = json.loads(IDL.read_text())
self.macros = idl.get("macros", [])
self.by_name = {m["name"]: m for m in self.macros}

def test_macros_present(self):
self.assertTrue(self.macros, "no macros extracted")

def test_wkb_variant_flags(self):
# The variant flags meos-rs uses to select a WKB encoding.
for name, value in (("WKB_EXTENDED", 4), ("WKB_NDR", 8), ("WKB_XDR", 16)):
self.assertIn(name, self.by_name, f"{name} not extracted")
self.assertEqual(self.by_name[name]["value"], value)

def test_values_are_integers(self):
for m in self.macros:
self.assertIsInstance(m["value"], int, f"{m['name']} value not int")

def test_function_like_macros_excluded(self):
# Object-like integer macros only; a function-like macro (e.g. a
# ``#define FOO(x) ...``) must never leak in as a constant.
for m in self.macros:
self.assertNotIn("(", m["name"])

def test_each_macro_has_family_and_file(self):
for m in self.macros:
self.assertTrue(m.get("family"))
self.assertTrue(m.get("file", "").endswith(".h"))


if __name__ == "__main__":
unittest.main()
Loading