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
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
# carries, or a change to them is not exercised until after it merges.
# Consumers use the action; this repository owns the rules.
- name: Refuse a skip, and a suite that shrank
run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 300
run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 304

# The rules earn their place by refusing a log that carries what they
# name. Both fixtures are written here rather than tracked, and the
Expand Down
77 changes: 74 additions & 3 deletions parser/shapeinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
TYPE *f(..., int *count) -> returns an array of ``count``
TYPE **f(..., TYPE **extra, int *count) -> primary array return PLUS one
or more parallel out-arrays
f(..., TYPE **values, int count, ...) -> reads an array of ``count``

The output length is always passed *by pointer* (``int *count``); an *input*
array instead carries its length *by value* (``int count``). That pointer/value
Expand Down Expand Up @@ -65,6 +66,71 @@ def _strip_one_ptr(ctype: str) -> str:
return s


#: The C scalars an array of by-value elements is made of. A pointer to one of
#: these beside a length is an array; a pointer to a MEOS value type beside an
#: integer is a value and a number, as ``text_left(text *txt, int n)`` is.
#: ``char *`` is a string in every binding and never an array of characters.
_ELEMENT_SCALARS = frozenset({
"bool", "int8", "int8_t", "uint8", "uint8_t", "short", "int16", "int16_t",
"uint16", "uint16_t", "int", "int32", "int32_t", "uint32", "uint32_t",
"float", "Oid", "DateADT", "long", "int64", "int64_t", "uint64",
"uint64_t", "double", "float8", "Datum", "Timestamp", "TimestampTz",
"TimeADT", "TimeOffset", "size_t",
})

#: The by-value integer spellings a length is written in.
_LENGTH_TYPES = frozenset({
"int", "int32", "int32_t", "uint32", "uint32_t", "int64", "int64_t",
"uint64", "uint64_t", "size_t", "int16",
})


def _bare(ctype: str) -> str:
return " ".join((ctype or "").replace("const ", "").split())


def _input_arrays(func: dict) -> list:
"""The array ARGUMENTS a function reads, with the parameter each takes its
length from.

An input array is a parameter that is an array of pointers (``TYPE **``) or
of by-value scalars (``uint8_t *``, ``int64_t *``), immediately followed by
a by-value integer. That the length is by VALUE is what tells an argument
apart from a written-back out-array, whose length is by POINTER — the same
distinction this module already reads in the other direction.

Without it a binding matches the LENGTH PARAMETER'S NAME, and the names
disagree: ``count``, ``size``, ``ngeoms``, ``keys_len``, ``path_len``,
``pixels_size``, ``wkb_size``, ``count1``. Every one of them is a length,
and a binding that knows only some of them silently drops the rest.
"""
params = func.get("params", [])
out = []
for i, prm in enumerate(params[:-1]):
ctype = _bare(prm.get("cType"))
if _bare(params[i + 1].get("cType")) not in _LENGTH_TYPES:
continue
if ctype.endswith("**"):
if ctype in ("char **", "void **"):
continue
elif not (ctype.endswith("*")
and ctype[:-1].strip() in _ELEMENT_SCALARS):
continue
out.append({
"param": prm["name"],
"lengthFrom": {"kind": "param", "name": params[i + 1]["name"]},
# The element reads as the return's does — the type with one
# pointer level off and no `const`, which belongs to the argument
# rather than to the element type a binding marshals.
"element": {
"c": _strip_one_ptr(_bare(prm.get("cType"))),
"canonical": _strip_one_ptr(
_bare(prm.get("canonical") or prm.get("cType"))),
},
})
return out


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

Expand Down Expand Up @@ -98,11 +164,15 @@ def infer_shapes(idl: dict) -> tuple[dict, dict]:
"""Populate ``func['shape']`` with ``arrayReturn``/``outputArrays`` derived
from the signatures. Returns ``(idl, stats)``. Idempotent and additive:
only the array-output families are touched, everything else is untouched."""
n_arr = n_oa = 0
n_arr = n_oa = n_ia = 0
for func in idl["functions"]:
inputs = _input_arrays(func)
if inputs:
func.setdefault("shape", {})["inputArrays"] = inputs
n_ia += len(inputs)
count = _out_count_param(func)
if not count:
continue # not array-returning; nothing to infer
continue # not array-returning; nothing more to infer
shape = func.setdefault("shape", {})
# The primary pointer return takes its length from the output count.
rtype = func.get("returnType", {})
Expand Down Expand Up @@ -134,4 +204,5 @@ def infer_shapes(idl: dict) -> tuple[dict, dict]:
if out:
shape["outputArrays"] = out
n_oa += len(out)
return idl, {"arrayReturn": n_arr, "outputArrays": n_oa}
return idl, {"arrayReturn": n_arr, "outputArrays": n_oa,
"inputArrays": n_ia}
8 changes: 5 additions & 3 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,13 @@ def main():
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.
# the hand-maintained meta stub. inputArrays/outputArrays/arrayReturn
# come from the parameter forms; nullable comes from the C
# `@param ... may be NULL` SoT.
idl, sh = infer_shapes(idl)
print(f" inferred shape: {sh['arrayReturn']} array returns, "
f"{sh['outputArrays']} output arrays", file=sys.stderr)
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*
# (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
Expand Down
70 changes: 65 additions & 5 deletions tests/test_shapeinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

* a written-back out-array pairs with a by-pointer ``int *count`` (the callee
fills the length) -> ``outputArrays`` + ``arrayReturn.lengthFrom``
* a read-only in-array pairs with a by-value ``int count`` -> left untouched
* a read-only in-array pairs with a by-value ``int count`` -> ``inputArrays``

Plain unittest, no pytest dependency; fully synthetic IDL, no build artifacts.
"""
Expand Down Expand Up @@ -95,25 +95,85 @@ def test_index_pair_rule_needs_an_array_argument(self):
idl, _ = infer_shapes(idl)
self.assertNotIn("groupSize", idl["functions"][0]["shape"]["arrayReturn"])

def test_input_array_with_value_count_untouched(self):
def test_input_array_with_value_count_is_read_not_written(self):
# tsequence_make-style: ** input array carries its length BY VALUE
idl = {"functions": [_fn(
"tsequence_make", "TSequence *",
[("instants", "const TInstant **"), ("count", "int"),
("lower_inc", "bool")])]}
idl, stats = infer_shapes(idl)
self.assertNotIn("shape", idl["functions"][0])
shape = idl["functions"][0]["shape"]
self.assertEqual(stats["outputArrays"], 0)
self.assertNotIn("outputArrays", shape)
self.assertEqual(shape["inputArrays"], [{
"param": "instants",
"lengthFrom": {"kind": "param", "name": "count"},
"element": {"c": "TInstant *", "canonical": "TInstant *"}}])

def test_nonconst_input_array_with_value_count_untouched(self):
def test_nonconst_input_array_with_value_count_is_read_not_written(self):
# tsequenceset_make_gaps-style: non-const ** but BY-VALUE count => input
idl = {"functions": [_fn(
"tsequenceset_make_gaps", "TSequenceSet *",
[("instants", "TInstant **"), ("count", "int"),
("maxt", "const Interval *")])]}
idl, stats = infer_shapes(idl)
self.assertEqual(stats["outputArrays"], 0)
self.assertNotIn("shape", idl["functions"][0])
self.assertEqual(
idl["functions"][0]["shape"]["inputArrays"][0]["param"], "instants")

def test_a_length_is_read_by_position_not_by_its_name(self):
# The names disagree across the surface — `ngeoms`, `keys_len`, `size`
# — and each is the length of the array before it.
idl = {"functions": [
_fn("geo_cluster_kmeans", "int *",
[("geoms", "const GSERIALIZED **"), ("ngeoms", "uint32_t"),
("k", "uint32_t"), ("count", "int *")]),
_fn("jsonb_delete_array", "Jsonb *",
[("jb", "const Jsonb *"), ("keys_elems", "text **"),
("keys_len", "int")]),
_fn("set_from_wkb", "Set *",
[("wkb", "const uint8_t *"), ("size", "size_t")]),
]}
idl, stats = infer_shapes(idl)
lengths = {f["name"]: f["shape"]["inputArrays"][0]["lengthFrom"]["name"]
for f in idl["functions"]}
self.assertEqual(lengths, {"geo_cluster_kmeans": "ngeoms",
"jsonb_delete_array": "keys_len",
"set_from_wkb": "size"})
self.assertEqual(stats["inputArrays"], 3)
# The byte buffer's element is the scalar itself, not a pointer to one.
self.assertEqual(
idl["functions"][2]["shape"]["inputArrays"][0]["element"]["c"],
"uint8_t")

def test_a_value_beside_a_number_is_not_an_array(self):
# `text_left(text *txt, int n)` takes ONE text and a character count;
# a pointer to a MEOS value type beside an integer says nothing about
# an array, and only a pointer to a C scalar does.
idl = {"functions": [
_fn("text_left", "text *", [("txt", "text *"), ("n", "int")]),
_fn("jsonb_hash_extended", "uint64_t",
[("jb", "const Jsonb *"), ("seed", "uint64_t")]),
_fn("interval_in", "Interval *",
[("str", "const char *"), ("typmod", "int32")]),
]}
idl, stats = infer_shapes(idl)
self.assertEqual(stats["inputArrays"], 0)
for f in idl["functions"]:
self.assertNotIn("inputArrays", f.get("shape", {}))

def test_an_out_array_is_not_read_as_an_input_one(self):
# `jsonb_each(jb, Jsonb **values, int *count)` writes `values` back, and
# its length being BY POINTER is what says so.
idl = {"functions": [_fn(
"jsonb_each", "text **",
[("jb", "const Jsonb *"), ("values", "Jsonb **"),
("count", "int *")])]}
idl, stats = infer_shapes(idl)
shape = idl["functions"][0]["shape"]
self.assertEqual(stats["inputArrays"], 0)
self.assertNotIn("inputArrays", shape)
self.assertEqual(shape["outputArrays"], [{"param": "values"}])


if __name__ == "__main__":
Expand Down
Loading