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 314
run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 315

# 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
22 changes: 19 additions & 3 deletions parser/shapeinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,26 @@ def _out_count_param(func: dict) -> str | None:


def _is_written_back_array(p: dict) -> bool:
"""A non-const double (or higher) pointer parameter the callee allocates
and writes back, i.e. a parallel output array."""
"""A non-const pointer parameter the callee ALLOCATES and writes back, i.e.
a parallel output array.

The callee writes ``*p = <the array>``, and an array of ``E`` is spelled
``E *``, so such a parameter is spelled ``E **`` — one pointer level for the
array and one for the write-back. Stripping both leaves ``E``, and ``E`` is
what says whether the callee can have allocated the array at all: a binding
reads an array of by-value scalars (``TimestampTz **bins`` -> ``TimestampTz``)
or an array of pointers (``SpanSet ***result`` -> ``SpanSet *``), and nothing
else. A MEOS value type left bare — ``Jsonb **values`` -> ``Jsonb`` — is
neither: MEOS holds such a value by reference, never by value in an array,
so that parameter is an array the CALLER allocates and the callee only
fills. Reading it as callee-allocated makes every binding hand the callee
one element's worth of storage and take the first element it writes for the
address of the array."""
ct = p.get("cType", "")
return "**" in ct and not ct.lstrip().startswith("const")
if "**" not in ct or ct.lstrip().startswith("const"):
return False
element = _strip_one_ptr(_strip_one_ptr(ct))
return element.endswith("*") or _bare(element) in _ELEMENT_SCALARS


def _strip_one_ptr(ctype: str) -> str:
Expand Down
30 changes: 25 additions & 5 deletions tests/test_shapeinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,37 @@ def test_a_value_beside_a_number_is_not_an_array(self):
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.
# `temporal_time_split(temp, ..., TimestampTz **bins, int *count)` writes
# `bins` back, and its length being BY POINTER is what says so.
idl = {"functions": [_fn(
"jsonb_each", "text **",
[("jb", "const Jsonb *"), ("values", "Jsonb **"),
"temporal_time_split", "Temporal **",
[("temp", "const Temporal *"), ("bins", "TimestampTz **"),
("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"}])
self.assertEqual(shape["outputArrays"], [{"param": "bins"}])

def test_an_array_of_bare_meos_values_is_not_callee_allocated(self):
# `jsonb_each(jb, Jsonb **values, int *count)` fills an array the CALLER
# allocates: stripping the write-back and the array levels leaves `Jsonb`,
# a value MEOS holds by reference, so `values` cannot be the address of an
# array MEOS made. Its callee-allocated sibling spells the same element
# `SpanSet ***periods`. A binding reading this one as callee-allocated
# hands MEOS one element's worth of storage for `count` of them.
idl = {"functions": [_fn(
"jsonb_each", "text **",
[("jb", "const Jsonb *"), ("values", "Jsonb **"),
("count", "int *")]),
_fn("tdwithin_tgeoarr_tgeoarr", "int *",
[("arr1", "const Temporal **"), ("count1", "int"),
("periods", "SpanSet ***"), ("count", "int *")])]}
idl, stats = infer_shapes(idl)
self.assertNotIn("outputArrays", idl["functions"][0]["shape"])
self.assertEqual(idl["functions"][1]["shape"]["outputArrays"],
[{"param": "periods"}])
self.assertEqual(stats["outputArrays"], 1)


if __name__ == "__main__":
Expand Down
Loading