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
36 changes: 33 additions & 3 deletions parser/shapeinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
int *count)`` (out) versus ``tsequence_make(const TInstant **instants, int
count, ...)`` (in).

Alongside ``lengthFrom`` the primary array return also carries its ``element``
type — the return with exactly one pointer level stripped, resolved as a
first-class ``{c, canonical}`` object (mirroring ``returnType``/``params``). A
binding thus composes its native list/array over ``element.canonical`` through
its EXISTING per-type marshaller, never re-parsing the return string — the
zero-heuristic collection boundary (see binding-is-thin-io-shell-over-meos).

This replaces the ``meta/meos-meta.json`` shape entries, which had drifted to a
3-function stub and silently mis-classified every out-array as an input
parameter, breaking the split / space-split / mvtgeom / normalize families in
Expand Down Expand Up @@ -47,6 +54,17 @@ def _is_written_back_array(p: dict) -> bool:
return "**" in ct and not ct.lstrip().startswith("const")


def _strip_one_ptr(ctype: str) -> str:
"""Remove exactly one trailing ``*`` (with any surrounding space) — the
inverse of "an array of ``E`` is spelled ``E *``". ``double *`` -> ``double``
(a by-value element); ``struct TInstant **`` -> ``struct TInstant *`` (an
array of element pointers). Mechanical and canonical, NOT a heuristic."""
s = ctype.rstrip()
if s.endswith("*"):
s = s[:-1].rstrip()
return s


def infer_shapes(idl: dict) -> tuple[dict, dict]:
"""Populate ``func['shape']`` with ``arrayReturn``/``outputArrays`` derived
from the signatures. Returns ``(idl, stats)``. Idempotent and additive:
Expand All @@ -58,10 +76,22 @@ def infer_shapes(idl: dict) -> tuple[dict, dict]:
continue # not array-returning; nothing to infer
shape = func.setdefault("shape", {})
# The primary pointer return takes its length from the output count.
ret = func.get("returnType", {}).get("c", "")
rtype = func.get("returnType", {})
ret = rtype.get("c", "")
if ret.rstrip().endswith("*"):
shape.setdefault("arrayReturn", {})["lengthFrom"] = {
"kind": "param", "name": count}
ar = shape.setdefault("arrayReturn", {})
ar["lengthFrom"] = {"kind": "param", "name": count}
# Element type = the return with exactly one pointer level stripped,
# resolved canonically so every binding reads a first-class
# ``{c, canonical}`` type object (mirroring ``returnType``/``params``)
# and routes it through its EXISTING per-type marshaller — never
# re-parsing the return string. ``double *`` -> ``double`` (by-value);
# ``struct TInstant **`` -> ``struct TInstant *`` (array of pointers).
ar["element"] = {
"c": _strip_one_ptr(ret),
"canonical": _strip_one_ptr(
rtype.get("canonical", ret)),
}
n_arr += 1
# Parallel written-back out-arrays (``TYPE **extra`` alongside count).
out = [{"param": p["name"]} for p in func["params"]
Expand Down
18 changes: 17 additions & 1 deletion tests/test_shapeinfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
fills the length) -> ``outputArrays`` + ``arrayReturn.lengthFrom``
* a read-only in-array pairs with a by-value ``int count`` -> left untouched

Plain unittest, no pytest dependency; fully synthetic IDL (no generated file).
Plain unittest, no pytest dependency; fully synthetic IDL, no build artifacts.
"""
import unittest

Expand All @@ -33,8 +33,24 @@ def test_output_array_with_pointer_count(self):
self.assertEqual(sh["outputArrays"], [{"param": "time_bins"}])
self.assertEqual(sh["arrayReturn"]["lengthFrom"],
{"kind": "param", "name": "count"})
# element = return with one pointer level stripped (array of pointers)
self.assertEqual(sh["arrayReturn"]["element"],
{"c": "Temporal *", "canonical": "Temporal *"})
self.assertEqual(stats["outputArrays"], 1)

def test_scalar_array_return_element_is_by_value(self):
# floatset_values-style: ``double *`` return + by-pointer count. The
# element strips one pointer level to the by-value scalar ``double`` so a
# binding composes native-list-of-double with zero return-string parsing.
idl = {"functions": [_fn(
"floatset_values", "double *",
[("s", "const Set *"), ("count", "int *")])]}
idl, stats = infer_shapes(idl)
ar = idl["functions"][0]["shape"]["arrayReturn"]
self.assertEqual(ar["lengthFrom"], {"kind": "param", "name": "count"})
self.assertEqual(ar["element"], {"c": "double", "canonical": "double"})
self.assertEqual(stats["arrayReturn"], 1)

def test_two_parallel_output_arrays(self):
idl = {"functions": [_fn(
"tfloat_value_time_split", "Temporal **",
Expand Down
Loading