diff --git a/parser/boundargs.py b/parser/boundargs.py index c2c047d..565f0d3 100644 --- a/parser/boundargs.py +++ b/parser/boundargs.py @@ -910,6 +910,24 @@ def _kernel_args(body: str, func: dict, meos_body: str | None) -> list[str | Non return None +def _expr_caller_index(body: str, expr: str) -> int | None: + """The SQL argument an expression handed to the kernel carries, the one argument the + locals it reads carry (#_caller_index): ``Raster_make`` passes + ``(const uint8_t *) VARDATA_ANY(pixels)``, which carries argument 0 through + ``pixels = PG_GETARG_BYTEA_PP(0)``. None for a bare local, which #_caller_index reads, + and where the locals carry no argument or several.""" + expr = _CAST.sub("", expr.strip()) + if _IDENT.match(expr): + return None + found = set() + for ident in set(re.findall(r"\b[a-z_]\w*\b", expr)): + if re.search(r"(?])" + re.escape(ident) + r"\s*=(?!=)", body): + k = _caller_index(body, ident) + if k is not None: + found.add(k) + return found.pop() if len(found) == 1 else None + + def _sql_arg_params(body: str, func: dict, meos_body: str | None = None) -> list[str] | None: """The C parameters of ``func`` in the order of the SQL arguments the wrapper ``body`` reads for them, or None when that is their C order or cannot be read: the call @@ -920,11 +938,17 @@ def _sql_arg_params(body: str, func: dict, meos_body: str | None = None) -> list if not args: return None params = func.get("params", []) + # The length of an input array travels with it, so it carries no argument of its own + lengths = {(a.get("lengthFrom") or {}).get("name") + for a in (func.get("shape") or {}).get("inputArrays") or []} by_k: dict[int, str] = {} for a, p in zip(args, params): - if a is None or a.strip().startswith("&") or _literal(a.strip()) is not None: + if a is None or a.strip().startswith("&") or _literal(a.strip()) is not None \ + or p["name"] in lengths: continue k = _caller_index(body, a) + if k is None: + k = _expr_caller_index(body, a) if k is not None: if k in by_k: return None diff --git a/tests/test_boundargs.py b/tests/test_boundargs.py index 89ba6cc..7b9847c 100644 --- a/tests/test_boundargs.py +++ b/tests/test_boundargs.py @@ -1093,6 +1093,18 @@ def test_the_literal_is_attached_then_stripped(self): return (int) nad_tnumber_tbox(temp, box); """ NAD_TINT_TBOX = {"name": "nad_tint_tbox", "params": [{"name": "temp"}, {"name": "box"}]} +# A wrapper reading a bytea first and passing it last, as an expression over its local. +BYTES_LAST = """ + bytea *pixels = PG_GETARG_BYTEA_PP(0); + int width = PG_GETARG_INT32(1); + char *pixeltype = text_to_cstring(PG_GETARG_TEXT_PP(3)); + Raster *result = raster_make(width, PG_GETARG_INT32(2), pixeltype, + (const uint8_t *) VARDATA_ANY(pixels), (size_t) VARSIZE_ANY_EXHDR(pixels)); +""" +RASTER_MAKE = {"name": "raster_make", "params": [ + {"name": n} for n in ("width", "height", "pixeltype", "pixels", "size")], + "shape": {"inputArrays": [{"param": "pixels", + "lengthFrom": {"kind": "param", "name": "size"}}]}} class SqlArgParamsTests(unittest.TestCase): @@ -1131,6 +1143,18 @@ def test_without_the_public_body_nothing_is_stated(self): """#test_a_shared_kernel_states_the_order without the body of the public function.""" self.assertIsNone(_sql_arg_params(KERNEL_WRAPPER, DISTANCE_SET_INT)) + def test_an_expression_argument_carries_what_its_local_carries(self): + """A bytea read first and passed last, as an expression over its local, with its + length beside it: the length travels with its array and carries no argument.""" + self.assertEqual(_sql_arg_params(BYTES_LAST, RASTER_MAKE), + ["pixels", "width", "height", "pixeltype"]) + + def test_an_expression_reading_two_arguments_carries_none(self): + """#test_an_expression_argument_carries_what_its_local_carries where the expression + reads two locals carrying different arguments, so the order cannot be read.""" + body = BYTES_LAST.replace("VARDATA_ANY(pixels)", "VARDATA_ANY(pixels) + width") + self.assertIsNone(_sql_arg_params(body, RASTER_MAKE)) + IDL = Path(__file__).resolve().parent.parent / "output" / "meos-idl.json"