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")