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
19 changes: 19 additions & 0 deletions parser/nullable.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ def extract_nullable(meos_root: str | Path) -> dict[str, list[str]]:
return out


def extract_param_docs(meos_root: str | Path) -> dict[str, dict[str, str]]:
"""Return ``{function: {parameter: description}}`` from the MEOS C Doxygen, the
``@param`` text #extract_nullable reads, whitespace collapsed, over the same files."""
root = Path(meos_root)
out: dict[str, dict[str, str]] = {}
files = glob.glob(str(root / "src/**/*.c"), recursive=True)
files += glob.glob(str(root / "include/**/*.h"), recursive=True)
for f in files:
txt = Path(f).read_text(errors="ignore")
for m in _FUNC.finditer(txt):
docs = out.setdefault(m.group("name"), {})
for pm in _PARAM.finditer(m.group("doc")):
desc = " ".join(pm.group("desc").split())
for p in (n.strip() for n in pm.group("names").split(",")):
if p:
docs.setdefault(p, desc)
return out


def merge_nullable(idl: dict, meos_root: str | Path) -> tuple[dict, int]:
"""Fold the extracted nullability into each function's ``shape.nullable``."""
nul = extract_nullable(meos_root)
Expand Down
48 changes: 48 additions & 0 deletions parser/shapeinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"""
from __future__ import annotations

import re

# Parameters that accept NULL by MEOS convention regardless of the function.
# ``srs`` is the optional spatial-reference string of every ``*_as_*json`` /
Expand Down Expand Up @@ -164,6 +165,53 @@ def is_array(prm) -> bool:
return out


_ARRAY_DOC = re.compile(r"(an\s+)?array\b", re.I)
_COUNT_DOC = re.compile(r"number of elements\b", re.I)


def attach_struct_input_arrays(idl: dict, docs: dict) -> tuple[dict, int]:
"""(idl, number of arrays added) once ``shape.inputArrays`` also names each contiguous
array of structs a function reads.

A pointer to one struct and a pointer to the first of several are the same C type:
``spanset_make(Span *spans, int count)`` reads ``count`` spans, ``stbox_round(const
STBox *box, int maxdd)`` one box. The MEOS documentation tells them apart, which is
why this runs once the ``@param`` text is read (#extract_param_docs of
parser/nullable.py): a single-pointer parameter documented as an array (``Array of
spans``) followed by a by-value integer documented as its number of elements
(``Number of elements in the array``) is an input array with that length, as
#_input_arrays states an array of pointers or of scalars. An integer documented
otherwise (``Index`` in ``meos_array_get``) is not a length."""
n = 0
for func in idl["functions"]:
params = func.get("params", [])
fdocs = docs.get(func["name"]) or {}
shape = func.get("shape") or {}
known = {a["param"] for a in shape.get("inputArrays") or ()}
found = []
for prm, nxt in zip(params, params[1:]):
ctype = _bare(prm.get("cType"))
if (prm["name"] in known or not ctype.endswith("*") or ctype.endswith("**")
or _bare(nxt.get("cType")) not in _LENGTH_TYPES):
continue
if not (_ARRAY_DOC.match(fdocs.get(prm["name"], ""))
and _COUNT_DOC.match(fdocs.get(nxt["name"], ""))):
continue
found.append({
"param": prm["name"],
"lengthFrom": {"kind": "param", "name": nxt["name"]},
"element": {
"c": _strip_one_ptr(ctype),
"canonical": _strip_one_ptr(
_bare(prm.get("canonical") or prm.get("cType"))),
},
})
if found:
func.setdefault("shape", {}).setdefault("inputArrays", []).extend(found)
n += len(found)
return idl, n


def _is_index_pair_return(func: dict, count: str) -> bool:
"""Whether the ``int *`` return is a FLATTENED array of index PAIRS.

Expand Down
7 changes: 5 additions & 2 deletions parser/sqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,11 +667,14 @@ def _signature_fits(func, sig, sqlc):
"""Whether the arguments of `sig` fit the C input parameters of `func`, in their order
or in the one other order a wrapper may pass them in, as #_Resolver.params of
parser/compositions.py matches the operands of a call: True or False, or None when the
two counts differ (an array passed with its count, an argument left to its default).
The parameters `sig` binds and the out-parameters take no argument."""
two counts differ (an argument left to its default). The parameters `sig` binds, the
out-parameters and the length of each input array take no argument: a SQL array is
the C array together with its count (`shape.inputArrays`)."""
shape = func.get("shape") or {}
bound = sig.get("boundArgs") or shape.get("boundArgs") or {}
out = set(shape.get("outParams") or ())
out |= {(a.get("lengthFrom") or {}).get("name") for a in shape.get("inputArrays") or ()
if (a.get("lengthFrom") or {}).get("kind") == "param"}
inputs = [p for p in func.get("params") or ()
if p["name"] not in out and p["name"] not in bound]
if len(inputs) != len(sig["args"]):
Expand Down
7 changes: 5 additions & 2 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from parser.typerecover import (recover_collapsed_types, normalize_canonical,
postgres_scalar_names)
from parser.header_types import reconcile
from parser.shapeinfer import infer_shapes
from parser.nullable import merge_nullable
from parser.shapeinfer import infer_shapes, attach_struct_input_arrays
from parser.nullable import merge_nullable, extract_param_docs
from parser.nullresult import attach_null_result
from parser.outparam import extract_param_names, merge_outparams
from parser.boundargs import (attach_call_literals, merge_boundargs, merge_sql_arg_params,
Expand Down Expand Up @@ -159,6 +159,9 @@ def main():
idl, nn = merge_nullable(idl, _doxy_root)
print(f" nullable params from Doxygen `may be NULL`: {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}",
file=sys.stderr)
idl, no, out_drift = merge_outparams(idl, _doxy_root)
print(f" out params from Doxygen `@param[out]`: {no}", file=sys.stderr)
if out_drift:
Expand Down
21 changes: 21 additions & 0 deletions tests/test_claims_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ def test_a_signature_no_claimant_fits_stays_on_each(self):
_fn("two", "f", [("box", TB), ("a", "double")], sig)))
self.assertEqual(n, 0)

def test_an_array_length_takes_no_argument(self):
# As #test_a_bound_parameter_takes_no_argument for a bound parameter: the count
# shape.inputArrays names for an array takes no SQL argument, so Set_constructor's
# set(float[]) stays on floatset_make and cbufferset_make gives it up
def shape(element):
return {"inputArrays": [{"param": "values",
"lengthFrom": {"kind": "param", "name": "count"},
"element": {"c": element, "canonical": element}}]}
sig = [{"args": ["float[]"], "ret": "floatset"}]
idl = _idl(
_fn("floatset_make", "set", [("values", "const double *"), ("count", "int")],
sig, shape=shape("double")),
_fn("cbufferset_make", "set", [("values", "const Cbuffer *"), ("count", "int")],
sig, shape=shape("Cbuffer")))
idl["objectModel"]["classes"]["Cbuffer"] = {"cType": "Cbuffer *"}
idl, n = attach_claims_fit(idl)
fns = {f["name"]: f for f in idl["functions"]}
self.assertEqual(_args(fns["floatset_make"]), [("float[]",)])
self.assertEqual(fns["cbufferset_make"]["sqlSignatures"], [])
self.assertEqual(n, 1)

def test_an_internal_claimant_is_left_alone(self):
idl, n = attach_claims_fit(_idl(_kernel("tintbox_value_tiles", "int"),
_kernel("internal_tiles", "int64") | {"api": "internal"}))
Expand Down
34 changes: 33 additions & 1 deletion tests/test_shapeinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"""
import unittest

from parser.shapeinfer import infer_shapes
from parser.shapeinfer import infer_shapes, attach_struct_input_arrays


def _fn(name, ret, params):
Expand Down Expand Up @@ -234,5 +234,37 @@ def test_an_array_of_bare_meos_values_is_not_callee_allocated(self):
self.assertEqual(stats["outputArrays"], 1)


class StructInputArrayTests(unittest.TestCase):
"""#attach_struct_input_arrays reads a contiguous array of structs from the @param text."""

def test_a_documented_array_of_structs_is_an_input_array(self):
idl = {"functions": [_fn("spanset_make", "SpanSet *",
[("spans", "Span *"), ("count", "int")])]}
docs = {"spanset_make": {"spans": "Array of spans",
"count": "Number of elements in the array"}}
idl, n = attach_struct_input_arrays(idl, docs)
self.assertEqual(n, 1)
self.assertEqual(idl["functions"][0]["shape"]["inputArrays"], [{
"param": "spans",
"lengthFrom": {"kind": "param", "name": "count"},
"element": {"c": "Span", "canonical": "Span"}}])

def test_one_struct_and_an_integer_is_no_array(self):
idl = {"functions": [_fn("stbox_round", "STBox *",
[("box", "const STBox *"), ("maxdd", "int")])]}
docs = {"stbox_round": {"box": "Spatiotemporal box",
"maxdd": "Maximum number of decimal digits"}}
idl, n = attach_struct_input_arrays(idl, docs)
self.assertEqual(n, 0)
self.assertNotIn("shape", idl["functions"][0])

def test_an_array_read_at_an_index_is_no_input_array(self):
idl = {"functions": [_fn("meos_array_get", "void *",
[("array", "MeosArray *"), ("n", "int")])]}
docs = {"meos_array_get": {"array": "Array", "n": "Index"}}
idl, n = attach_struct_input_arrays(idl, docs)
self.assertEqual(n, 0)


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