diff --git a/parser/boundargs.py b/parser/boundargs.py index dabf623..16d9bcb 100644 --- a/parser/boundargs.py +++ b/parser/boundargs.py @@ -535,6 +535,88 @@ def own(w, sig=None): return idl, n, list(dict.fromkeys(drift)) +# A local a wrapper sets from the class of a temporal type: `interp = +# temptype_supports_linear(temptype) ? LINEAR : STEP`. +_TYPE_CHOICE = r"(?]){var}\s*=(?!=)\s*temptype_supports_linear\s*\(\s*(\w+)\s*\)" \ + r"\s*\?\s*(\w+)\s*:\s*(\w+)\s*;" +# The temporal type of the call, read from its return or from argument i: +# `temptype = oid_meostype(get_fn_expr_rettype(fcinfo->flinfo))`. +_CALL_TYPE = r"(?]){var}\s*=(?!=)\s*oid_meostype\s*\(\s*get_fn_expr_(rettype|argtype)" \ + r"\s*\(\s*fcinfo\s*->\s*flinfo\s*(?:,\s*(\d+)\s*)?\)\s*\)" + + +def _type_choice(body: str, var: str) -> tuple[int | None, str, int | None, str, str] | None: + """``(k, source, i, linear, other)`` when local ``var`` of wrapper ``body`` is set to + ``temptype_supports_linear(t) ? linear : other`` and ``t`` is the temporal type of the + call's return (``source`` "ret") or of its argument ``i`` (``source`` "arg"); ``k`` is + the SQL argument the wrapper otherwise reads ``var`` from (#_caller_index), None when it + reads none.""" + m = re.search(_TYPE_CHOICE.format(var=re.escape(var)), body) + if not m: + return None + t = re.search(_CALL_TYPE.format(var=re.escape(m.group(1))), body) + if not t: + return None + k = _caller_index(body, var) + return (k, "ret" if t.group(1) == "rettype" else "arg", + int(t.group(2)) if t.group(2) else None, m.group(2), m.group(3)) + + +def attach_type_derived_args(idl: dict, mdb_src: str | Path, + sql_src: str | Path | None = None, + meos_src: str | Path | None = None) -> tuple[dict, int]: + """(idl, number of arguments bound) once each SQL signature carries in ``boundArgs`` the + value a wrapper derives from the temporal type of the signature. + + A wrapper that reads no interpolation from the call takes the one the type supports, + ``interp = temptype_supports_linear(temptype) ? LINEAR : STEP`` with ``temptype`` the + type the function returns (``Tsequenceset_constructor_gaps``) or the type of one of + its arguments (``Temporal_append_tinstant``), and reads argument ``k`` instead only + when the call carries it. A signature stating at most ``k`` arguments therefore passes + LINEAR or STEP as fixed as a literal, the class #attach_temporal_types states for its + temporal type (``linear``, read from ``temptype_supports_linear``), so it is bound on + the signature as #merge_boundargs binds a literal the wrapper starts a local from. Runs + once the temporal types are attached; the wrappers are traced as #merge_boundargs + traces them.""" + from parser.sqlfn import _meos_to_mdb, _wrapper_sql_sigs + wrappers = extract_wrappers(mdb_src) + m2d = _meos_to_mdb(meos_src) if meos_src else {} + w2sig = _wrapper_sql_sigs(sql_src) if sql_src else {} + linear = {t: rec.get("linear") for t, rec in (idl.get("temporalTypes") or {}).items()} + n = 0 + for func in idl["functions"]: + primary = func.get("mdbC") + if not primary: + continue + ws = [primary] + [w for w in m2d.get(func["name"]) or () if w != primary] + params = [p.get("name") for p in func.get("params", [])] + for sig in func.get("sqlSignatures") or []: + w = _signature_wrapper(func, sig, ws, w2sig) or ws[0] + body = wrappers.get(w) + if not body: + continue + args = _call_args(body, func["name"]) or _call_args(body, "pg_" + func["name"]) + for pos, a in enumerate(args or ()): + if pos >= len(params) or not _IDENT.match(a): + continue + choice = _type_choice(body, a) + if choice is None: + continue + k, source, i, lin, other = choice + sargs = sig.get("args") or [] + if k is not None and len(sargs) > k: + continue + t = sig.get("ret") if source == "ret" else ( + sargs[i] if i is not None and i < len(sargs) else None) + if t not in linear: + continue + bound = sig.setdefault("boundArgs", {}) + if params[pos] not in bound: + bound[params[pos]] = lin if linear[t] else other + n += 1 + return idl, n + + # The SQL argument a wrapper reads: `PG_GETARG_(k)`, or a helper handed the call info # with the index, as `input_interp_string(fcinfo, 1)` reads argument 1. _GETARG = re.compile(r"PG_GETARG_\w+\s*\(\s*(\d+)\s*\)|\bfcinfo\s*,\s*(\d+)\s*\)") diff --git a/run.py b/run.py index 1cd0333..5ea6f09 100644 --- a/run.py +++ b/run.py @@ -15,7 +15,8 @@ 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, +from parser.boundargs import (attach_call_literals, attach_type_derived_args, merge_boundargs, + merge_sql_arg_params, resolve_bound_names, strip_call_literals) from parser.aggregates import attach_aggregates from parser.altsqlfn import attach_alt_sql_names @@ -380,6 +381,15 @@ def main(): # moment a family is added. idl = attach_temporal_types(idl, MOBILITYDB_SRC) + # State on each SQL signature the value a wrapper derives from its temporal type where + # the call carries none, the interpolation `temptype_supports_linear(temptype) ? LINEAR : + # STEP` of appendInstant and the sequence set constructors: it reads the classes the + # temporal types above state. + if MDB_SRC.exists(): + idl, ntd = attach_type_derived_args(idl, MDB_SRC, sql_src=SQL_SRC, meos_src=MEOS_SRC) + print(f" arguments a wrapper derives from the signature's temporal type: {ntd}", + file=sys.stderr) + # Keep a SQL signature two public functions claim on the one whose C parameters it # fits, matched by type once the object model and the type relations state the C type # of each SQL type. diff --git a/tests/test_boundargs.py b/tests/test_boundargs.py index 1f17495..25da7f5 100644 --- a/tests/test_boundargs.py +++ b/tests/test_boundargs.py @@ -12,7 +12,8 @@ import unittest from pathlib import Path -from parser.boundargs import (_sql_arg_params, attach_call_literals, extract_call_literals, +from parser.boundargs import (_sql_arg_params, attach_call_literals, attach_type_derived_args, + extract_call_literals, extract_wrappers, merge_boundargs, resolve_bound_names, strip_call_literals) @@ -1132,5 +1133,107 @@ def test_the_number_first_reads_through_the_shared_kernel(self): self.assertEqual([s.get("sqlArgParams") for s in sigs], [None, ["d", "temp"]]) +# The interpolation two wrappers take from the class of a temporal type, of the call's +# argument 1 and of its return, where the call carries none (Temporal_append_tinstant, +# Tsequenceset_constructor_gaps). +TYPE_DERIVED = ''' +PGDLLEXPORT Datum Temporal_append_tinstant(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(Temporal_append_tinstant); +Datum +Temporal_append_tinstant(PG_FUNCTION_ARGS) +{ + Temporal *temp = PG_GETARG_TEMPORAL_P(0); + TInstant *inst = PG_GETARG_TINSTANT_P(1); + interpType interp; + if (PG_NARGS() == 2 || PG_ARGISNULL(2)) + { + MeosType temptype = oid_meostype(get_fn_expr_argtype(fcinfo->flinfo, 1)); + interp = temptype_supports_linear(temptype) ? LINEAR : STEP; + } + else + interp = input_interp_string(fcinfo, 2); + Temporal *result = temporal_append_tinstant(temp, inst, interp, 0.0, NULL, false); + PG_RETURN_TEMPORAL_P(result); +} + +PGDLLEXPORT Datum Tsequenceset_constructor_gaps(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(Tsequenceset_constructor_gaps); +Datum +Tsequenceset_constructor_gaps(PG_FUNCTION_ARGS) +{ + ArrayType *array = PG_GETARG_ARRAYTYPE_P(0); + double maxdist = -1.0; + Interval *maxt = NULL; + MeosType temptype = oid_meostype(get_fn_expr_rettype(fcinfo->flinfo)); + interpType interp = temptype_supports_linear(temptype) ? LINEAR : STEP; + if (PG_NARGS() > 1 && ! PG_ARGISNULL(1)) + maxt = PG_GETARG_INTERVAL_P(1); + if (PG_NARGS() > 2 && ! PG_ARGISNULL(2)) + maxdist = PG_GETARG_FLOAT8(2); + if (PG_NARGS() > 3 && ! PG_ARGISNULL(3)) + interp = input_interp_string(fcinfo, 3); + int count; + TInstant **instants = (TInstant **) temparr_extract(array, &count); + TSequenceSet *result = tsequenceset_make_gaps(instants, count, interp, maxt, maxdist); + PG_RETURN_TSEQUENCESET_P(result); +} +''' + + +class TypeDerivedArgTests(unittest.TestCase): + """#attach_type_derived_args binds the interpolation a wrapper derives from the + temporal type of a signature, as #DelegatingWrapperTests reads a literal behind a + helper, only on the signatures that do not carry it.""" + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + src = Path(self.tmp.name) / "src" + src.mkdir() + (src / "temporal.c").write_text(TYPE_DERIVED) + + def tearDown(self): + self.tmp.cleanup() + + @staticmethod + def _idl(): + def sig(name, args, ret): + return {"sqlName": name, "args": args, "ret": ret} + return { + "temporalTypes": {"tfloat": {"linear": True}, "tint": {"linear": False}}, + "functions": [ + {"name": "temporal_append_tinstant", "mdbC": "Temporal_append_tinstant", + "params": [{"name": n} for n in + ("temp", "inst", "interp", "maxdist", "maxt", "expand")], + "sqlSignatures": [sig("appendInstant", ["tfloat", "tfloat"], "tfloat"), + sig("appendInstant", ["tint", "tint"], "tint"), + sig("appendInstant", ["tfloat", "tfloat", "text"], + "tfloat")]}, + {"name": "tsequenceset_make_gaps", "mdbC": "Tsequenceset_constructor_gaps", + "params": [{"name": n} for n in + ("instants", "count", "interp", "maxt", "maxdist")], + "sqlSignatures": [sig("tintSeqSetGaps", ["tint[]", "interval", "float"], + "tint"), + sig("tfloatSeqSetGaps", + ["tfloat[]", "interval", "float", "text"], + "tfloat")]}]} + + def test_the_class_of_the_argument_type_is_bound(self): + idl, n = attach_type_derived_args(self._idl(), self.tmp.name) + sigs = idl["functions"][0]["sqlSignatures"] + self.assertEqual(sigs[0]["boundArgs"], {"interp": "LINEAR"}) + self.assertEqual(sigs[1]["boundArgs"], {"interp": "STEP"}) + + def test_the_class_of_the_return_type_is_bound(self): + idl, _ = attach_type_derived_args(self._idl(), self.tmp.name) + self.assertEqual(idl["functions"][1]["sqlSignatures"][0]["boundArgs"], + {"interp": "STEP"}) + + def test_a_signature_carrying_the_interpolation_binds_none(self): + idl, n = attach_type_derived_args(self._idl(), self.tmp.name) + self.assertNotIn("boundArgs", idl["functions"][0]["sqlSignatures"][2]) + self.assertNotIn("boundArgs", idl["functions"][1]["sqlSignatures"][1]) + self.assertEqual(n, 3) + + if __name__ == "__main__": unittest.main()