tinycwrap 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinycwrap
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Lightweight C-to-Python wrapper generator using CFFI and NumPy
5
5
  Author: TinyCWrap Contributors
6
6
  Requires-Python: >=3.9
@@ -28,9 +28,7 @@ Write a small C file (only non-`static` functions are exported):
28
28
  ```c
29
29
  /* kernels.c */
30
30
  double dot(const double *x, const double *y, int len_x)
31
- /* Return dot product between x and y
32
- Contract: len_x=len(x);
33
- */
31
+ /* Return dot product between x and y */
34
32
  {
35
33
  double acc = 0.0;
36
34
  for (int i = 0; i < len_x; ++i)
@@ -50,7 +48,7 @@ cm = CModule("kernels.c") # builds the shared library, creates wrappers
50
48
  x = np.arange(5, dtype=np.float64)
51
49
  y = np.ones_like(x)
52
50
 
53
- cm.dot(x, y) # -> 10.0, len_x auto-filled by the contract
51
+ cm.dot(x, y) # -> 10.0, len_x auto-filled by convention
54
52
  print(cm.dot.__doc__) # docstring comes from the C comment
55
53
  ```
56
54
 
@@ -65,7 +63,7 @@ TinyCWrap infers how to build wrappers from simple C conventions:
65
63
  - `T *arg` -> output/in-place array (prefer naming it `out_*` when possible).
66
64
  - `T arg[N]` in the signature -> fixed-size array (input if `const`, otherwise output).
67
65
  - Plain scalars (`double`, `int`, ...) -> Python scalars.
68
- - **Length parameters**: integers such as `len_x`, `n`, `size_x` can be auto-filled if you declare a contract (see below). Otherwise pass them explicitly from Python.
66
+ - **Length parameters**: integers named like `len_x`, `n_x`, or `size_x` are auto-filled from array `x` by convention. Otherwise pass them explicitly from Python or declare a contract (see below).
69
67
  - **Docstrings**: the block comment immediately after the function header becomes the Python docstring.
70
68
  - **Structs**: `typedef struct { ... } Name;` definitions in your headers/sources become Python classes with a `.dtype` and NumPy-backed storage.
71
69
  - **Compilation**: extra sources can be passed (`CModule("main.c", "helper.c")`), and extra include dirs via `include_dirs=[...]`. Default compiler flags are `-O3 -shared -fPIC -march=native -mtune=native` plus the NumPy include path.
@@ -79,6 +77,15 @@ Contracts are declared inside the doc comment with `Contract:` (or `Contracts:`)
79
77
  - `len(out)=len_x` — allocate a 1D output array.
80
78
  - `postlen(out)=out_len` — slice the output after the call using an integer pointer result `out_len` (useful when the C code writes less than the allocated length).
81
79
  - `own(return)` — the returned pointer is owned by the caller; the wrapper will `free` it after copying to NumPy (requires a `len(return)=...` contract).
80
+ - `expose(len_x)` — opt out of naming-convention inference for `len_x`, keeping it as a required Python argument.
81
+
82
+ TinyCWrap also infers low-risk contracts from names:
83
+
84
+ - `len_x`, `n_x`, or `size_x` are treated as `len(x)` when `x` is an array argument.
85
+ - `out_x` is allocated with `shape(x)` when input array `x` exists.
86
+ - `out_x` is allocated with `len_x`, `n_x`, or `size_x` when no input `x` exists but such a length argument does.
87
+
88
+ Explicit contracts override inferred contracts.
82
89
 
83
90
  Examples pulled from the test suite:
84
91
 
@@ -109,9 +116,9 @@ Rules are parsed case-insensitively; whitespace does not matter.
109
116
 
110
117
  ## Python wrapper behavior
111
118
 
112
- - **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
119
+ - **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, naming conventions, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
113
120
  - **Struct pointer outputs**: non-const struct pointers are treated as in/out; when you pass an object/array explicitly, it is mutated in place and not returned. When you pass `None`, TinyCWrap allocates and returns the struct (or struct array).
114
- - **Optional length arguments**: integer length parameters inferred from contracts default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the expression from the contract is evaluated.
121
+ - **Optional length arguments**: integer length parameters inferred from contracts or naming conventions default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the inferred expression is evaluated.
115
122
  - **Post contracts**: when a contract uses `postlen(...)` or `post shape(...)`, the wrapper slices/reshapes outputs after the C call using the values written by the C function.
116
123
  - **Scalar pointer outputs**: pointers to integer-like types (e.g., `int *out_len`) are returned as plain Python integers alongside other outputs.
117
124
  - **Structs**: for `typedef struct` declarations TinyCWrap generates a Python class:
@@ -174,7 +181,7 @@ arr, n = cm.alloc_random_array() # returns NumPy array, frees the C buffer
174
181
  ## Tips
175
182
 
176
183
  - Keep function declarations (or headers) visible at the top level; TinyCWrap scans the C files and headers you pass.
177
- - If a length cannot be inferred from a contract, you must pass it explicitly from Python.
184
+ - If a length cannot be inferred from a naming convention or contract, you must pass it explicitly from Python.
178
185
  - Use `cm.<func>.__source__` to inspect the wrapper code if something behaves unexpectedly.
179
186
  - For debugging parsed signatures/contracts call `cm._debug_specs()`.
180
187
 
@@ -18,9 +18,7 @@ Write a small C file (only non-`static` functions are exported):
18
18
  ```c
19
19
  /* kernels.c */
20
20
  double dot(const double *x, const double *y, int len_x)
21
- /* Return dot product between x and y
22
- Contract: len_x=len(x);
23
- */
21
+ /* Return dot product between x and y */
24
22
  {
25
23
  double acc = 0.0;
26
24
  for (int i = 0; i < len_x; ++i)
@@ -40,7 +38,7 @@ cm = CModule("kernels.c") # builds the shared library, creates wrappers
40
38
  x = np.arange(5, dtype=np.float64)
41
39
  y = np.ones_like(x)
42
40
 
43
- cm.dot(x, y) # -> 10.0, len_x auto-filled by the contract
41
+ cm.dot(x, y) # -> 10.0, len_x auto-filled by convention
44
42
  print(cm.dot.__doc__) # docstring comes from the C comment
45
43
  ```
46
44
 
@@ -55,7 +53,7 @@ TinyCWrap infers how to build wrappers from simple C conventions:
55
53
  - `T *arg` -> output/in-place array (prefer naming it `out_*` when possible).
56
54
  - `T arg[N]` in the signature -> fixed-size array (input if `const`, otherwise output).
57
55
  - Plain scalars (`double`, `int`, ...) -> Python scalars.
58
- - **Length parameters**: integers such as `len_x`, `n`, `size_x` can be auto-filled if you declare a contract (see below). Otherwise pass them explicitly from Python.
56
+ - **Length parameters**: integers named like `len_x`, `n_x`, or `size_x` are auto-filled from array `x` by convention. Otherwise pass them explicitly from Python or declare a contract (see below).
59
57
  - **Docstrings**: the block comment immediately after the function header becomes the Python docstring.
60
58
  - **Structs**: `typedef struct { ... } Name;` definitions in your headers/sources become Python classes with a `.dtype` and NumPy-backed storage.
61
59
  - **Compilation**: extra sources can be passed (`CModule("main.c", "helper.c")`), and extra include dirs via `include_dirs=[...]`. Default compiler flags are `-O3 -shared -fPIC -march=native -mtune=native` plus the NumPy include path.
@@ -69,6 +67,15 @@ Contracts are declared inside the doc comment with `Contract:` (or `Contracts:`)
69
67
  - `len(out)=len_x` — allocate a 1D output array.
70
68
  - `postlen(out)=out_len` — slice the output after the call using an integer pointer result `out_len` (useful when the C code writes less than the allocated length).
71
69
  - `own(return)` — the returned pointer is owned by the caller; the wrapper will `free` it after copying to NumPy (requires a `len(return)=...` contract).
70
+ - `expose(len_x)` — opt out of naming-convention inference for `len_x`, keeping it as a required Python argument.
71
+
72
+ TinyCWrap also infers low-risk contracts from names:
73
+
74
+ - `len_x`, `n_x`, or `size_x` are treated as `len(x)` when `x` is an array argument.
75
+ - `out_x` is allocated with `shape(x)` when input array `x` exists.
76
+ - `out_x` is allocated with `len_x`, `n_x`, or `size_x` when no input `x` exists but such a length argument does.
77
+
78
+ Explicit contracts override inferred contracts.
72
79
 
73
80
  Examples pulled from the test suite:
74
81
 
@@ -99,9 +106,9 @@ Rules are parsed case-insensitively; whitespace does not matter.
99
106
 
100
107
  ## Python wrapper behavior
101
108
 
102
- - **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
109
+ - **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, naming conventions, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
103
110
  - **Struct pointer outputs**: non-const struct pointers are treated as in/out; when you pass an object/array explicitly, it is mutated in place and not returned. When you pass `None`, TinyCWrap allocates and returns the struct (or struct array).
104
- - **Optional length arguments**: integer length parameters inferred from contracts default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the expression from the contract is evaluated.
111
+ - **Optional length arguments**: integer length parameters inferred from contracts or naming conventions default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the inferred expression is evaluated.
105
112
  - **Post contracts**: when a contract uses `postlen(...)` or `post shape(...)`, the wrapper slices/reshapes outputs after the C call using the values written by the C function.
106
113
  - **Scalar pointer outputs**: pointers to integer-like types (e.g., `int *out_len`) are returned as plain Python integers alongside other outputs.
107
114
  - **Structs**: for `typedef struct` declarations TinyCWrap generates a Python class:
@@ -164,7 +171,7 @@ arr, n = cm.alloc_random_array() # returns NumPy array, frees the C buffer
164
171
  ## Tips
165
172
 
166
173
  - Keep function declarations (or headers) visible at the top level; TinyCWrap scans the C files and headers you pass.
167
- - If a length cannot be inferred from a contract, you must pass it explicitly from Python.
174
+ - If a length cannot be inferred from a naming convention or contract, you must pass it explicitly from Python.
168
175
  - Use `cm.<func>.__source__` to inspect the wrapper code if something behaves unexpectedly.
169
176
  - For debugging parsed signatures/contracts call `cm._debug_specs()`.
170
177
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinycwrap"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Lightweight C-to-Python wrapper generator using CFFI and NumPy"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -16,23 +16,30 @@ def cm():
16
16
  def test_dot(cm):
17
17
  x = np.array([1.0, 2.0, 3.0], dtype=np.float64)
18
18
  y = np.array([4.0, 5.0, 6.0], dtype=np.float64)
19
- assert cm.dot(x, y, len_x=len(x)) == np.dot(x, y)
19
+ assert cm.dot(x, y) == np.dot(x, y)
20
20
 
21
21
 
22
22
  def test_scale_auto_output(cm):
23
23
  x = np.arange(10, dtype=np.float64)
24
- scaled_auto = cm.scale(x, 1.1, len_x=len(x))
24
+ scaled_auto = cm.scale(x, 1.1)
25
25
  np.testing.assert_allclose(scaled_auto, x * 1.1)
26
26
 
27
27
 
28
28
  def test_scale_explicit_output(cm):
29
29
  x = np.arange(10, dtype=np.float64)
30
30
  out = np.empty_like(x)
31
- scaled_explicit = cm.scale(x, 2.0, len_x=len(x), out_x=out)
31
+ scaled_explicit = cm.scale(x, 2.0, out_x=out)
32
32
  np.testing.assert_allclose(out, x * 2.0)
33
33
  np.testing.assert_allclose(scaled_explicit, x * 2.0)
34
34
 
35
35
 
36
+ def test_expose_keeps_inferred_length_visible(cm):
37
+ x = np.arange(5, dtype=np.float64)
38
+ assert cm.prefix_sum(x, len_x=3) == 3.0
39
+ with pytest.raises(TypeError):
40
+ cm.prefix_sum(x)
41
+
42
+
36
43
  def test_mat_add_shape_contract(cm):
37
44
  a = np.arange(6, dtype=np.float64).reshape(2, 3)
38
45
  b = np.ones_like(a)
@@ -129,7 +136,7 @@ def test_struct_array_argument(cm):
129
136
  particles["vel"][0] = [1.0, 0.0, 0.0]
130
137
  particles["pos"][1] = [0.0, 0.0, 0.0]
131
138
  particles["vel"][1] = [0.0, 2.0, 0.0]
132
- ke = cm.kinetic_energy(particles, len_p=len(particles))
139
+ ke = cm.kinetic_energy(particles)
133
140
  assert np.isclose(ke, 0.5 * (1.0 + 4.0))
134
141
 
135
142
 
@@ -6,6 +6,6 @@ from .cmodule import CModule
6
6
  try:
7
7
  __version__ = version("tinycwrap")
8
8
  except PackageNotFoundError:
9
- __version__ = "0.1.1"
9
+ __version__ = "0.1.2"
10
10
 
11
11
  __all__ = ["CModule", "__version__"]
@@ -169,6 +169,7 @@ class CModule:
169
169
  self._func_specs.pop("free", None)
170
170
  self._struct_specs = parse_structs_from_cdef(self._cdef)
171
171
  self._attach_docs_from_source()
172
+ self._infer_contracts_from_names()
172
173
  self._mark_length_params_from_contracts()
173
174
  self._create_struct_classes()
174
175
  self._create_wrappers()
@@ -448,6 +449,7 @@ class CModule:
448
449
  fspec.doc = doc
449
450
  contracts = []
450
451
  owns: list[str] = []
452
+ exposes: set[str] = set()
451
453
 
452
454
  def _normalize_shape_expr(expr: str) -> str:
453
455
  expr = expr.strip()
@@ -483,6 +485,13 @@ class CModule:
483
485
  if n:
484
486
  owns.append(n)
485
487
  continue
488
+ m_expose_call = re.match(r"expose\s*\(\s*([^)]+)\s*\)\s*$", entry, flags=re.IGNORECASE)
489
+ if m_expose_call:
490
+ for name in m_expose_call.group(1).split(","):
491
+ n = name.strip()
492
+ if n:
493
+ exposes.add(n)
494
+ continue
486
495
  is_post = False
487
496
  m_post = re.match(r"post\s*(.+)", entry, flags=re.IGNORECASE)
488
497
  if m_post and (m_post.group(1).strip().lower().startswith("len(") or m_post.group(1).strip().lower().startswith("shape(")):
@@ -515,6 +524,84 @@ class CModule:
515
524
  contracts.append((target, expr, is_post))
516
525
  fspec.contracts = contracts or None
517
526
  fspec.owns = owns or None
527
+ fspec.exposes = exposes or None
528
+
529
+ @staticmethod
530
+ def _length_arg_target(arg_name: str) -> str | None:
531
+ for prefix in ("len_", "n_", "size_"):
532
+ if arg_name.startswith(prefix) and len(arg_name) > len(prefix):
533
+ return arg_name[len(prefix) :]
534
+ return None
535
+
536
+ @staticmethod
537
+ def _is_integer_arg(arg) -> bool:
538
+ return arg.base_type in (
539
+ "int",
540
+ "unsigned int",
541
+ "unsigned",
542
+ "long",
543
+ "long int",
544
+ "long long",
545
+ "long long int",
546
+ "unsigned long",
547
+ "unsigned long long",
548
+ "size_t",
549
+ "ssize_t",
550
+ )
551
+
552
+ def _infer_contracts_from_names(self):
553
+ """Infer low-risk contracts from conventional argument names."""
554
+ for fspec in self._func_specs.values():
555
+ contracts = list(fspec.contracts or [])
556
+ exposes = fspec.exposes or set()
557
+ explicit_targets = {target for target, _expr, _is_post in contracts}
558
+ explicit_shapes = {
559
+ target[len("shape(") : -1]
560
+ for target, _expr, _is_post in contracts
561
+ if target.startswith("shape(") and target.endswith(")")
562
+ }
563
+ arg_by_name = {a.name: a for a in fspec.args}
564
+ array_names = {
565
+ a.name
566
+ for a in fspec.args
567
+ if a.is_array_in or a.is_array_out or a.is_pointer or a.array_len is not None
568
+ }
569
+
570
+ for arg in fspec.args:
571
+ if arg.name in exposes or not self._is_integer_arg(arg):
572
+ continue
573
+ target = self._length_arg_target(arg.name)
574
+ if target and target in array_names and arg.name not in explicit_targets:
575
+ contracts.append((arg.name, f"len({target})", False))
576
+ explicit_targets.add(arg.name)
577
+
578
+ length_names_by_target: dict[str, str] = {}
579
+ for arg in fspec.args:
580
+ target = self._length_arg_target(arg.name)
581
+ if target and self._is_integer_arg(arg):
582
+ length_names_by_target[target] = arg.name
583
+
584
+ for arg in fspec.args:
585
+ if not (arg.is_array_out and arg.name.lower().startswith("out_")):
586
+ continue
587
+ target = arg.name[4:]
588
+ if arg.name in explicit_targets or arg.name in explicit_shapes:
589
+ continue
590
+ if target in arg_by_name and (
591
+ arg_by_name[target].is_array_in
592
+ or arg_by_name[target].is_array_out
593
+ or arg_by_name[target].is_pointer
594
+ or arg_by_name[target].array_len is not None
595
+ ):
596
+ contracts.append((f"shape({arg.name})", f"shape({target})", False))
597
+ explicit_shapes.add(arg.name)
598
+ continue
599
+ length_name = length_names_by_target.get(target)
600
+ if length_name:
601
+ contracts.append((arg.name, length_name, False))
602
+ explicit_targets.add(arg.name)
603
+
604
+ fspec.contracts = contracts or None
518
605
 
519
606
  def _mark_length_params_from_contracts(self):
520
607
  """Mark length-like parameters based solely on explicit contracts."""
@@ -981,7 +1068,7 @@ class CModule:
981
1068
  if a.is_array_out and a.name.lower().startswith("out"):
982
1069
  extra.append("auto if None")
983
1070
  if a.is_length_param:
984
- extra.append("auto from Contract")
1071
+ extra.append("auto")
985
1072
  extra_txt = f" [{' '.join(extra)}]" if extra else ""
986
1073
  arg_docs.append(f"{a.name} : {ctype} ({role}){extra_txt}")
987
1074
 
@@ -1194,22 +1281,23 @@ class CModule:
1194
1281
  out_lines += [
1195
1282
  f" arr_{a.name} = np.zeros(int({expr_py}), dtype=base_dtype)"
1196
1283
  ]
1197
- elif a.array_len is None and len_expr is None and shape_expr is None:
1198
- out_lines += [
1199
- f" arr_{a.name} = np.zeros((), dtype=base_dtype)"
1200
- ]
1201
1284
  elif ref_name:
1202
1285
  out_lines += [
1203
1286
  f" ref_arr = locals().get('arr_{ref_name}', None)",
1204
1287
  " if ref_arr is not None:",
1205
1288
  f" arr_{a.name} = np.zeros_like(ref_arr, dtype=base_dtype)",
1206
1289
  " else:",
1207
- f" raise ValueError('{fspec.name}: provide {a.name} or a Contract for its length')",
1290
+ f" arr_{a.name} = np.zeros((), dtype=base_dtype)",
1208
1291
  ]
1209
1292
  else:
1210
- out_lines += [
1211
- f" raise ValueError('{fspec.name}: provide {a.name} or a Contract for its length')",
1212
- ]
1293
+ if a.array_len is None and len_expr is None and shape_expr is None:
1294
+ out_lines += [
1295
+ f" arr_{a.name} = np.zeros((), dtype=base_dtype)"
1296
+ ]
1297
+ else:
1298
+ out_lines += [
1299
+ f" raise ValueError('{fspec.name}: provide {a.name} or a Contract for its length')",
1300
+ ]
1213
1301
  out_lines += [
1214
1302
  " else:",
1215
1303
  ]
@@ -73,6 +73,7 @@ class FuncSpec:
73
73
  doc: str | None = None
74
74
  contracts: list[tuple[str, str, bool]] | None = None
75
75
  owns: list[str] | None = None
76
+ exposes: set[str] | None = None
76
77
 
77
78
 
78
79
  @dataclass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinycwrap
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Lightweight C-to-Python wrapper generator using CFFI and NumPy
5
5
  Author: TinyCWrap Contributors
6
6
  Requires-Python: >=3.9
@@ -28,9 +28,7 @@ Write a small C file (only non-`static` functions are exported):
28
28
  ```c
29
29
  /* kernels.c */
30
30
  double dot(const double *x, const double *y, int len_x)
31
- /* Return dot product between x and y
32
- Contract: len_x=len(x);
33
- */
31
+ /* Return dot product between x and y */
34
32
  {
35
33
  double acc = 0.0;
36
34
  for (int i = 0; i < len_x; ++i)
@@ -50,7 +48,7 @@ cm = CModule("kernels.c") # builds the shared library, creates wrappers
50
48
  x = np.arange(5, dtype=np.float64)
51
49
  y = np.ones_like(x)
52
50
 
53
- cm.dot(x, y) # -> 10.0, len_x auto-filled by the contract
51
+ cm.dot(x, y) # -> 10.0, len_x auto-filled by convention
54
52
  print(cm.dot.__doc__) # docstring comes from the C comment
55
53
  ```
56
54
 
@@ -65,7 +63,7 @@ TinyCWrap infers how to build wrappers from simple C conventions:
65
63
  - `T *arg` -> output/in-place array (prefer naming it `out_*` when possible).
66
64
  - `T arg[N]` in the signature -> fixed-size array (input if `const`, otherwise output).
67
65
  - Plain scalars (`double`, `int`, ...) -> Python scalars.
68
- - **Length parameters**: integers such as `len_x`, `n`, `size_x` can be auto-filled if you declare a contract (see below). Otherwise pass them explicitly from Python.
66
+ - **Length parameters**: integers named like `len_x`, `n_x`, or `size_x` are auto-filled from array `x` by convention. Otherwise pass them explicitly from Python or declare a contract (see below).
69
67
  - **Docstrings**: the block comment immediately after the function header becomes the Python docstring.
70
68
  - **Structs**: `typedef struct { ... } Name;` definitions in your headers/sources become Python classes with a `.dtype` and NumPy-backed storage.
71
69
  - **Compilation**: extra sources can be passed (`CModule("main.c", "helper.c")`), and extra include dirs via `include_dirs=[...]`. Default compiler flags are `-O3 -shared -fPIC -march=native -mtune=native` plus the NumPy include path.
@@ -79,6 +77,15 @@ Contracts are declared inside the doc comment with `Contract:` (or `Contracts:`)
79
77
  - `len(out)=len_x` — allocate a 1D output array.
80
78
  - `postlen(out)=out_len` — slice the output after the call using an integer pointer result `out_len` (useful when the C code writes less than the allocated length).
81
79
  - `own(return)` — the returned pointer is owned by the caller; the wrapper will `free` it after copying to NumPy (requires a `len(return)=...` contract).
80
+ - `expose(len_x)` — opt out of naming-convention inference for `len_x`, keeping it as a required Python argument.
81
+
82
+ TinyCWrap also infers low-risk contracts from names:
83
+
84
+ - `len_x`, `n_x`, or `size_x` are treated as `len(x)` when `x` is an array argument.
85
+ - `out_x` is allocated with `shape(x)` when input array `x` exists.
86
+ - `out_x` is allocated with `len_x`, `n_x`, or `size_x` when no input `x` exists but such a length argument does.
87
+
88
+ Explicit contracts override inferred contracts.
82
89
 
83
90
  Examples pulled from the test suite:
84
91
 
@@ -109,9 +116,9 @@ Rules are parsed case-insensitively; whitespace does not matter.
109
116
 
110
117
  ## Python wrapper behavior
111
118
 
112
- - **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
119
+ - **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, naming conventions, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
113
120
  - **Struct pointer outputs**: non-const struct pointers are treated as in/out; when you pass an object/array explicitly, it is mutated in place and not returned. When you pass `None`, TinyCWrap allocates and returns the struct (or struct array).
114
- - **Optional length arguments**: integer length parameters inferred from contracts default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the expression from the contract is evaluated.
121
+ - **Optional length arguments**: integer length parameters inferred from contracts or naming conventions default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the inferred expression is evaluated.
115
122
  - **Post contracts**: when a contract uses `postlen(...)` or `post shape(...)`, the wrapper slices/reshapes outputs after the C call using the values written by the C function.
116
123
  - **Scalar pointer outputs**: pointers to integer-like types (e.g., `int *out_len`) are returned as plain Python integers alongside other outputs.
117
124
  - **Structs**: for `typedef struct` declarations TinyCWrap generates a Python class:
@@ -174,7 +181,7 @@ arr, n = cm.alloc_random_array() # returns NumPy array, frees the C buffer
174
181
  ## Tips
175
182
 
176
183
  - Keep function declarations (or headers) visible at the top level; TinyCWrap scans the C files and headers you pass.
177
- - If a length cannot be inferred from a contract, you must pass it explicitly from Python.
184
+ - If a length cannot be inferred from a naming convention or contract, you must pass it explicitly from Python.
178
185
  - Use `cm.<func>.__source__` to inspect the wrapper code if something behaves unexpectedly.
179
186
  - For debugging parsed signatures/contracts call `cm._debug_specs()`.
180
187
 
File without changes
File without changes
File without changes