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

# 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
7 changes: 7 additions & 0 deletions docs/enrichment.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ literals are in the catalog.
`encoderAux`, `readerAux` and `writerAux` (and `in_aux` / `out_aux`,
`bytes.encoderAux`). A binding builds each call from the function's own parameters,
filling each trailing input by name, and refuses one the catalog does not fill.
- **Default of a trailing input** — the literal MEOS's own calls pass for a
parameter of its name, where every call passing a literal passes the same one
(`parser/boundargs.py` `extract_call_literals`): every literal `typmod` is -1,
PostgreSQL's unspecified type modifier, so `interval_in`, `pg_timetz_in` and
`numeric_in` read with -1. A name passed two literals (`srid`, `maxdd`) or one
macro takes the formatting default: `maxdd` and `precision` 15, flags 0, `srs`
NULL.
- **`variant`** of a WKB writer — the value the type's own `send` binds
(`WKB_EXTENDED`, 4, keeping the SRID), else the value its SQL hex writer passes when
the byte order is left out (`asHexWKB(raster, endian DEFAULT '')` passes 0).
Expand Down
57 changes: 57 additions & 0 deletions parser/boundargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,3 +587,60 @@ def resolve_bound_names(idl: dict, include_root: str | Path) -> tuple[dict, int,
"vendored": False, "value": val})
n += 1
return idl, n, unresolved


# A C comment, block or line.
_COMMENT = re.compile(r"/\*.*?\*/|//[^\n]*", re.S)


def extract_call_literals(meos_src: str | Path, functions: list) -> dict[str, str]:
"""``{parameter name: literal}`` for every parameter name to which MEOS's own calls of
catalog functions pass one literal alone, read argument by argument as
#_wrapper_bound reads a wrapper's call.

``geom_in(wkt, -1)``, ``pg_timestamptz_in(str, -1)`` and every other call passing a
literal for a ``typmod`` pass ``-1``, PostgreSQL's unspecified type modifier, so
``typmod`` reads ``-1``. A name receiving two literals (``srid`` is passed ``0`` and
``SRID_UNKNOWN``) states none; an argument that is not a literal is not counted."""
params = {f["name"]: [p["name"] for p in f.get("params", [])] for f in functions}
seen: dict[str, set[str]] = {}
for path in sorted(Path(meos_src).rglob("*.c")):
text = _COMMENT.sub(" ", path.read_text(errors="ignore"))
for m in _CALLEE.finditer(text):
names = params.get(m.group("name"))
if not names:
continue
start, depth = m.end() - 1, 0
for i in range(start, len(text)):
depth += (text[i] == "(") - (text[i] == ")")
if depth == 0:
break
if re.match(r"\s*\{", text[i + 1:]):
continue # the definition, not a call
for name, arg in zip(names, _split_args(text[start + 1:i])):
lit = _literal(arg)
if lit is not None:
seen.setdefault(name, set()).add(lit)
return {name: next(iter(lits)) for name, lits in seen.items() if len(lits) == 1}


def attach_call_literals(idl: dict, meos_src: str | Path) -> tuple[dict, int]:
"""Set ``_callLiteral`` on every parameter whose name #extract_call_literals reads one
literal for. The key is private: #_aux_specs of parser/enrich.py reads it as a trailing
input's default and run.py removes it before writing the catalog."""
lits = extract_call_literals(meos_src, idl.get("functions", []))
n = 0
for f in idl.get("functions", []):
for p in f.get("params", []):
if p["name"] in lits:
p["_callLiteral"] = lits[p["name"]]
n += 1
return idl, n


def strip_call_literals(idl: dict) -> dict:
"""Remove the private ``_callLiteral`` #attach_call_literals sets."""
for f in idl.get("functions", []):
for p in f.get("params", []):
p.pop("_callLiteral", None)
return idl
27 changes: 26 additions & 1 deletion parser/enrich.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,28 @@ def _is_scalar_pointer(c_type: str, enums: set) -> bool:
return False


# No literal of the parameter's kind: #_call_default answers this rather than None, a value.
_UNSTATED = object()


def _call_default(literal, kind: str):
"""The value of ``literal``, the ``_callLiteral`` #attach_call_literals of
parser/boundargs.py sets, as a ``kind`` (``integer``, ``number``, ``boolean`` or
``string``) default: ``-1`` reads -1, ``NULL`` None for a string; a macro name or a
literal of another kind is #_UNSTATED."""
if literal is None:
return _UNSTATED
if kind == "integer" and re.fullmatch(r"-?\d+", literal):
return int(literal)
if kind == "number" and re.fullmatch(r"-?\d+(?:\.\d+)?", literal):
return float(literal)
if kind == "boolean" and literal in ("true", "false"):
return literal == "true"
if kind == "string" and literal == "NULL":
return None
return _UNSTATED


def _aux_specs(params: list):
"""Defaults for the trailing args of an in/out helper.

Expand All @@ -168,7 +190,10 @@ def _aux_specs(params: list):
if "type" in nm: # temptype/basetype/settype tag
return None
j = sc["json"]
if j == "integer":
passed = _call_default(p.get("_callLiteral"), j)
if passed is not _UNSTATED:
default = passed
elif j == "integer":
default = (15 if any(k in nm for k in
("maxdd", "decimal", "digit", "precision"))
else 0)
Expand Down
9 changes: 8 additions & 1 deletion run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
from parser.nullable import merge_nullable
from parser.nullresult import attach_null_result
from parser.outparam import extract_param_names, merge_outparams
from parser.boundargs import merge_boundargs, resolve_bound_names
from parser.boundargs import (attach_call_literals, merge_boundargs, resolve_bound_names,
strip_call_literals)
from parser.compositions import attach_compositions
from parser.enrich import enrich_idl
from parser.codecs import state_type_encodings
Expand Down Expand Up @@ -172,6 +173,11 @@ def main():
_grp_root / "pgtypes")
print(f" attached {ngrp} doxygen @ingroup groups", file=sys.stderr)

# A trailing input's default is the literal MEOS's own calls pass for a parameter
# of its name, where they pass one alone (typmod reads -1).
idl, ncall = attach_call_literals(idl, _grp_root / "meos" / "src")
print(f" parameters MEOS's calls pass one literal: {ncall}", file=sys.stderr)

# 1f. Derive service-projection metadata (category / encodings / network).
# Runs before the merge so manual annotations override the heuristics.
idl = enrich_idl(idl)
Expand Down Expand Up @@ -377,6 +383,7 @@ def main():
idl["families"] = list(all_families())
print(f" families = {', '.join(idl['families'])}", file=sys.stderr)

idl = strip_call_literals(idl)
idl_path = OUTPUT_DIR / "meos-idl.json"
with open(idl_path, "w") as f:
json.dump(idl, f, indent=2)
Expand Down
47 changes: 46 additions & 1 deletion tests/test_boundargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
import unittest
from pathlib import Path

from parser.boundargs import extract_wrappers, merge_boundargs, resolve_bound_names
from parser.boundargs import (attach_call_literals, extract_call_literals, extract_wrappers,
merge_boundargs, resolve_bound_names, strip_call_literals)

# A synthetic MobilityDB wrapper source (mobilitydb/src/**/*.c shape).
SAMPLE = '''
Expand Down Expand Up @@ -816,5 +817,49 @@ def test_names_without_one_literal_stay_unresolved(self):
self.assertEqual(unresolved, ["GUARDED", "MISSING", "SHADOWED"])


class CallLiteralTests(unittest.TestCase):
"""A parameter name MEOS's own calls pass one literal alone reads that literal."""

def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
(Path(self.tmp.name) / "type_in.c").write_text(
"GSERIALIZED *\n"
"geom_in(const char *str, int32 typmod)\n"
"{\n return pg_geom_in(str, typmod);\n}\n"
"void f(const char *s, int32 tm)\n"
"{\n"
" /* geom_in(s, 7) in a comment is no call */\n"
" geom_in(s, -1);\n"
" geog_in(s, -1);\n"
" geog_in(s, tm);\n"
" geo_set_srid(g, 0);\n"
" geo_set_srid(g, SRID_UNKNOWN);\n"
"}\n")
self.functions = [
{"name": "geom_in", "params": [{"name": "str"}, {"name": "typmod"}]},
{"name": "geog_in", "params": [{"name": "str"}, {"name": "typmod"}]},
{"name": "geo_set_srid", "params": [{"name": "gs"}, {"name": "srid"}]}]

def tearDown(self):
self.tmp.cleanup()

def test_one_literal_alone_is_read(self):
# the definition's typmod, the variable tm and the commented call are no literals
lits = extract_call_literals(self.tmp.name, self.functions)
self.assertEqual(lits, {"typmod": "-1"})

def test_two_literals_state_none(self):
self.assertNotIn("srid", extract_call_literals(self.tmp.name, self.functions))

def test_the_literal_is_attached_then_stripped(self):
idl, n = attach_call_literals({"functions": self.functions}, self.tmp.name)
self.assertEqual(n, 2)
self.assertEqual(idl["functions"][0]["params"][1]["_callLiteral"], "-1")
self.assertNotIn("_callLiteral", idl["functions"][2]["params"][1])
strip_call_literals(idl)
self.assertFalse(any("_callLiteral" in p for f in idl["functions"]
for p in f["params"]))


if __name__ == "__main__":
unittest.main()
37 changes: 36 additions & 1 deletion tests/test_enrich.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
parameters), so the assertions double as a specification.
"""

import json
import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from parser.enrich import enrich_idl, classify_category, build_type_encodings
from parser.enrich import (_aux_specs, build_type_encodings, classify_category,
enrich_idl)


def fn(name, ret, *params):
Expand Down Expand Up @@ -267,6 +269,39 @@ def test_a_standard_integer_result_is_a_json_integer(self):
{"kind": "json", "json": "integer"})


class CallLiteralDefaultTests(unittest.TestCase):
"""A trailing input whose name MEOS's calls pass one literal for, as
#attach_call_literals of parser/boundargs.py reads it, defaults to that literal;
a macro name keeps the default #TypeEncodingTests states."""

def _aux(self, ctype, name, literal):
p = {"name": name, "cType": ctype, "canonical": ctype, "_callLiteral": literal}
return _aux_specs([p])[0]["default"]

def test_the_literal_is_the_default(self):
self.assertEqual(self._aux("int32_t", "typmod", "-1"), -1)
self.assertIsNone(self._aux("const char *", "srs", "NULL"))
self.assertIs(self._aux("bool", "with_bbox", "true"), True)

def test_a_macro_or_a_literal_of_another_kind_keeps_the_default(self):
self.assertEqual(self._aux("int", "maxdd", "OUT_DEFAULT_DECIMAL_DIGITS"), 15)
self.assertEqual(self._aux("int", "option", "NULL"), 0)


class CallLiteralCatalogTests(unittest.TestCase):
"""Over the generated catalog, the PostgreSQL readers taking a type modifier read
with -1, the modifier MEOS's own calls pass, as #CallLiteralDefaultTests states."""

def test_a_type_modifier_reads_minus_one(self):
idl_path = Path(__file__).resolve().parents[1] / "output" / "meos-idl.json"
if not idl_path.exists():
self.skipTest(f"{idl_path} not generated; run `python run.py` first")
te = json.loads(idl_path.read_text())["typeEncodings"]
for cls in ("Interval", "TimeTzADT", "NumericData"):
self.assertEqual(te[cls]["in_aux"],
[{"name": "typmod", "kind": "integer", "default": -1}], cls)


class ApiClassificationTests(unittest.TestCase):
def setUp(self):
self.fns = by_name(make_idl())
Expand Down
Loading