diff --git a/parser/nullable.py b/parser/nullable.py
index f50b952..90a5980 100644
--- a/parser/nullable.py
+++ b/parser/nullable.py
@@ -1,18 +1,17 @@
"""Extract per-parameter nullability from the MEOS C Doxygen as the SoT.
A MEOS function parameter accepts NULL iff its Doxygen ``@param`` entry carries
-the ``@nullable`` tag, e.g. ``@param[in] newjb New value @nullable``, or, in the
-sources not yet converted to the tag, says so in prose, e.g.
-``@param[in] srs Spatial reference system, may be `NULL```. The tag sits inside the
-entry of the parameter it qualifies, so renaming the parameter edits the line that
-carries both; on an output it states that a caller may pass NULL to skip it. This is the single source of truth the codegens consume — grounded in
+the ``@nullable`` tag, e.g. ``@param[in] newjb New value @nullable``. The tag sits
+inside the entry of the parameter it qualifies, so renaming the parameter edits the
+line that carries both; on an output it states that a caller may pass NULL to skip
+it. A description saying ``NULL`` in prose states nothing here: it describes a
+value written or another argument. This is the single source of truth the codegens consume — grounded in
the C code, keyed by parameter name, and cross-checked in MobilityDB against the PG
layer (a SQL function declared without ``STRICT`` + the wrapper's ``PG_ARGISNULL``
guards).
The extractor walks the MEOS sources, pairs each Doxygen block with the function
-it documents, and records the params whose description carries the tag or a NULL
-note. The result feeds ``shape.nullable`` in the IDL so every binding can guard the
+it documents, and records the params whose description carries the tag. The result feeds ``shape.nullable`` in the IDL so every binding can guard the
param.
"""
from __future__ import annotations
@@ -30,18 +29,10 @@
_PARAM = re.compile(
r'@param\[(?P
[^\]]*)\]\s+(?P\w+(?:\s*,\s*\w+)*)\s+(?P.*?)'
r'(?=\n\s*\*\s*@|\*/|\Z)', re.S)
-# The @nullable tag inside a @param description. A line opening with @nullable ends
-# the description before it, so the tag is read only where it qualifies one entry.
+# The @nullable tag inside a @param description, of an input or of an output a
+# caller may pass as NULL to skip it. A line opening with @nullable ends the
+# description before it, so the tag is read only where it qualifies one entry.
_TAG = re.compile(r'(? bool:
- """Whether an @param entry states that its parameter accepts NULL: the @nullable
- tag, on an input or on an output a caller may pass as NULL to skip it (the
- direction #extract_outparams reads from ``@param[out]``), or the prose note."""
- return bool(_TAG.search(desc) or _NULLISH.search(desc))
def extract_nullable(meos_root: str | Path) -> dict[str, list[str]]:
@@ -53,7 +44,7 @@ def extract_nullable(meos_root: str | Path) -> dict[str, list[str]]:
for m in _FUNC.finditer(txt):
name = m.group("name")
for pm in _PARAM.finditer(m.group("doc")):
- if not _is_nullable(pm.group("dir"), pm.group("desc")):
+ if not _TAG.search(pm.group("desc")):
continue
for p in (n.strip() for n in pm.group("names").split(",")):
out.setdefault(name, [])
diff --git a/run.py b/run.py
index 7fcaf74..d2b1a05 100644
--- a/run.py
+++ b/run.py
@@ -145,12 +145,12 @@ def main():
# 1d. Generate the codegen `shape` from the signatures + Doxygen, replacing
# the hand-maintained meta stub. inputArrays/outputArrays/arrayReturn
# come from the parameter forms; nullable comes from the C
- # `@param ... may be NULL` SoT.
+ # `@param ... @nullable` SoT.
idl, sh = infer_shapes(idl)
print(f" inferred shape: {sh['arrayReturn']} array returns, "
f"{sh['outputArrays']} output arrays, "
f"{sh['inputArrays']} input arrays", file=sys.stderr)
- # The `may be NULL` / `@param[out]` Doxygen tags live in the MEOS C *source*
+ # The `@nullable` / `@param[out]` Doxygen tags live in the MEOS C *source*
# (meos/src/**/*.c), not the parsed header tree. On the build-libmeos path
# HEADERS_DIR is the INSTALLED headers (generated meos_export.h, no src/), so
# scan from the source checkout (MDB_SRC_ROOT/meos) — the same root the
@@ -160,7 +160,7 @@ def main():
if os.environ.get("MDB_SRC_ROOT") else HEADERS_DIR.parent)
_doxy_root = _src_root if (_src_root / "src").is_dir() else HEADERS_DIR.parent
idl, nn = merge_nullable(idl, _doxy_root)
- print(f" nullable params from Doxygen `may be NULL`: {nn}",
+ print(f" nullable params from Doxygen `@nullable`: {nn}",
file=sys.stderr)
idl, nsa = attach_struct_input_arrays(idl, extract_param_docs(_doxy_root))
print(f" input arrays of structs from Doxygen `Array of`: {nsa}",
diff --git a/tests/test_nullable.py b/tests/test_nullable.py
index 76c237a..bfb37c7 100644
--- a/tests/test_nullable.py
+++ b/tests/test_nullable.py
@@ -1,9 +1,9 @@
"""Regression tests for parser/nullable.py.
Nullability is read from the C Doxygen (the source of truth), the `@nullable` tag
-inside a `@param` entry or the `@param ... may be NULL` note, and folded into each
-function's ``shape.nullable`` for the params that actually exist on the IDL
-function.
+inside a `@param` entry, and folded into each function's ``shape.nullable`` for
+the params that actually exist on the IDL function. A description naming NULL in
+prose states nothing.
Plain unittest, no pytest dependency; writes a tiny synthetic source tree.
"""
@@ -18,7 +18,7 @@
* @ingroup meos_temporal_inout
* @brief Return the MF-JSON representation
* @param[in] temp Temporal value
- * @param[in] srs Spatial reference system, may be `NULL`
+ * @param[in] srs Spatial reference system @nullable
*/
char *
temporal_as_mfjson(const Temporal *temp, char *srs)
@@ -29,7 +29,7 @@
/**
* @brief Append an instant
* @param[in] inst Instant
- * @param[in] maxt Maximum time interval, may be `NULL`
+ * @param[in] maxt Maximum time interval @nullable
* @param[in] interp Interpolation
*/
Temporal *
@@ -38,6 +38,17 @@
return NULL;
}
+/**
+ * @brief Dissolve the parts of a geometry
+ * @param[in] geom Geometry, may be `NULL`
+ * @param[out] dissolved The dissolved geometry, or NULL where nothing dissolves
+ */
+bool
+geom_dissolve(const LWGEOM *geom, LWGEOM **dissolved)
+{
+ return false;
+}
+
/**
* @brief Replace a value at a path
* @param[in] jb JSONB value
@@ -70,16 +81,22 @@ def setUp(self):
def tearDown(self):
self.tmp.cleanup()
- def test_extracts_only_may_be_null_params(self):
+ def test_extracts_only_tagged_params(self):
nul = extract_nullable(self.tmp.name)
self.assertEqual(nul["temporal_as_mfjson"], ["srs"])
self.assertEqual(nul["temporal_append_tinstant"], ["maxt"])
- # `temp`, `inst`, `interp` carry no NULL note -> not nullable
+ # `temp`, `inst`, `interp` carry no tag -> not nullable
self.assertNotIn("temp", nul["temporal_as_mfjson"])
+ def test_prose_naming_null_states_nothing(self):
+ """A description naming NULL without the tag, as
+ #test_extracts_only_tagged_params reads the tag."""
+ nul = extract_nullable(self.tmp.name)
+ self.assertNotIn("geom_dissolve", nul)
+
def test_reads_the_tag_inside_the_param_entry(self):
- """The @nullable tag, as #test_extracts_only_may_be_null_params reads the
- prose note."""
+ """The @nullable tag on its continuation line, behind a guard and on an
+ output, as #test_extracts_only_tagged_params reads it on one line."""
nul = extract_nullable(self.tmp.name)
# On the entry's line and on its continuation line, behind an `#if MEOS`
# guard, and on an output a caller may pass as NULL
@@ -89,7 +106,7 @@ def test_reads_the_tag_inside_the_param_entry(self):
self.assertNotIn("jb", nul["jsonb_set_lax"])
def test_param_docs_leave_out_the_tag(self):
- """The description #test_extracts_only_may_be_null_params reads, without the
+ """The description #test_extracts_only_tagged_params reads, without the
tag."""
docs = extract_param_docs(self.tmp.name)
self.assertEqual(docs["jsonb_set_lax"]["newjb"], "New value")