From 1643415fa37a96665ee59682548f3b2e6273c8f6 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Thu, 29 Oct 2026 14:17:34 +0900 Subject: [PATCH 1/4] docs: expand Python API contracts and isolate executable examples Adapt numpydoc content conventions to the existing Markdown reference, including object-kind-aware coverage, stable symbol anchors, numerical semantics, ownership, errors, and the optional cffi interface. Add independent example execution, documentation-check regression tests, and explicit checkout/released package selection in documentation CI. No numerical kernels or public Python API behavior are changed. --- .github/workflows/docs.yml | 34 +- docs/contributing.md | 171 ++- docs/explanation/numerical-conventions.md | 35 + docs/guides/numpy.md | 151 ++- docs/reference/python.md | 1485 +++++++++++++++------ mkdocs.yml | 1 + tools/check_api_docs.py | 162 ++- tools/check_docs_examples.py | 120 +- tools/test_docs_checks.py | 241 ++++ 9 files changed, 1924 insertions(+), 476 deletions(-) create mode 100644 tools/test_docs_checks.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9ad78a0..4964f82 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,6 +6,12 @@ on: branches: [master] pull_request: workflow_dispatch: + inputs: + python-runtime: + description: Python package to execute the documentation against + type: choice + options: [checkout, released] + default: checkout permissions: contents: read @@ -24,6 +30,11 @@ jobs: python-version: '3.12' cache: pip cache-dependency-path: requirements-docs.txt + - name: Test documentation validators + run: | + python -m unittest discover -s tools -p 'test_docs_checks.py' + python tools/check_api_docs.py --python-only + python tools/check_docs_examples.py --check-syntax - uses: ./.github/actions/setup-haskell with: ghc-version: '9.10.3' @@ -36,14 +47,29 @@ jobs: run: | python -m pip install -r requirements-docs.txt python tools/build_docs.py - - name: Verify Python documentation examples against the released package + - name: Native dependencies for the checkout wheel + if: inputs.python-runtime != 'released' + run: | + sudo apt-get update + sudo apt-get install -y liblapacke-dev libopenblas-dev zlib1g-dev + - name: Verify Python documentation examples against the selected package + env: + DOCS_PYTHON_RUNTIME: ${{ inputs.python-runtime || 'checkout' }} run: | python - <<'PY' - import pathlib, subprocess, sys, tomllib + import os + import pathlib + import subprocess + import sys + import tomllib + version = tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'] - subprocess.run([sys.executable, '-m', 'pip', 'install', f'hmath-python[numpy,cffi]=={version}'], check=True) + target = os.environ['DOCS_PYTHON_RUNTIME'] + package = '.[numpy,cffi]' if target == 'checkout' else f'hmath-python[numpy,cffi]=={version}' + print(f'Documentation package source: {target}', flush=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', '--force-reinstall', package], check=True) + subprocess.run([sys.executable, 'tools/check_docs_examples.py', '--expected-version', version], check=True) PY - python tools/check_docs_examples.py - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: site diff --git a/docs/contributing.md b/docs/contributing.md index d23795f..9507237 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -26,47 +26,148 @@ python tools/build_docs.py python -m mkdocs serve --config-file build-docs/mkdocs.generated.yml ``` -To build the site together with the complete Haskell API reference, install -the Haskell toolchain and run: +The complete site build requires the Haskell toolchain and generates the +Haskell API reference. It validates reference coverage, Haskell example +programs, the strict MkDocs build, and local file/fragment links. Do not commit +generated HTML. CI deploys the generated `site/` after changes reach `master`. +The home-page version is substituted from `pyproject.toml` during generation; +documentation changes alone do not require a package release. + +For fast Python documentation checks without a native runtime or Haskell +compiler, run: ```sh -python tools/build_docs.py +python -m unittest discover -s tools -p 'test_docs_checks.py' +python tools/check_api_docs.py --python-only +python tools/check_docs_examples.py --check-syntax ``` -CI builds with strict validation and deploys the generated `site/` directory -to GitHub Pages after changes reach `master`. Do not commit generated HTML. -Use runnable examples with expected results; distinguish available interfaces -from future or Haskell-only features. +Syntax checking is not execution. To validate the current checkout, install +its wheel (GHC and Cabal are required for the native source build) and run: + +```sh +python -m pip install --force-reinstall '.[numpy,cffi]' +python tools/check_docs_examples.py +``` + +To validate a published version, use a clean environment and install that +exact version of `hmath-python[numpy,cffi]`, then pass the same version to +`python tools/check_docs_examples.py --expected-version VERSION`. Replace +`VERSION` with a real release identifier; it is not a literal command argument. +The checker rejects an installed distribution with a different version. +The Documentation workflow normally tests a wheel built from the checkout; +its manual `python-runtime` input selects `released` for explicit published- +package validation. The tested distribution version is printed in the log. +These documentation checks do not replace the portable wheel pipeline. ## Reference writing standard -Follow the [numpydoc section conventions](https://numpydoc.readthedocs.io/en/latest/format.html) +Use the [numpydoc section conventions](https://numpydoc.readthedocs.io/en/stable/format.html) for Python reference content and [Haddock conventions](https://haskell-haddock.readthedocs.io/latest/markup.html) -for Haskell source documentation. The website uses Markdown; adopting these -content conventions does not require changing its site generator. - -Each Python, C, or Wave entry needs a one-line purpose, exact signature, -`Parameters` table with one row per argument in declaration order, a `Returns` -section, and relevant `See also` links. Include types, defaults, units, shapes, -bounds, ownership, and failure conditions where they apply. Use `None.` for -an empty parameter list. Properties have no call parentheses. Explain errors -with the actual exception/status/constructor, rather than “may fail”. - -Add examples for operations that need an explanation of usage. Show expected -results, resource cleanup, and meaningful failure cases. For numerical -algorithms, explain the equation, assumptions, stopping criteria, and accuracy -limitations; keep implementation history out of user instructions. A simple -getter does not need a paper citation or an artificial standalone example. - -Haskell module and area indexes are generated from declarations and authored +for Haskell source documentation. The website uses Markdown with definition +lists; it adopts numpydoc's content conventions, not its reStructuredText +syntax. This does not require changing the site generator. Python source +docstrings, when edited, follow [PEP 257](https://peps.python.org/pep-0257/), +and examples follow [PEP 8](https://peps.python.org/pep-0008/). + +### Python object kinds and sections + +Every entry has a one-line purpose, an exact source signature (or the name of +an attribute without call parentheses), and a stable symbol anchor, for +example ``. Preserve existing heading-derived +anchors when changing navigation. Link new references to the symbol anchor +rather than to a signature whose defaults may change. + +Use the following section order, omitting optional sections when irrelevant: +`Parameters`, `Returns`, `Raises`, `Warns`, `Warnings`, `Other Parameters`, +`Attributes`, `Methods`, `See Also`, `Notes`, `References`, `Examples`. +Keep mathematical definitions, algorithms, numerical behavior, and cost +explanations under `Notes`; use lower-level headings there when needed. +Do not invent a paper reference or an error estimate for a simple accessor. + +| Object | Required content | Distinction | +| --- | --- | --- | +| Function or method | `Parameters`, `Returns`, `See Also` | State `None.` for an empty parameter list and document a `None` return explicitly. | +| Class, including an exception | Constructor `Parameters`, public `Attributes`, `See Also` | Do not add a function-style `Returns` section to the class description. | +| Property or public data attribute | `Type`, `See Also` | No call parentheses, `Parameters`, or `Returns`; describe access/lifetime conditions as appropriate. | + +For `Parameters`, use a table with one row per argument in declaration order. +Include type, default value and its meaning, units when applicable, valid +shapes/ranges, and conversion behavior. Keep the heading signature identical +to the source; explanatory types do not justify adding nonexistent annotations +or arguments. A constructor's `shape=None` must describe how shape is chosen. + +`Returns` identifies type, meaning, shape, and whether a result is a new owned +handle, an independent copy, or a view. `Raises` names actual exception classes +and conditions, distinguishing wrapper validation, conversion, runtime +initialization, and checked native failures. Native status codes belong only +on paths where they can occur. Do not replace these conditions with "may fail" +or "constructor errors". Diagnostic text is not a string-matching guarantee. + +Public data attributes are part of reference coverage even when they are not +properties: `.status` and cffi `.shape` are intentional examples. Update the +explicit attribute inventory in `tools/check_api_docs.py` when that public +surface changes; raw FFI handles and incidental imports are not automatically +promoted to public APIs. Keep classes, methods, and attributes linked. + +The Python checker validates kinds, exact heading signatures, parameter order, +defaults, required/ordered sections, duplicate and unknown entries, and stable +symbol anchors. The C/Wave reference keeps its existing `Parameters`, +`Returns`, and `See also` convention and exact signature/status checks. + +### Numerical and memory contracts + +State the mathematical definition separately from implementation-specific +behavior. Check the actual Python-to-native-to-Haskell call path before naming +an algorithm, backend, complexity, or precision property. Distinguish arithmetic +work from input gathering, output allocation, and validation; zero-copy is not +synonymous with allocation-free or constant-time. Include empty and rank-zero +inputs where meaningful. Work budgets are not accuracy guarantees. + +Preserve the distinction between Python-exposed functionality and Haskell-only +or future functionality. Do not claim NumPy, Python Array API, ISO, or WCAG +conformance merely because documentation resembles another project's pages. +A compatibility or accuracy guarantee needs its own evidence. + +Document ownership for every returned handle/view, the effect of closing the +original handle, the caller's obligations for borrowed aliases, and the scope +of any synchronization. Thread safety of individual operations is not an +atomicity guarantee for arbitrary sequences or external array mutation. + +### Executable examples + +Reference examples explain usage; they are not an exhaustive replacement for +regression tests. Include imports, input data, observable/checked results, and +resource cleanup. Simple exact examples may use equality; general floating- +point comparisons need explicit, justified tolerances. An error example must +fail when the expected exception is not raised, and close unexpected results. + +Mark a page whose Python blocks are independent with +``. Each fenced `python` block then runs in +a fresh isolated interpreter, including shutdown examples. Pages without the +marker retain a shared namespace within a page for sequential tutorials, but +never share a process with another page. Keep each independent example complete; +no imports or objects may be borrowed from a preceding block. Use ordinary +`python` fences, not `text` fences to hide runnable examples from checks. + +The example checker reports original Markdown line numbers, times out hung +processes, and requires completion of every block. `--check-syntax` compiles +without importing HMath; `--expected-version` verifies the installed package +before execution. Run C/Wave example programs with the native build when +changing them. Complete Haskell example programs are checked against the +checkout during the full documentation build. + +### Navigation and review + +Keep task guides distinct from reference contracts and conceptual explanations, +following [Diátaxis](https://diataxis.fr/start-here/). The existing +`getting-started/`, `guides/`, `explanation/`, and `reference/` structure is the +starting point; do not split files merely to imitate another library. +Haskell indexes continue to be generated from declarations and authored Haddock prose, including constructors, record fields, and class methods. -The API check includes symbolic function names as well as ordinary identifiers. -Keep standalone task guides distinct from reference contracts and conceptual -explanations, following [Diátaxis](https://diataxis.fr/start-here/). - -The documentation workflow checks source signatures and parameter order, -reference coverage, local file/fragment links, Python examples against the -released wheel, and complete Haskell example programs against this checkout. -Run the C/Wave example programs with the native build when changing them. -The home-page version is substituted from `pyproject.toml` during generation; -documentation changes alone do not require a package release. + +Review heading hierarchy, tables, definition lists, rendered formulas, code +copying, keyboard navigation, zoom, and contrast in the built site. Preserve +useful existing explanations and inbound links. PRs should identify the source +version, documentation scope, checks actually executed, and any validation +that still depends on CI or a toolchain unavailable locally. diff --git a/docs/explanation/numerical-conventions.md b/docs/explanation/numerical-conventions.md index 93c0e1c..91d9fdd 100644 --- a/docs/explanation/numerical-conventions.md +++ b/docs/explanation/numerical-conventions.md @@ -55,6 +55,39 @@ with Tensor([0.1, 0.2]) as samples: assert math.isclose(samples.sum(), 0.3, rel_tol=1e-12, abs_tol=1e-15) ``` +## Python reduction and matrix-product behavior + +The Python wrappers call the checked native tensor kernels, not every +algorithm exposed by the Haskell library. The current reference path gathers +logical elements into contiguous native scratch arrays before entering +`HMath.FFI`. Sum uses ordinary Haskell `sum`; dot uses `HMath.Linear.dot`, +which sums corresponding products. Matrix multiplication accumulates each +cell in increasing inner-index order. These descriptions identify the current +implementation, not a cross-release promise of bit-for-bit reduction order. + +The mathematical definition of a sum or dot product does not imply exact +accumulation, compensated summation, or a correctly rounded final answer. +Finite inputs can produce a non-finite computed result, which is rejected. +Conversely, a finite accepted result can still have substantial relative error +when cancellation makes the true result small. No universal error tolerance +is implied by accepting a result. The checked path is distinct from legacy +native calls that retain their own non-finite policies. + +For `a.dot(b)`, all corresponding elements are reduced to a Python float and +the shapes must match exactly. This is not NumPy's rank-dependent `dot`. +For `a @ b`, `(m, k)` and `(k, n)` produce `(m, n)`; the binding supports rank +two only. The `m*k*n` work limit bounds computation, not numerical error. +A compatible zero inner dimension yields a zero matrix, while a zero outer +dimension yields an empty result. See the +[Python calculations reference](../reference/python.md#calculations) for +individual contracts, budgets, and costs. + +Input conversion is another numerical step. Python integers do not remain +arbitrary-precision integers inside a Tensor: they are converted to binary64. +Likewise, copying a NumPy integer array to float64 can round values before +any HMath calculation takes place. For copied versus borrowed data and +lifetime guarantees, see [NumPy interoperability](../guides/numpy.md). + ## Residual, conditioning, and rank For a proposed solution of $A x = b$, the residual is @@ -89,3 +122,5 @@ bounds on every possible callback. - [SciPy linear solve](https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.solve.html): shape contracts and singular/ill-conditioned systems. - [GNU Scientific Library linear algebra](https://www.gnu.org/software/gsl/doc/html/linalg.html): decompositions, rank, and numerical tolerances. - [Python `math.isclose`](https://docs.python.org/3/library/math.html#math.isclose): approximate comparison semantics. +- [NumPy sum](https://numpy.org/doc/stable/reference/generated/numpy.sum.html): an example of documenting implementation-specific reduction precision; its algorithm is not an HMath guarantee. +- [HMath native boundary](https://github.com/wavefnd/hmath/blob/master/cbits/native.c), [Haskell FFI kernels](https://github.com/wavefnd/hmath/blob/master/src/HMath/FFI.hs), and [linear operations](https://github.com/wavefnd/hmath/blob/master/src/HMath/Linear.hs): implementation sources for the Python numerical path. diff --git a/docs/guides/numpy.md b/docs/guides/numpy.md index 8457c73..06646c3 100644 --- a/docs/guides/numpy.md +++ b/docs/guides/numpy.md @@ -1,6 +1,25 @@ # Work with NumPy arrays -Install the `numpy` extra as described in [installation](../getting-started/installation.md). + + +Use this guide to move data between NumPy and HMath and choose an ownership +policy. Install the `numpy` extra as described in +[installation](../getting-started/installation.md). Each example is independent. +The complete argument and failure contracts are in +[Tensor.from_numpy](../reference/python.md#hmath.Tensor.from_numpy) and +[Tensor.numpy](../reference/python.md#hmath.Tensor.numpy). + +## Choose whether to copy + +| Task | Input conversion | Output conversion | Ownership consequence | +| --- | --- | --- | --- | +| Keep working with a mutable source | `Tensor.from_numpy(source)` | `tensor.numpy()` | Both boundaries copy; each side can change independently. | +| Read eligible existing data without an element copy | `Tensor.from_numpy(source, copy=False)` | Either mode | All aliases to the borrowed input must stay immutable. | +| Inspect a result without an element copy | Either mode | `tensor.numpy(copy=False)` | A read-only view retains the result storage after the Tensor closes. | + +Borrowing avoids copying the element buffer, not validation or object/metadata +allocation. It is an ownership choice, not a promise of constant-time input +conversion. Start with the copying path when another component may mutate data. ## Copy an array into a tensor @@ -8,14 +27,21 @@ Install the `numpy` extra as described in [installation](../getting-started/inst import numpy as np from hmath import Tensor -source = np.array([[1, 2], [3, 4]], dtype=np.float64) +source = np.array([[1, 2], [3, 4]], dtype=np.int32) with Tensor.from_numpy(source) as tensor: assert tensor.shape == (2, 2) - assert np.array_equal(tensor.numpy(), source) + source[0, 0] = 99 + result = tensor.numpy() + assert result.dtype == np.float64 + assert result.tolist() == [[1.0, 2.0], [3.0, 4.0]] +result[0, 0] = -1 +assert source[0, 0] == 99 ``` -Both conversions copy by default. Later changes to the original array do not -change the copied tensor, and the returned NumPy array owns independent data. +The input is converted to float64 and copied in logical C order. A returned +copy is writable and remains independent of both the Tensor and the original +array. Converting large integers to float64 can round them; copying does not +provide arbitrary-precision arithmetic. ## Borrow an array without copying @@ -31,16 +57,113 @@ with Tensor.from_numpy(source[:, ::2], copy=False) as tensor: assert tensor.sum() == 30.0 assert np.shares_memory(view, source) assert not view.flags.writeable +assert view.tolist() == [[0.0, 2.0], [4.0, 6.0], [8.0, 10.0]] +del view ``` -Zero-copy input requires native-endian, aligned, read-only `float64` storage -and nonnegative strides that are multiples of eight bytes. Negative-stride -views and writable arrays are rejected with `ValueError`. +This non-contiguous, positive-stride view is eligible. The normalized array +must be aligned, native-endian, read-only float64, with nonnegative byte strides +that are multiples of eight. Logical count, stride, and reachable storage span +must also fit the native budgets. Negative-stride views and writable arrays are +rejected; there is no automatic copy fallback when `copy=False`. + +The Tensor retains the array owner. Every alias to borrowed storage must remain +immutable until all tensors and derived views are released. Setting one array +read-only cannot prevent writes through another writable alias. Use this path +only when the application controls those aliases. + +## Handle a conversion that needs copying + +A reversed NumPy slice has a negative stride. Copy it explicitly instead of +relying on implicit fallback: + +```python +import numpy as np +from hmath import Tensor -The caller must keep all aliases immutable until every tensor and derived -view has been released. Setting one array to read-only cannot prevent writes -through another writable alias. +source = np.arange(4, dtype=np.float64) +source.setflags(write=False) +reversed_source = source[::-1] +try: + with Tensor.from_numpy(reversed_source, copy=False): + pass +except ValueError: + pass +else: + raise AssertionError("negative-stride borrowing must be rejected") +with Tensor.from_numpy(reversed_source, copy=True) as tensor: + assert tensor.values() == [3.0, 2.0, 1.0, 0.0] +``` + +For other failures, inspect the exception rather than catching every error and +silently copying: non-finite data and oversized layouts can remain invalid even +when copied. `numpy.asarray` normalization can allocate for array-like inputs +before HMath reaches its borrowing checks. + +## Keep a result view after closing its handle + +```python +import numpy as np +from hmath import Tensor + +with Tensor([1, 2, 3, 4], (2, 2)) as tensor: + view = tensor.numpy(copy=False) + column = view[:, 1] + assert np.shares_memory(column, view) +del view +assert column.tolist() == [2.0, 4.0] +assert not column.flags.writeable +del column +``` + +The view's backing buffer retains native storage and any borrowed array owner. +A derived view keeps that ownership chain alive. Closing a Tensor releases its +handle, not all references to the storage. Release views when no longer needed. +For empty arrays, `numpy(copy=False)` can use a separate empty read-only +allocation; there are no element bytes to share. + +## Reshape a borrowed tensor deliberately + +Borrowing accepts some layouts that `reshape()` does not. A non-contiguous +Tensor cannot be reshaped by silently copying. When copying is acceptable, +create a contiguous Tensor explicitly: + +```python +import numpy as np +from hmath import Tensor + +source = np.arange(8, dtype=np.float64) +source.setflags(write=False) +with Tensor.from_numpy(source[::2], copy=True) as contiguous: + with contiguous.reshape((2, 2)) as matrix: + assert matrix.numpy().tolist() == [[0.0, 2.0], [4.0, 6.0]] +``` + +See [Tensor.reshape](../reference/python.md#hmath.Tensor.reshape) for its exact +contiguity and shape rules. Unlike NumPy's reshape, it does not infer `-1` or +fall back to a copy. + +## Compare a calculation with NumPy + +Use the same operation on each side. HMath `dot()` reduces corresponding +products at every rank; NumPy `dot()` has different higher-rank semantics. +For matrix multiplication, compare `@` with `@`: + +```python +import numpy as np +from hmath import Tensor + +left = np.array([[0.1, 0.2], [0.3, 0.4]], dtype=np.float64) +right = np.array([[0.5], [0.6]], dtype=np.float64) +with Tensor.from_numpy(left) as a, Tensor.from_numpy(right) as b: + with a @ b as result: + actual = result.numpy() +expected = left @ right +assert actual.shape == expected.shape +np.testing.assert_allclose(actual, expected, rtol=1e-12, atol=1e-15) +``` -A NumPy view retains a native reference and its borrowed array owner, so it -remains valid after the original tensor is closed. Release the view when it -is no longer needed. Empty arrays can use a separate empty NumPy allocation. +These tolerances belong to this example, not to a library-wide accuracy +guarantee. Choose tolerances for the scale and conditioning of the actual +problem. See [numerical conventions](../explanation/numerical-conventions.md) +and [validate a matrix calculation](matrix-calculation.md). diff --git a/docs/reference/python.md b/docs/reference/python.md index edb61f3..22aadff 100644 --- a/docs/reference/python.md +++ b/docs/reference/python.md @@ -1,65 +1,107 @@ # Python API -`hmath` is the primary interface. Calculations use finite IEEE 754 binary64 -values. Tensors are immutable and do not broadcast implicitly. NumPy is needed -only for the conversion methods. Python code blocks run in documentation checks; -the final-shutdown example is displayed separately. + + +This reference describes the Python interfaces in HMath **@HMATH_VERSION@**, +using native **ABI 1**. Install `hmath-python` and import `hmath`; the distribution +name and the import name are different. See [installation](../getting-started/installation.md) +for supported wheels and optional dependencies. + +`hmath` is the primary interface. `hmath_cffi` is an optional, smaller interface +to the same native runtime. This page documents their public Python operations, +not every function available in the C ABI or the Haskell library. Each Python +example below includes its own imports and runs in a separate process during +documentation checks, including the final-shutdown example. ## Function index -| API | Purpose | -| --- | --- | -| [hmath.initialize](#hmathinitialize) | Initialize the shared numerical runtime. | -| [hmath.add](#hmathadda-b) | Add two real scalars. | -| [hmath.shutdown](#hmathshutdown) | Permanently shut down the process-owned runtime. | -| [hmath.HMathError](#hmathhmatherrorstatus) | Represent a failed native checked operation. | -| [hmath.Tensor](#hmathtensorvalues-shapenone) | Copy flat values into a new immutable tensor. | -| [hmath.Tensor.scalar](#hmathtensorscalarvalue) | Construct a rank-zero tensor. | -| [hmath.Tensor.matrix](#hmathtensormatrixvalues-rows-columns) | Construct a rank-two matrix from flat row-major values. | -| [hmath.Tensor.from_numpy](#hmathtensorfrom_numpyarray-copytrue) | Convert an array to a tensor, copying or borrowing its storage. | -| [hmath.Tensor.shape](#hmathtensorshape) | Read the tensor’s dimensions. | -| [hmath.Tensor.values](#hmathtensorvalues) | Copy logical elements to a flat Python list. | -| [hmath.Tensor.numpy](#hmathtensornumpycopytrue) | Return a NumPy copy or a read-only view. | -| [hmath.Tensor.sum](#hmathtensorsum) | Sum all tensor elements to one scalar. | -| [hmath.Tensor.dot](#hmathtensordotother) | Reduce corresponding products of two identically shaped tensors. | -| [hmath.Tensor.__add__](#hmathtensor__add__other-a-b) | Add two tensors element by element. | -| [hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b) | Multiply two rank-two matrices. | -| [hmath.Tensor.reshape](#hmathtensorreshapeshape) | Create a new shape over the same contiguous storage. | -| [hmath.Tensor.close](#hmathtensorclose) | Release this tensor handle deterministically. | -| [hmath.Tensor.__enter__](#hmathtensor__enter__) | Enter a context that owns this tensor handle. | -| [hmath.Tensor.__exit__](#hmathtensor__exit__args) | Close the tensor when leaving a context. | -| [hmath.Tensor.__del__](#hmathtensor__del__) | Release the handle as a garbage-collection fallback. | -| [hmath_cffi.add](#hmath_cffiadda-b) | Add two real scalars. | -| [hmath_cffi.HMathError](#hmath_cffihmatherrorstatus) | Represent a failed native checked operation. | -| [hmath_cffi.check](#hmath_cfficheckstatus) | Convert a raw native status into a Python exception. | -| [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone) | Copy flat values into a new immutable tensor. | -| [hmath_cffi.Tensor.values](#hmath_cffitensorvalues) | Copy logical elements to a flat Python list. | -| [hmath_cffi.Tensor.sum](#hmath_cffitensorsum) | Sum all tensor elements to one scalar. | -| [hmath_cffi.Tensor.dot](#hmath_cffitensordotother) | Reduce corresponding products of two identically shaped tensors. | -| [hmath_cffi.Tensor.__add__](#hmath_cffitensor__add__other-a-b) | Add two tensors element by element. | -| [hmath_cffi.Tensor.__matmul__](#hmath_cffitensor__matmul__other-a-b) | Multiply two rank-two matrices. | -| [hmath_cffi.Tensor.close](#hmath_cffitensorclose) | Release this tensor handle deterministically. | -| [hmath_cffi.Tensor.__enter__](#hmath_cffitensor__enter__) | Enter a context that owns this tensor handle. | -| [hmath_cffi.Tensor.__exit__](#hmath_cffitensor__exit__args) | Close the tensor when leaving a context. | +| Area | Primary interface | Optional cffi interface | +| --- | --- | --- | +| Runtime | [initialize](#hmath.initialize), [shutdown](#hmath.shutdown) | Initialized when the module is imported. | +| Scalars | [add](#hmath.add) | [add](#hmath_cffi.add) | +| Exceptions | [HMathError](#hmath.HMathError), [status](#hmath.HMathError.status) | [HMathError](#hmath_cffi.HMathError), [status](#hmath_cffi.HMathError.status), [check](#hmath_cffi.check) | +| Construction | [Tensor](#hmath.Tensor), [scalar](#hmath.Tensor.scalar), [matrix](#hmath.Tensor.matrix), [from_numpy](#hmath.Tensor.from_numpy) | [Tensor](#hmath_cffi.Tensor) | +| Inspection | [shape](#hmath.Tensor.shape), [values](#hmath.Tensor.values), [numpy](#hmath.Tensor.numpy) | [shape](#hmath_cffi.Tensor.shape), [values](#hmath_cffi.Tensor.values) | +| Calculations | [sum](#hmath.Tensor.sum), [dot](#hmath.Tensor.dot), [+](#hmath.Tensor.__add__), [@](#hmath.Tensor.__matmul__) | [sum](#hmath_cffi.Tensor.sum), [dot](#hmath_cffi.Tensor.dot), [+](#hmath_cffi.Tensor.__add__), [@](#hmath_cffi.Tensor.__matmul__) | +| Shape changes | [reshape](#hmath.Tensor.reshape) | Not exposed. | +| Lifetime | [close](#hmath.Tensor.close), [with](#hmath.Tensor.__enter__), [exit](#hmath.Tensor.__exit__), [finalizer](#hmath.Tensor.__del__) | [close](#hmath_cffi.Tensor.close), [with](#hmath_cffi.Tensor.__enter__), [exit](#hmath_cffi.Tensor.__exit__) | ## Common contracts -- Shapes contain at most **8** nonnegative Python integers (not booleans). - Each dimension and the total size are bounded by **1,048,576**. Empty - dimensions are supported. Shape `()` is a scalar with exactly one value. -- Use `with` or `close()` to release each owned tensor. Closing one handle - preserves independent retained handles and NumPy views. -- Native failures raise `HMathError`. Shape validation raises `ValueError`; - use after close raises `RuntimeError`. Numeric conversion can also raise - Python `TypeError`, `ValueError`, or `OverflowError`. -- Primary-interface methods synchronize access to tensor handles. Prevent - concurrent close/use yourself when using the optional cffi interface. +### Numeric values and shapes + +Tensor elements and scalar calculations use finite IEEE 754 binary64 values. +Python integers are converted to this representation; arbitrary-size integer +arithmetic is not provided. Rounding can occur during conversion and arithmetic. +NaN and infinity are rejected as native inputs, and a non-finite computed result +is reported as an error rather than returned as a successful value. + +| Shape | Meaning | Required number of values | +| --- | --- | --- | +| `()` | Rank-zero scalar | 1 | +| `(n,)` | Vector | n | +| `(m, n)` | Matrix | m × n | +| `(0, n)` | Empty matrix | 0 | + +Shapes contain at most **8** dimensions. Each dimension must be a built-in +Python `int` in `[0, 1_048_576]`; `bool`, integer subclasses, and NumPy integer +scalars are not accepted as dimensions by the Python shape validator. Convert +such dimensions explicitly with `int()` when appropriate. The logical element +count must not exceed **1,048,576**. Borrowed layouts also have bounded strides +and a bounded reachable storage span. A zero dimension makes the logical count +zero, but does not waive the other layout checks. `-1` dimension inference is +not supported. + +`values` inputs are flat, in logical row-major (C) order. Nested lists belong +in `Tensor.from_numpy(...)`, not in the flat `Tensor(...)` constructor. Shape +must match exactly for elementwise addition and `dot`; there is no implicit +broadcasting. `@` accepts compatible rank-two matrices only. + +### Ownership and concurrency + +Every returned Tensor owns a native handle. Use `with` or `close()` to release +that handle. `reshape()` and NumPy views can retain the same storage through +independent references; closing one handle does not close those references. +Tensor operations do not mutate their inputs. + +The primary interface locks handle operations, including `close()`. A sequence +of separate calls is not an atomic transaction, and locking cannot protect +against writes through external NumPy aliases. In particular, entering a +`with` block does not reserve a handle against another thread closing it. +The cffi interface has no corresponding per-handle locks: callers must prevent +concurrent close/use. Process-wide shutdown requires application-level +coordination for both interfaces. See [architecture and ownership](../explanation/architecture.md). + +### Errors + +| Layer | Exception or status | Interpretation | +| --- | --- | --- | +| Python shape/size validation | `ValueError` | A Python-side dimension, rank, size, or borrowing condition is invalid. | +| Input conversion | `TypeError`, `ValueError`, `OverflowError`, or `ctypes.ArgumentError` | Conversion failed before a native checked operation; the exact class depends on the wrapper and conversion path. | +| Closed primary/cffi handle | `RuntimeError` | An operation requires a live handle. | +| Native checked operation | The interface's `HMathError` | Inspect `.status` and `str(error)`. | +| Missing optional dependency | `ImportError` | The requested NumPy or cffi functionality is unavailable. | + +Frequently relevant native codes are 1 (invalid/non-finite input), 2 (shape), +3 (overflow, non-finite result, or a work/layout budget), 4 (allocation), +5 (runtime unavailable), 6 (contained Haskell exception), and 9 (shutdown busy). +The [C ABI status table](c.md) is authoritative for names and numeric values; +not every C status is reachable from every Python operation. Exception messages +provide diagnostics, not a stable string-matching API. Allocation of Python +objects can also raise `MemoryError`; iterator and conversion hooks can propagate +their own exceptions. The entries below describe the expected API-specific cases. + +Both wrappers require ABI 1. Module import can fail before any function call if +the native library cannot be loaded or the ABI is incompatible. See +[troubleshooting](../guides/troubleshooting.md). ## Module functions ### `hmath.initialize()` -Initialize the shared numerical runtime. + + +Initialize the process-wide numerical runtime. #### Parameters @@ -71,59 +113,89 @@ None. #### Raises -`RuntimeError` for ABI incompatibility or -an irreversibly shut down runtime. +`RuntimeError` +: The native ABI is incompatible, or the runtime was permanently shut down. + +#### See Also + +[hmath.shutdown](#hmath.shutdown), [hmath.add](#hmath.add). #### Notes -Initialize the process-wide runtime; repeated calls are -allowed. Constructors and `add()` already do this automatically. +Repeated initialization is allowed. Tensor construction and `add()` already +initialize automatically. This does not create a private runtime or a resource +that each caller should later shut down. #### Examples ```python from hmath import initialize + initialize() -initialize() +initialize() # Repeated initialization is allowed. ``` -#### See also - -[hmath.shutdown](#hmathshutdown), [hmath.add](#hmathadda-b). - ### `hmath.add(a, b)` -Add two real scalars. + + +Add two finite real scalars. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `a` | `real scalar` | First finite value, convertible to C double. | -| `b` | `real scalar` | Second finite value, convertible to C double. | +| `a` | `real scalar` | First operand, convertible to C `double` by ctypes. | +| `b` | `real scalar` | Second operand, convertible to C `double` by ctypes. | #### Returns -their sum as a Python `float`, initializing the runtime if needed. +`float` +: The binary64 sum of the converted operands. #### Raises -`HMathError` for non-finite input/result, or runtime/conversion errors. +`ctypes.ArgumentError` +: ctypes cannot convert an operand to the declared C argument type. + +`RuntimeError` +: Automatic initialization finds an incompatible or shut-down runtime. + +`HMathError` +: Non-finite input (status 1), non-finite result (3), runtime failure (5), + or a contained Haskell exception (6). + +#### See Also + +[Tensor addition](#hmath.Tensor.__add__), [hmath.initialize](#hmath.initialize). + +#### Notes + +This is scalar addition, not broadcasting over a Tensor. A finite result is +not an exact-arithmetic guarantee. Use an application-appropriate tolerance for +non-exact floating-point comparisons. #### Examples ```python -from hmath import add +import math +from hmath import HMathError, add + assert add(2, 3.5) == 5.5 +assert math.isclose(add(0.1, 0.2), 0.3, rel_tol=1e-12, abs_tol=1e-15) +try: + add(float("inf"), 1.0) +except HMathError as error: + assert error.status == 1 +else: + raise AssertionError("non-finite input must be rejected") ``` -#### See also - -[hmath.Tensor.__add__](#hmathtensor__add__other-a-b), [hmath.initialize](#hmathinitialize). - ### `hmath.shutdown()` -Permanently shut down the process-owned runtime. + + +Permanently shut down the process-owned numerical runtime. #### Parameters @@ -131,247 +203,409 @@ None. #### Returns -`None`. +`None` on successful shutdown. #### Raises -`HMathError` with status 9 while work is active; -join workers and retry. Successful shutdown is **irreversible**. +`HMathError` +: Active work prevents shutdown (status 9). Join the numerical workers and + retry only after all users have finished. -Example, as the last operation of a dedicated process (not run in shared tests): +#### See Also + +[hmath.initialize](#hmath.initialize), [hmath.Tensor.close](#hmath.Tensor.close). #### Notes -Only the process owner should call this after joining -all numerical users and releasing their resources. Applications can instead -allow process exit to clean up the runtime. +Only the process owner should call this, after joining numerical users and +releasing their resources. It is not per-Tensor cleanup. A successful shutdown +is irreversible: later `initialize()` calls fail. Applications may instead let +process exit clean up the runtime. This example runs in its own process. #### Examples -```text -from hmath import shutdown +```python +from hmath import add, initialize, shutdown + +assert add(1, 2) == 3.0 shutdown() +try: + initialize() +except RuntimeError: + pass +else: + raise AssertionError("a shut-down runtime must not restart") ``` -#### See also - -[hmath.initialize](#hmathinitialize), [hmath.Tensor.close](#hmathtensorclose). - ### `hmath.HMathError(status)` + + Represent a failed native checked operation. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `status` | `int` | Native status code; available as the exception’s status attribute. | +| `status` | `int` | Native failure code captured by the wrapper. | -#### Returns +#### Attributes + +`status` : `int` +: Native failure code; see [HMathError.status](#hmath.HMathError.status). -a `RuntimeError` subclass instance with `.status` and a snapshot -of the calling thread's native diagnostic in `str(error)`. Catch exceptions -raised by operations rather than constructing this manually. +#### See Also + +[hmath.HMathError.status](#hmath.HMathError.status), [hmath_cffi.HMathError](#hmath_cffi.HMathError). + +#### Notes + +This exception inherits from `RuntimeError`. `str(error)` holds a snapshot of +the calling thread's native diagnostic. Catch exceptions raised by operations; +manually constructing this class does not perform an operation or generate a +new diagnostic. The cffi exception is a different Python class. #### Examples ```python from hmath import HMathError, Tensor + try: - Tensor([1, 2], (3,)) + with Tensor([1, 2], (3,)): + pass except HMathError as error: assert error.status == 2 + assert str(error) +else: + raise AssertionError("shape mismatch must be rejected") ``` -#### See also +### `hmath.HMathError.status` + + + +Read the native status captured by an exception. + +#### Type + +`int`. + +#### See Also + +[hmath.HMathError](#hmath.HMathError), [native status definitions](c.md). + +#### Notes -[hmath.add](#hmathadda-b), [hmath.Tensor](#hmathtensorvalues-shapenone). +The value describes the failed native call, not a later operation. Prefer the +status and exception class over exact diagnostic text when classifying failures. ## Construction ### `hmath.Tensor(values, shape=None)` -Copy flat values into a new immutable tensor. + + +Own an immutable binary64 tensor copied from flat values. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `values` | `iterable of real scalars` | Finite values in logical row-major order; at most 1,048,576 elements. | -| `shape` | `iterable of int or None; default None` | At most eight bounded nonnegative dimensions. None selects (len(values),). | +| `values` | `iterable of real scalars` | Finite values in logical row-major order. Generators are accepted; nested sequences are not flat values. | +| `shape` | `iterable of int or None; default None` | Dimensions obeying the common shape contract. `None` chooses `(number_of_values,)`. | -#### Returns +#### Raises -a new owned tensor. +`ValueError` +: Python rank/dimension validation fails, or more than 1,048,576 values + are consumed. -#### Raises +`TypeError`, `ValueError`, `OverflowError` +: The iterable or an element cannot be converted by the Python/ctypes path. + +`RuntimeError` +: Automatic initialization cannot use the runtime. + +`HMathError` +: The shape product exceeds a native budget (3), count differs from the shape + product (2), an element is non-finite (1), or native allocation fails (4). -`ValueError` for Python shape/size -bounds; `HMathError` for mismatched element count, non-finite input, or allocation -failure; ordinary numeric conversion errors may also occur. +#### Attributes + +`shape` : `tuple[int, ...]` +: Read-only dimensions; see [Tensor.shape](#hmath.Tensor.shape). + +#### See Also + +[hmath.Tensor.scalar](#hmath.Tensor.scalar), [hmath.Tensor.matrix](#hmath.Tensor.matrix), [hmath.Tensor.from_numpy](#hmath.Tensor.from_numpy). #### Notes -`values`: finite numeric iterable in flat row-major order; -`shape`: optional iterable of dimensions, defaulting to `(len(values),)`. -Values are copied, so later changes to the input do not affect the tensor. -At most 1,048,577 entries are consumed before oversized input is rejected. +The constructor consumes at most 1,048,577 values to detect oversized input. +It copies values, so later changes to a source list do not affect the tensor. +The object owns one handle; use a context manager or `close()`. + +Shape `()` requires exactly one value; shape `(1,)` describes a vector instead. +For shape `(m, n)`, flat position `i*n + j` corresponds to row `i`, column `j`. +A rank-zero tensor and a one-element vector are not interchangeable operands +for operations requiring equal shapes. #### Examples ```python -with Tensor([1, 2, 3, 4], (2, 2)) as matrix: - assert matrix.shape == (2, 2) - assert matrix.values() == [1., 2., 3., 4.] -``` - -#### See also +from hmath import Tensor -[hmath.Tensor.matrix](#hmathtensormatrixvalues-rows-columns), [hmath.Tensor.scalar](#hmathtensorscalarvalue), [hmath.Tensor.from_numpy](#hmathtensorfrom_numpyarray-copytrue). +source = [1, 2, 3, 4, 5, 6] +with Tensor(source, (2, 3)) as matrix: + source[0] = 99 + assert matrix.shape == (2, 3) + assert matrix.values() == [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] +with Tensor([], (0, 3)) as empty: + assert empty.shape == (0, 3) + assert empty.values() == [] +``` ### `hmath.Tensor.scalar(value)` -Construct a rank-zero tensor. + + +Construct a rank-zero tensor from one finite value. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `value` | `real scalar` | Single finite value; the resulting shape is (). | +| `value` | `real scalar` | Value converted to binary64; integers do not remain integer-typed storage. | #### Returns -a new owned rank-zero tensor -with shape `()`. +`Tensor` +: New owned tensor with shape `()` and exactly one element. #### Raises -the constructor's conversion and native errors. +`TypeError`, `ValueError`, `OverflowError` +: The value cannot be converted by the constructor. + +`RuntimeError` +: Automatic initialization fails. + +`HMathError` +: The converted value is non-finite (1), or native allocation fails (4). + +#### See Also + +[hmath.Tensor](#hmath.Tensor), [hmath.Tensor.shape](#hmath.Tensor.shape). #### Examples ```python -with Tensor.scalar(7) as scalar: +from hmath import Tensor + +with Tensor.scalar(7) as scalar, Tensor([7]) as vector: assert scalar.shape == () - assert scalar.values() == [7.0] + assert vector.shape == (1,) + assert scalar.values() == vector.values() == [7.0] ``` -#### See also - -[hmath.Tensor](#hmathtensorvalues-shapenone), [hmath.Tensor.matrix](#hmathtensormatrixvalues-rows-columns). - ### `hmath.Tensor.matrix(values, rows, columns)` + + Construct a rank-two matrix from flat row-major values. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `values` | `iterable of real scalars` | Exactly rows * columns finite values. | -| `rows` | `int` | Nonnegative row count, at most 1,048,576; bool is rejected. | -| `columns` | `int` | Nonnegative column count, at most 1,048,576; bool is rejected. | +| `values` | `iterable of real scalars` | Exactly `rows * columns` finite values in row-major order. | +| `rows` | `int` | Built-in Python integer in `[0, 1_048_576]`; booleans are rejected. | +| `columns` | `int` | Built-in Python integer in `[0, 1_048_576]`; booleans are rejected. | #### Returns -a new owned rank-two tensor. +`Tensor` +: New owned tensor with shape `(rows, columns)`. #### Raises -constructor errors. +`ValueError` +: Invalid dimensions or an oversized value sequence. + +`TypeError`, `OverflowError` +: Values cannot be converted to the constructor's numeric representation. + +`RuntimeError` +: Automatic initialization fails. + +`HMathError` +: Count mismatch (2), native layout budget (3), non-finite values (1), + or allocation failure (4). Conversion hooks may also propagate `ValueError`. + +#### See Also + +[hmath.Tensor](#hmath.Tensor), [matrix multiplication](#hmath.Tensor.__matmul__). #### Notes -flat row-major values and two nonnegative integer dimensions; -exactly `rows * columns` values are required. +Equivalent to `Tensor(values, (rows, columns))`. Zero rows or zero columns +require an empty value sequence. This constructor does not infer dimensions. #### Examples ```python +from hmath import Tensor + with Tensor.matrix([1, 2, 3, 4, 5, 6], 2, 3) as matrix: assert matrix.shape == (2, 3) + assert matrix.values() == [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] ``` -#### See also - -[hmath.Tensor](#hmathtensorvalues-shapenone), [hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b). - ### `hmath.Tensor.from_numpy(array, copy=True)` -Convert an array to a tensor, copying or borrowing its storage. + + +Create a tensor by copying or borrowing NumPy-compatible data. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `array` | `numpy.ndarray or array-like` | Input whose shape is retained; borrowing requirements are below. | -| `copy` | `bool; default True` | True converts to float64 and copies; False borrows eligible read-only storage. | +| `array` | `numpy.ndarray or array-like` | Input normalized with `numpy.asarray`; shape is preserved. | +| `copy` | `bool; default True` | `True` converts to float64 and copies. `False` borrows only eligible read-only storage; there is no copy fallback. | #### Returns -an owned tensor preserving shape. +`Tensor` +: New owned handle with the normalized array's shape. In borrowing mode the + handle retains the normalized NumPy array as its storage owner. #### Raises -`ImportError` without -NumPy, `ValueError` for borrowing/rank/size violations, or `HMathError` for native -storage validation, non-finite data, or allocation failures. +`ImportError` +: NumPy is not installed. + +`ValueError` +: Rank/element or Python dimension bounds fail; borrowing receives a writable, + misaligned, non-native-endian, or non-float64 array; or strides are negative + or not whole multiples of eight bytes. NumPy conversion may also raise it. + +`TypeError`, `OverflowError` +: NumPy/Python cannot convert the input. + +`RuntimeError` +: Automatic initialization fails. + +`HMathError` +: Native layout/span budgets (3 or 8), non-finite reachable elements (1), + or native allocation failure (4). + +#### See Also + +[hmath.Tensor.numpy](#hmath.Tensor.numpy), [hmath.Tensor.reshape](#hmath.Tensor.reshape), [NumPy guide](../guides/numpy.md). #### Notes -`array`: NumPy array or array-like input; `copy`: whether to copy. -With `True`, convert to `float64` and copy in row-major order. With `False`, -borrow aligned, native-endian, **read-only** `float64` storage with nonnegative -strides divisible by eight bytes. The tensor keeps the array alive. The caller -must prevent writes through every alias until all borrowed tensors/views die. +With `copy=True`, NumPy converts to `float64`, the wrapper flattens in C order, +and HMath copies the elements. Input strides need not be contiguous and the +source may be writable. The resulting Tensor has independent contiguous storage. + +With `copy=False`, the normalized array must already be aligned, native-endian, +read-only `float64`. Nonnegative strides, including zero strides, must be +multiples of eight bytes; positive-stride non-contiguous views are supported +within the native layout budgets. Native strides count elements, while NumPy +strides count bytes. For a nonempty shape, reachable span in elements is +`1 + sum((dimension - 1) * element_stride)`; logical count and span can differ. +Native validation examines reachable values for finiteness. + +Borrowing avoids copying the element buffer, not validation, object allocation, +or metadata allocation. Initial `numpy.asarray` normalization of an array-like +object may itself allocate. Passing an eligible ndarray makes the intended +borrowed-buffer relationship explicit. + +Keep every alias to the borrowed storage immutable until all derived tensors +and NumPy views are released. Setting one array read-only does not prevent a +write through a different writable alias. Use `copy=True` when ownership or +immutability cannot be coordinated. #### Examples ```python import numpy as np -array = np.array([[1., 2.], [3., 4.]]) -array.setflags(write=False) -with Tensor.from_numpy(array, copy=False) as tensor: - assert tensor.sum() == 10.0 +from hmath import Tensor + +source = np.arange(12, dtype=np.float64).reshape(3, 4) +source.setflags(write=False) +with Tensor.from_numpy(source[:, ::2], copy=False) as tensor: + view = tensor.numpy(copy=False) + assert tensor.shape == (3, 2) + assert tensor.values() == [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] + assert np.shares_memory(view, source) + assert not view.flags.writeable +assert view.sum() == 30.0 # The view survives closing this Tensor handle. +del view ``` -#### See also +```python +import numpy as np +from hmath import Tensor -[hmath.Tensor.numpy](#hmathtensornumpycopytrue), [hmath.Tensor](#hmathtensorvalues-shapenone), [hmath.Tensor.close](#hmathtensorclose). +source = np.arange(4, dtype=np.float64) # Writable. +try: + with Tensor.from_numpy(source, copy=False): + pass +except ValueError: + pass +else: + raise AssertionError("writable borrowing must be rejected") +with Tensor.from_numpy(source) as copied: + source[0] = 99 + assert copied.values()[0] == 0.0 +``` ## Inspection and conversion ### `hmath.Tensor.shape` -Read the tensor’s dimensions. + -#### Parameters +Read the dimensions of a live tensor. -None. - -#### Returns +#### Type -a tuple of dimensions, -including `()` for scalars. +`tuple[int, ...]`; this is a read-only property, not a method. #### Raises -`RuntimeError` after close or -`HMathError` if native metadata retrieval fails. +`RuntimeError` +: The handle is closed. + +`HMathError` +: Native metadata retrieval fails. + +#### See Also + +[hmath.Tensor](#hmath.Tensor), [hmath.Tensor.reshape](#hmath.Tensor.reshape). + +#### Notes + +Shape `()` denotes one scalar, while a shape containing zero denotes an +empty tensor. Reading the property does not copy the element buffer. #### Examples ```python -with Tensor([], (0, 3)) as empty: +from hmath import Tensor + +with Tensor.scalar(2) as scalar, Tensor([], (0, 3)) as empty: + assert scalar.shape == () assert empty.shape == (0, 3) ``` -#### See also - -[hmath.Tensor.reshape](#hmathtensorreshapeshape), [hmath.Tensor.values](#hmathtensorvalues). - ### `hmath.Tensor.values()` + + Copy logical elements to a flat Python list. #### Parameters @@ -380,69 +614,109 @@ None. #### Returns -an independent flat `list[float]` in logical -row-major order, including for strided input; `[]` for empty tensors. +`list[float]` +: Independent values in logical row-major order. Empty tensors produce `[]`; + scalars produce a one-element list. #### Raises -closed-handle or native copy errors. +`RuntimeError` +: The handle is closed. + +`HMathError` +: Native copy validation fails. + +#### See Also + +[hmath.Tensor.numpy](#hmath.Tensor.numpy), [hmath.Tensor.shape](#hmath.Tensor.shape). + +#### Notes + +Strided input is gathered in logical order, not raw storage-span order. +The list remains valid after the Tensor closes. Copying N elements takes +O(N) output space; this is not a zero-copy access method. #### Examples ```python +from hmath import Tensor + with Tensor([2, 4]) as tensor: values = tensor.values() -values[0] = 99 # Independent list remains usable after close. + values[0] = 99.0 + assert tensor.values() == [2.0, 4.0] +assert values == [99.0, 4.0] ``` -#### See also - -[hmath.Tensor.numpy](#hmathtensornumpycopytrue), [hmath.Tensor.shape](#hmathtensorshape). - ### `hmath.Tensor.numpy(copy=True)` -Return a NumPy copy or a read-only view. + + +Return a NumPy copy or a read-only view of tensor storage. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `copy` | `bool; default True` | True returns an independent writable copy; False returns a read-only view. | +| `copy` | `bool; default True` | `True` returns independent writable storage; `False` returns a read-only view for nonempty storage. | #### Returns -`numpy.ndarray` of `float64`. +`numpy.ndarray` +: Float64 array with the tensor's logical shape. A non-copying view preserves + the tensor's strides and retains storage independently of this handle. #### Raises -`ImportError` without NumPy, -closed-handle errors, or native metadata/data errors. +`ImportError` +: NumPy is unavailable. + +`RuntimeError` +: The handle is closed. + +`HMathError` +: Native data, metadata, or copy retrieval fails. + +#### See Also + +[hmath.Tensor.from_numpy](#hmath.Tensor.from_numpy), [hmath.Tensor.values](#hmath.Tensor.values), [hmath.Tensor.close](#hmath.Tensor.close). #### Notes -`copy`: `True` for a writable independent copy, `False` for a -read-only native view with the tensor's shape and strides. Views retain storage -and borrowed owners even after this tensor closes. Do not mutate borrowed aliases. +A copy is writable and unaffected by closing the tensor. A view is read-only +and retains both native storage and any borrowed NumPy owner. Closing the +original Tensor therefore does not invalidate the view or its derived views. +For empty storage, `copy=False` may return a separate empty read-only NumPy +allocation; there are no elements whose sharing can be demonstrated. + +Read-only views do not make other aliases to borrowed storage read-only. +The borrowing immutability contract still applies. View construction allocates +Python/NumPy metadata even when it does not copy elements. #### Examples ```python +import numpy as np +from hmath import Tensor + with Tensor([1, 2, 3]) as tensor: + copied = tensor.numpy() view = tensor.numpy(copy=False) -assert view.tolist() == [1., 2., 3.] + copied[0] = 99 + assert tensor.values() == [1.0, 2.0, 3.0] + assert not np.shares_memory(copied, view) +assert view.tolist() == [1.0, 2.0, 3.0] assert not view.flags.writeable del view ``` -#### See also - -[hmath.Tensor.from_numpy](#hmathtensorfrom_numpyarray-copytrue), [hmath.Tensor.values](#hmathtensorvalues), [hmath.Tensor.close](#hmathtensorclose). - ## Calculations ### `hmath.Tensor.sum()` -Sum all tensor elements to one scalar. + + +Sum every logical element to one scalar. #### Parameters @@ -450,168 +724,336 @@ None. #### Returns -a `float`, or `0.0` for an empty tensor. +`float` +: The sum of all elements, or `0.0` for an empty tensor. #### Raises -closed-handle errors or native arithmetic/runtime errors, including -non-finite results. +`RuntimeError` +: A required Tensor handle is closed. + +`HMathError` +: Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). + +#### See Also + +[hmath.Tensor.dot](#hmath.Tensor.dot), [numerical conventions](../explanation/numerical-conventions.md). + +#### Notes + +For N logical elements, the mathematical operation is + +$$ +s = \sum_{i=0}^{N-1} x_i. +$$ + +This method has no `axis`, `dtype`, or `keepdims` argument: even a matrix is +reduced to a Python float. A scalar Tensor contributes its one value. +The native boundary gathers values in logical row-major order and the current +Haskell reference kernel uses ordinary `sum`, not a documented compensated +summation algorithm. Reduction work is O(N); the current boundary also uses +O(N) gathered storage. An empty reduction has constant overhead. + +Finite-result checking does not eliminate rounding or cancellation, and no +universal relative-error or correctly-rounded-result guarantee is made. #### Examples ```python -with Tensor([1, 2, 3]) as tensor: - assert tensor.sum() == 6.0 -``` - -#### See also +import math +from hmath import Tensor -[hmath.Tensor.dot](#hmathtensordotother). +with Tensor([1, 2, 3, 4], (2, 2)) as matrix: + assert matrix.sum() == 10.0 +with Tensor([], (0, 3)) as empty: + assert empty.sum() == 0.0 +with Tensor([0.1, 0.2]) as samples: + assert math.isclose(samples.sum(), 0.3, rel_tol=1e-12, abs_tol=1e-15) +``` ### `hmath.Tensor.dot(other)` -Reduce corresponding products of two identically shaped tensors. + + +Sum corresponding products of two identically shaped tensors. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `Tensor` | Live tensor with exactly the same shape as this tensor. | +| `other` | `Tensor` | Live primary-interface Tensor with exactly the same shape, including rank. | #### Returns -a `float`, or `0.0` for matching empty tensors. +`float` +: Scalar sum of corresponding products at every rank; `0.0` for matching empty tensors. #### Raises -`HMathError` for shape/arithmetic/runtime errors; closed operands -raise `RuntimeError`. +`RuntimeError` +: A required Tensor handle is closed. + +`HMathError` +: Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). + +`HMathError` with status 2 +: The operand shapes differ, even when their total element counts are equal. + +#### See Also + +[hmath.Tensor.sum](#hmath.Tensor.sum), [matrix multiplication](#hmath.Tensor.__matmul__). #### Notes -a live tensor with exactly the same shape. Sum corresponding -products over the entire tensor; this is a scalar reduction at every rank. +For equal shapes and logical flattened elements, the mathematical definition is + +$$ +d = \sum_{i=0}^{N-1} a_i b_i. +$$ + +This is not NumPy's dimension-dependent `dot`: two matrices produce a scalar +sum of elementwise products, not a matrix product. Use `a @ b` for matrix +multiplication. There is no broadcasting or mixing of `hmath.Tensor` and +`hmath_cffi.Tensor` operands. + +The current native boundary gathers both inputs; the Haskell reference kernel +forms elementwise products and sums them. Arithmetic work and gathered input +storage are O(N), with ordinary binary64 rounding. It does not promise +compensated accumulation or exact products. + +#### References + +[NumPy dot](https://numpy.org/doc/stable/reference/generated/numpy.dot.html) +documents the different NumPy semantics; it is not a compatibility claim. #### Examples ```python -with Tensor([1, 2, 3]) as a, Tensor([4, 5, 6]) as b: - assert a.dot(b) == 32.0 -``` - -#### See also +from hmath import HMathError, Tensor -[hmath.Tensor.sum](#hmathtensorsum), [hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b). +with Tensor([1, 2, 3, 4], (2, 2)) as a: + with Tensor([5, 6, 7, 8], (2, 2)) as b: + assert a.dot(b) == 70.0 + with a @ b as product: + assert product.values() == [19.0, 22.0, 43.0, 50.0] + with Tensor([1, 2, 3, 4], (4,)) as flat: + try: + a.dot(flat) + except HMathError as error: + assert error.status == 2 + else: + raise AssertionError("equal counts do not imply equal shapes") +``` ### `hmath.Tensor.__add__(other)` — `a + b` + + Add two tensors element by element. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `Tensor` | Live tensor of the same shape; broadcasting is unsupported. | +| `other` | `Tensor` | Live primary-interface Tensor with exactly the same shape. | #### Returns -a new owned tensor of elementwise sums. Inputs are unchanged. +`Tensor` +: New owned tensor with the same shape and newly allocated result storage. + Neither input is changed. #### Raises -native shape/allocation/arithmetic/runtime or closed-handle errors. +`RuntimeError` +: A required Tensor handle is closed. + +`HMathError` +: Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). + +`HMathError` with status 2 +: The shapes do not match. + +#### See Also + +[hmath.add](#hmath.add), [hmath.Tensor.dot](#hmath.Tensor.dot). + +#### Notes + +For each logical position, `result[i] = a[i] + b[i]`. Scalars represented +as rank-zero tensors can be added to other rank-zero tensors. `tensor + 1` +is not a supported scalar-broadcast operation. Work and result storage are O(N), +with additional O(N) scratch storage in the current native boundary. #### Examples ```python +from hmath import Tensor + with Tensor([1, 2]) as a, Tensor([3, 4]) as b: with a + b as result: - assert result.values() == [4., 6.] + assert result.values() == [4.0, 6.0] + assert a.values() == [1.0, 2.0] ``` -#### See also - -[hmath.Tensor.dot](#hmathtensordotother), [hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b). - ### `hmath.Tensor.__matmul__(other)` — `a @ b` -Multiply two rank-two matrices. + + +Multiply two compatible rank-two matrices. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `Tensor` | Live (k, n) matrix when this tensor has shape (m, k). | +| `other` | `Tensor` | Live matrix of shape `(k, n)` when this matrix has shape `(m, k)`. | #### Returns -a new owned matrix. +`Tensor` +: New owned matrix of shape `(m, n)` with independent result storage. #### Raises -native rank/inner-dimension, -size/work budget, allocation, arithmetic/runtime, or closed-handle errors. +`RuntimeError` +: A required Tensor handle is closed. + +`HMathError` +: Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). + +`HMathError` with status 2 +: An operand is not rank two or the inner dimensions differ. + +`HMathError` with status 3 +: Output size or the scalar-operation work budget is exceeded. + +#### See Also + +[hmath.Tensor.matrix](#hmath.Tensor.matrix), [hmath.Tensor.dot](#hmath.Tensor.dot), [matrix calculation guide](../guides/matrix-calculation.md). #### Notes -a live rank-two tensor. Shapes `(m, k)` and `(k, n)` produce -`(m, n)`. Work is bounded by **16,777,216** scalar multiply-add steps (`m*k*n`). -This binding does not expose batched multiplication. +The mathematical definition is + +$$ +C_{ij} = \sum_{p=0}^{k-1} A_{ip} B_{pj}, +\qquad C \in \mathbb{R}^{m \times n}. +$$ + +The output must contain at most **1,048,576** elements and `m*k*n` must not +exceed **16,777,216**. This is a work budget, not a precision target or a promise +of hardware fused multiply-add instructions. Vectors, batched products, and +broadcasting are not supported by this binding. + +For `k == 0`, compatible `(m, 0)` and `(0, n)` inputs yield an `(m, n)` zero +matrix, subject to the output budget. A zero outer dimension produces an empty +result. The current Haskell reference loop accumulates each cell in increasing +inner-index order. It performs O(m*k*n) arithmetic and produces O(m*n) output; +gathering inputs and preparing output also costs O(m*k + k*n + m*n), including +when an arithmetic dimension is zero. Scratch buffers are used. Do not infer +BLAS acceleration from other Haskell modules. #### Examples ```python -with Tensor.matrix([1, 2, 3, 4], 2, 2) as a: - with a @ a as squared: - assert squared.values() == [7., 10., 15., 22.] +from hmath import Tensor + +with Tensor.matrix([1, 2, 3, 4, 5, 6], 2, 3) as a: + with Tensor.matrix([7, 8, 9, 10, 11, 12], 3, 2) as b: + with a @ b as result: + assert result.shape == (2, 2) + assert result.values() == [58.0, 64.0, 139.0, 154.0] +with Tensor([], (2, 0)) as a, Tensor([], (0, 3)) as b: + with a @ b as result: + assert result.shape == (2, 3) + assert result.values() == [0.0] * 6 ``` -#### See also - -[hmath.Tensor.matrix](#hmathtensormatrixvalues-rows-columns), [hmath.Tensor.dot](#hmathtensordotother). - ### `hmath.Tensor.reshape(shape)` -Create a new shape over the same contiguous storage. + + +Create an independently owned handle with a new shape over shared storage. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `shape` | `iterable of int` | Nonnegative dimensions with unchanged element count; -1 inference is unsupported. | +| `shape` | `iterable of int` | Built-in nonnegative dimensions with the same total element count; `-1` inference is unsupported. | #### Returns -a new owned handle sharing storage; both handles can close independently. +`Tensor` +: New handle sharing the contiguous element storage, not a new element copy. + Original and result handles can be closed independently. #### Raises -`ValueError` for invalid dimensions, `HMathError` for incompatible -count or non-contiguous input, or `RuntimeError` after close. +`ValueError` +: The Python shape/rank contract is violated. + +`TypeError` +: `shape` is not iterable. + +`RuntimeError` +: This handle is closed. + +`HMathError` +: Count mismatch or non-contiguous storage (2), native layout budget (3), + or new-handle allocation failure (4). + +#### See Also + +[hmath.Tensor.shape](#hmath.Tensor.shape), [hmath.Tensor.from_numpy](#hmath.Tensor.from_numpy). #### Notes -new dimensions with unchanged element count. Storage must be -contiguous. Negative dimensions, including `-1` for inference, are unsupported. +No element reordering or copy fallback is performed. Contiguity is checked +by the native layout validator: singleton dimensions do not constrain strides, +and empty tensors are considered contiguous. A positive-stride borrowed view +can still be non-contiguous and rejected. The reshaped handle retains any +borrowed owner. Validation and metadata construction depend on rank, not on +copying all elements. #### Examples ```python +from hmath import Tensor + with Tensor([1, 2, 3, 4]) as flat: reshaped = flat.reshape((2, 2)) with reshaped: assert reshaped.shape == (2, 2) + assert reshaped.values() == [1.0, 2.0, 3.0, 4.0] ``` -#### See also +```python +import numpy as np +from hmath import HMathError, Tensor -[hmath.Tensor.shape](#hmathtensorshape), [hmath.Tensor](#hmathtensorvalues-shapenone). +source = np.arange(8, dtype=np.float64) +source.setflags(write=False) +with Tensor.from_numpy(source[::2], copy=False) as strided: + try: + with strided.reshape((2, 2)): + pass + except HMathError as error: + assert error.status == 2 + else: + raise AssertionError("non-contiguous reshape must be rejected") +``` ## Lifetime protocol ### `hmath.Tensor.close()` -Release this tensor handle deterministically. + + +Release this Tensor handle deterministically. #### Parameters @@ -619,24 +1061,40 @@ None. #### Returns -`None`; repeated calls are harmless. Other retained handles/views -remain valid. Using this closed handle raises `RuntimeError`. +`None`; repeated calls are harmless. + +#### See Also + +[hmath.Tensor.__enter__](#hmath.Tensor.__enter__), [hmath.Tensor.__exit__](#hmath.Tensor.__exit__), [hmath.Tensor.__del__](#hmath.Tensor.__del__). + +#### Notes + +After closing, handle-dependent operations raise `RuntimeError`. Retained +Tensor handles and NumPy views remain valid. This does not shut down the +process-wide runtime. Closing is synchronized with primary-interface handle +operations, not with arbitrary external writes to borrowed storage. #### Examples ```python +from hmath import Tensor + tensor = Tensor([1]) tensor.close() tensor.close() +try: + tensor.values() +except RuntimeError: + pass +else: + raise AssertionError("closed handles must not be used") ``` -#### See also - -[hmath.Tensor.__enter__](#hmathtensor__enter__), [hmath.Tensor.__exit__](#hmathtensor__exit__args), [hmath.Tensor.__del__](#hmathtensor__del__). - ### `hmath.Tensor.__enter__()` -Enter a context that owns this tensor handle. + + +Enter a context for this live Tensor handle. #### Parameters @@ -644,40 +1102,68 @@ None. #### Returns -the same live tensor for a `with` block; -this creates no additional reference. +`Tensor` +: The same object, not an additional retained handle. #### Raises -`RuntimeError` after close. -See the context-manager examples above. +`RuntimeError` +: The handle is already closed. + +#### See Also -#### See also +[hmath.Tensor.__exit__](#hmath.Tensor.__exit__), [hmath.Tensor.close](#hmath.Tensor.close). -[hmath.Tensor.close](#hmathtensorclose), [hmath.Tensor.__exit__](#hmathtensor__exit__args). +#### Notes + +Use `with tensor:` or `with Tensor(...) as tensor:`. Nested contexts over +the same object do not create nested ownership: the inner exit closes that +handle. Create an independent retained result when independent lifetimes are needed. ### `hmath.Tensor.__exit__(*args)` -Close the tensor when leaving a context. + + +Close the handle when leaving a context. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `args` | `exception triple` | Python supplies (exception type, exception value, traceback); exceptions are not suppressed. | +| `args` | `exception triple` | Python supplies exception type, exception value, and traceback, or three `None` values on normal exit. | #### Returns -`None`, closing the tensor on normal or exceptional block exit -without suppressing the exception. See the context-manager examples above. +`None`. The original exception is not suppressed. -#### See also +#### See Also -[hmath.Tensor.close](#hmathtensorclose), [hmath.Tensor.__enter__](#hmathtensor__enter__). +[hmath.Tensor.__enter__](#hmath.Tensor.__enter__), [hmath.Tensor.close](#hmath.Tensor.close). + +#### Examples + +```python +from hmath import Tensor + +tensor = Tensor([1]) +try: + with tensor: + raise ValueError("example failure") +except ValueError: + pass +try: + tensor.sum() +except RuntimeError: + pass +else: + raise AssertionError("exceptional exit must close the handle") +``` ### `hmath.Tensor.__del__()` -Release the handle as a garbage-collection fallback. + + +Release the handle as a finalization fallback. #### Parameters @@ -685,131 +1171,264 @@ None. #### Returns -`None`, calling `close()` as a finalization -fallback. Collection timing is unspecified; use `with` for deterministic cleanup. +`None`. + +#### See Also -#### See also +[hmath.Tensor.close](#hmath.Tensor.close), [hmath.Tensor.__enter__](#hmath.Tensor.__enter__). -[hmath.Tensor.close](#hmathtensorclose). +#### Notes + +Calls `close()` as a fallback. Do not rely on collection timing or call +this method directly as the usual cleanup API. Use `with` or `close()`. ## Optional cffi interface -Install `hmath-python[cffi]` to use `hmath_cffi`. Both wrappers share the native -runtime. Prevent concurrent close/use of a cffi tensor. NumPy conversion, -`reshape`, `scalar`, and `matrix` constructors belong to the primary interface. +Install `hmath-python[cffi]` and import `hmath_cffi`. Import verifies ABI 1 and +initializes the shared runtime. ABI/runtime problems can raise `RuntimeError` +at import; missing cffi raises `ImportError`. + +Use only cffi Tensor operands with cffi operations. These objects are not +interchangeable with primary-interface Tensor objects. The mathematical +contracts and native budgets are shared, but Python conversion paths, +exception classes, initialization timing, and handle synchronization differ. +NumPy conversion, `reshape`, `scalar`, and `matrix` are not exposed here. + +The common import alias used below is `hc`. Each example defines it independently. ### `hmath_cffi.add(a, b)` -Add two real scalars. + + +Add two finite scalars through the cffi binding. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `a` | `real scalar` | First finite value, convertible to C double. | -| `b` | `real scalar` | Second finite value, convertible to C double. | +| `a` | `real scalar` | First operand, convertible to C `double` by cffi. | +| `b` | `real scalar` | Second operand, convertible to C `double` by cffi. | #### Returns -their binary64 sum as a -Python float. +`float` +: Binary64 sum. #### Raises -`hmath_cffi.HMathError` for native failures or ordinary -conversion errors. Import initializes the runtime and checks ABI 1. +`TypeError`, `OverflowError` +: cffi cannot convert an operand; conversion hooks can propagate other exceptions. + +`hmath_cffi.HMathError` +: Non-finite input (1), non-finite result (3), unavailable runtime (5), + or contained Haskell exception (6). + +#### See Also + +[hmath.add](#hmath.add), [hmath_cffi.HMathError](#hmath_cffi.HMathError). + +#### Notes + +Unlike primary `add()`, this function does not itself reinitialize the runtime. +Initialization happens during module import. #### Examples ```python import hmath_cffi as hc -assert hc.add(2, 3) == 5.0 -``` - -#### See also -[hmath.add](#hmathadda-b), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). +assert hc.add(2, 3.5) == 5.5 +``` ### `hmath_cffi.HMathError(status)` -Represent a failed native checked operation. + + +Represent a failed native operation through the cffi interface. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `status` | `int` | Native status code; available as the exception’s status attribute. | +| `status` | `int` | Native failure code captured by this wrapper. | -#### Returns +#### Attributes + +`status` : `int` +: Captured code; see [cffi HMathError.status](#hmath_cffi.HMathError.status). -an exception with `.status` -and a snapshot of the current native error message. Catch this class for cffi -errors; it is a separate class from `hmath.HMathError`. +#### See Also -#### See also +[hmath.HMathError](#hmath.HMathError), [hmath_cffi.check](#hmath_cffi.check). -[hmath.HMathError](#hmathhmatherrorstatus), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). +#### Notes + +Inherits from `RuntimeError`, but is not `hmath.HMathError`. Catch the +exception class belonging to the interface being used. The message snapshots +the calling thread's diagnostic; manual construction reads the current native +message rather than performing a new operation. + +#### Examples + +```python +import hmath_cffi as hc + +try: + with hc.Tensor([1, 2], (3,)): + pass +except hc.HMathError as error: + assert error.status == 2 +else: + raise AssertionError("shape mismatch must be rejected") +``` + +### `hmath_cffi.HMathError.status` + + + +Read the native status captured by a cffi exception. + +#### Type + +`int`. + +#### See Also + +[hmath_cffi.HMathError](#hmath_cffi.HMathError), [native status definitions](c.md). + +#### Notes + +The same native numeric code definitions apply to both wrappers, even +though their Python exception classes differ. ### `hmath_cffi.check(status)` -Convert a raw native status into a Python exception. + + +Raise a cffi exception for a nonzero native status. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `status` | `int` | Status returned by the immediately preceding checked C call. | +| `status` | `int` | Status from the immediately preceding checked native operation. | #### Returns -`None` for zero. +`None` for status zero. #### Raises -`hmath_cffi.HMathError` for nonzero. Call immediately after the raw -call so its thread-local error message has not been replaced. +`hmath_cffi.HMathError` +: The supplied status is nonzero; `.status` is the supplied code. + +#### See Also -#### See also +[hmath_cffi.HMathError](#hmath_cffi.HMathError), [hmath_cffi.add](#hmath_cffi.add). -[hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). +#### Notes + +The wrapper uses this helper internally. If using it with a raw checked +call, invoke it immediately on the same thread so another operation cannot +replace the diagnostic. High-level operations already call it; users do not +need to wrap `Tensor.sum()` or `add()` in another `check()`. + +#### Examples + +```python +import hmath_cffi as hc + +assert hc.check(0) is None +``` ### `hmath_cffi.Tensor(values, shape=None)` -Copy flat values into a new immutable tensor. + + +Own an immutable tensor through the optional cffi binding. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `values` | `iterable of real scalars` | Finite values in logical row-major order; at most 1,048,576 elements. | -| `shape` | `iterable of int or None; default None` | At most eight bounded nonnegative dimensions. None selects (len(values),). | +| `values` | `iterable of real scalars` | Flat finite row-major values; at most 1,048,576 elements. | +| `shape` | `iterable of int or None; default None` | Built-in Python dimensions under the common shape contract. `None` selects `(number_of_values,)`. | -#### Returns +#### Raises -an owned cffi tensor with `.shape` -stored as a tuple. +`ValueError` +: Rank, Python dimension, or input-count validation fails. -#### Raises +`TypeError`, `OverflowError` +: cffi cannot convert values; conversion hooks can propagate other exceptions. + +`hmath_cffi.HMathError` +: Count mismatch (2), layout budget (3), non-finite data (1), or native + allocation failure (4). + +#### Attributes -`ValueError` for Python bounds, -`hmath_cffi.HMathError` for native input/allocation errors, or conversion errors. +`shape` : `tuple[int, ...]` +: Cached dimensions; see [cffi Tensor.shape](#hmath_cffi.Tensor.shape). + +#### See Also + +[hmath.Tensor](#hmath.Tensor), [hmath_cffi.Tensor.close](#hmath_cffi.Tensor.close). + +#### Notes + +Values are copied. The constructor consumes at most 1,048,577 values to +detect oversized input. Use `with` or `close()`; do not close this handle +concurrently with an operation using it. Runtime initialization occurs on +module import. Garbage-collection cleanup is a fallback, not a deterministic +replacement for explicit cleanup. #### Examples ```python -with hc.Tensor([1, 2, 3, 4], (2, 2)) as a: - assert a.shape == (2, 2) - with a @ a as result: - assert result.values() == [7., 10., 15., 22.] +import hmath_cffi as hc + +with hc.Tensor([1, 2, 3, 4], (2, 2)) as matrix: + assert matrix.shape == (2, 2) + with matrix @ matrix as squared: + assert squared.values() == [7.0, 10.0, 15.0, 22.0] ``` -#### See also +### `hmath_cffi.Tensor.shape` + + + +Read the cached shape of a cffi Tensor. -[hmath.Tensor](#hmathtensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose), [hmath_cffi.check](#hmath_cfficheckstatus). +#### Type + +`tuple[int, ...]`. + +#### See Also + +[hmath_cffi.Tensor](#hmath_cffi.Tensor), [hmath.Tensor.shape](#hmath.Tensor.shape). + +#### Notes + +Unlike the primary interface's property, this is a stored Python attribute +and remains readable after `close()`. Treat it as read-only: assigning to it +does not reshape native storage and can make Python metadata inconsistent. + +#### Examples + +```python +import hmath_cffi as hc + +with hc.Tensor([1, 2]) as tensor: + assert tensor.shape == (2,) +assert tensor.shape == (2,) # Cached metadata, not evidence of a live handle. +``` ### `hmath_cffi.Tensor.values()` -Copy logical elements to a flat Python list. + + +Copy a cffi Tensor to a flat Python list. #### Parameters @@ -817,20 +1436,36 @@ None. #### Returns -an independent row-major `list[float]`. +`list[float]` +: Independent row-major values; `[]` for an empty tensor. #### Raises -`RuntimeError` after close or `hmath_cffi.HMathError` on copy failure. -Use `.values()` inside the context-manager example above. +`RuntimeError` +: The handle is closed. + +`hmath_cffi.HMathError` +: Native copy validation fails. + +#### See Also + +[hmath.Tensor.values](#hmath.Tensor.values), [hmath_cffi.Tensor.close](#hmath_cffi.Tensor.close). + +#### Examples -#### See also +```python +import hmath_cffi as hc -[hmath.Tensor.values](#hmathtensorvalues), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). +with hc.Tensor([2, 4]) as tensor: + values = tensor.values() +assert values == [2.0, 4.0] +``` ### `hmath_cffi.Tensor.sum()` -Sum all tensor elements to one scalar. + + +Sum every element of a cffi Tensor. #### Parameters @@ -838,91 +1473,182 @@ None. #### Returns -scalar sum, or `0.0` for an empty tensor. +`float` +: Scalar sum, or `0.0` for an empty tensor. #### Raises -native arithmetic/runtime or closed-handle errors. For example, -`hc.Tensor([1, 2])` sums to `3.0`; close the owner after use. +`RuntimeError` +: A required Tensor handle is closed. -#### See also +`hmath_cffi.HMathError` +: Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). -[hmath.Tensor.sum](#hmathtensorsum), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). +#### See Also + +[hmath.Tensor.sum](#hmath.Tensor.sum), [hmath_cffi.Tensor.dot](#hmath_cffi.Tensor.dot). + +#### Notes + +The whole-tensor reduction, numerical behavior, and native scratch costs +are the same as primary `Tensor.sum()`. No axis argument is exposed. + +#### Examples + +```python +import hmath_cffi as hc + +with hc.Tensor([1, 2, 3]) as tensor, hc.Tensor([]) as empty: + assert tensor.sum() == 6.0 + assert empty.sum() == 0.0 +``` ### `hmath_cffi.Tensor.dot(other)` -Reduce corresponding products of two identically shaped tensors. + + +Sum corresponding products of identically shaped cffi tensors. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `Tensor` | Live tensor with exactly the same shape as this tensor. | +| `other` | `hmath_cffi.Tensor` | Live cffi Tensor with exactly the same shape. | #### Returns -scalar sum -of elementwise products. +`float` +: Sum of corresponding products; `0.0` for matching empty tensors. #### Raises -native shape/arithmetic/runtime or -closed-handle errors. The primary interface's dot example also applies to cffi. +`RuntimeError` +: A required Tensor handle is closed. -#### See also +`hmath_cffi.HMathError` +: Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). -[hmath.Tensor.dot](#hmathtensordotother), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). +`hmath_cffi.HMathError` with status 2 +: Shapes differ. + +#### See Also + +[hmath.Tensor.dot](#hmath.Tensor.dot), [hmath_cffi.Tensor.__matmul__](#hmath_cffi.Tensor.__matmul__). + +#### Notes + +This is the same whole-tensor scalar reduction as primary `dot`, not +matrix multiplication. See that entry for the equation and numerical behavior. + +#### Examples + +```python +import hmath_cffi as hc + +with hc.Tensor([1, 2, 3]) as a, hc.Tensor([4, 5, 6]) as b: + assert a.dot(b) == 32.0 +``` ### `hmath_cffi.Tensor.__add__(other)` — `a + b` -Add two tensors element by element. + + +Add two cffi tensors element by element. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `Tensor` | Live tensor of the same shape; broadcasting is unsupported. | +| `other` | `hmath_cffi.Tensor` | Live cffi Tensor with exactly the same shape; no broadcasting. | #### Returns -a new owned -elementwise sum without broadcasting. +`hmath_cffi.Tensor` +: New owned result with independent storage and unchanged input tensors. #### Raises -native shape/allocation/ -arithmetic/runtime or closed-handle errors. Use with `with a + b as result:`. +`RuntimeError` +: A required Tensor handle is closed. + +`hmath_cffi.HMathError` +: Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). -#### See also +`hmath_cffi.HMathError` with status 2 +: Shapes differ. -[hmath.Tensor.__add__](#hmathtensor__add__other-a-b), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). +#### See Also + +[hmath.Tensor.__add__](#hmath.Tensor.__add__), [hmath_cffi.add](#hmath_cffi.add). + +#### Examples + +```python +import hmath_cffi as hc + +with hc.Tensor([1, 2]) as a, hc.Tensor([3, 4]) as b: + with a + b as result: + assert result.values() == [4.0, 6.0] +``` ### `hmath_cffi.Tensor.__matmul__(other)` — `a @ b` -Multiply two rank-two matrices. + + +Multiply two compatible rank-two cffi matrices. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `Tensor` | Live (k, n) matrix when this tensor has shape (m, k). | +| `other` | `hmath_cffi.Tensor` | Live shape `(k, n)` matrix when this matrix has shape `(m, k)`. | #### Returns -a new owned matrix. +`hmath_cffi.Tensor` +: New owned matrix with shape `(m, n)` and independent storage. #### Raises -the same shape/work-bound and -native/lifecycle errors as primary `@`. See the executable cffi example above. +`RuntimeError` +: A required Tensor handle is closed. + +`hmath_cffi.HMathError` +: Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). + +`hmath_cffi.HMathError` +: Rank/inner-dimension mismatch (2), or output/work budget exceeded (3). + +#### See Also + +[hmath.Tensor.__matmul__](#hmath.Tensor.__matmul__), [hmath_cffi.Tensor.dot](#hmath_cffi.Tensor.dot). + +#### Notes + +The primary matrix-product definition and budgets apply: at most +1,048,576 output elements and 16,777,216 scalar multiply-add steps. +There is no batching or vector promotion. -#### See also +#### Examples -[hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). +```python +import hmath_cffi as hc + +with hc.Tensor([1, 2, 3, 4], (2, 2)) as a: + with a @ a as result: + assert result.shape == (2, 2) + assert result.values() == [7.0, 10.0, 15.0, 22.0] +``` ### `hmath_cffi.Tensor.close()` -Release this tensor handle deterministically. + + +Release this cffi Tensor handle. #### Parameters @@ -930,17 +1656,33 @@ None. #### Returns -`None`, releasing its cffi-managed reference. -Repeated calls are harmless. Do not close concurrently with an operation on -this tensor. Other independent owners remain valid. +`None`. Repeated calls are harmless. + +#### See Also + +[hmath_cffi.Tensor.__enter__](#hmath_cffi.Tensor.__enter__), [hmath_cffi.Tensor.shape](#hmath_cffi.Tensor.shape). -#### See also +#### Notes + +Do not call concurrently with an operation using this handle. Numerical +operations and `values()` fail after close; cached `.shape` remains readable. +The shared runtime is not shut down. + +#### Examples + +```python +import hmath_cffi as hc -[hmath.Tensor.close](#hmathtensorclose), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.check](#hmath_cfficheckstatus). +tensor = hc.Tensor([1]) +tensor.close() +tensor.close() +``` ### `hmath_cffi.Tensor.__enter__()` -Enter a context that owns this tensor handle. + + +Enter a context for a live cffi Tensor. #### Parameters @@ -948,43 +1690,40 @@ None. #### Returns -the same live tensor in `with`. +`hmath_cffi.Tensor` +: The same object, without an extra native reference. #### Raises -`RuntimeError` after close. No new native owner is created. +`RuntimeError` +: The handle is closed. + +#### See Also -#### See also +[hmath_cffi.Tensor.__exit__](#hmath_cffi.Tensor.__exit__), [hmath_cffi.Tensor.close](#hmath_cffi.Tensor.close). -[hmath.Tensor.__enter__](#hmathtensor__enter__), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). +#### Notes + +As with the primary interface, nested contexts on the same object do not +create independent handle lifetimes. ### `hmath_cffi.Tensor.__exit__(*args)` -Close the tensor when leaving a context. + + +Release a cffi Tensor on normal or exceptional context exit. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `args` | `exception triple` | Python supplies (exception type, exception value, traceback); exceptions are not suppressed. | +| `args` | `exception triple` | Exception type, value, and traceback supplied by Python; all are `None` on normal exit. | #### Returns -`None`, closing this -tensor on block exit without suppressing exceptions. - -#### See also - -[hmath.Tensor.__exit__](#hmathtensor__exit__args), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). - -## Low-level cffi attributes +`None`. Exceptions are not suppressed. -`hmath_cffi.ffi` is the configured `cffi.FFI` object; `hmath_cffi.lib` is the -loaded native handle exposing the declarations used by this wrapper. Raw calls -follow the [C ABI](c.md), including status and ownership rules. `tensor.ptr` is -borrowed: do not release it separately or use it after close. `tensor.shape` -is the construction/result shape tuple and must not be reassigned by callers. +#### See Also -## Native status codes +[hmath_cffi.Tensor.__enter__](#hmath_cffi.Tensor.__enter__), [hmath_cffi.Tensor.close](#hmath_cffi.Tensor.close). -See the canonical [C status table](c.md#native-status-codes). diff --git a/mkdocs.yml b/mkdocs.yml index 5ffadb1..0a76ac6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,6 +34,7 @@ plugins: - search markdown_extensions: - admonition + - def_list - pymdownx.arithmatex: generic: true - pymdownx.highlight: diff --git a/tools/check_api_docs.py b/tools/check_api_docs.py index cd12a6f..d59c938 100644 --- a/tools/check_api_docs.py +++ b/tools/check_api_docs.py @@ -10,6 +10,17 @@ ROOT = Path(__file__).resolve().parent.parent PROTOCOLS = {"__add__", "__matmul__", "__enter__", "__exit__", "__del__"} +# Intentional public data attributes, not raw FFI pointers or imported names. +PYTHON_ATTRIBUTES = { + "hmath.HMathError.status", + "hmath_cffi.HMathError.status", + "hmath_cffi.Tensor.shape", +} +PYTHON_SECTIONS = ( + "Parameters", "Type", "Returns", "Raises", "Warns", "Warnings", + "Other Parameters", "Attributes", "Methods", "See Also", "Notes", + "References", "Examples", +) def python_exports(path, module): @@ -31,22 +42,102 @@ def python_exports(path, module): return names -def check_entries(names, page, pattern, parameters, signatures, defaults=None): +def python_kinds(): + """Classify public objects without importing the native runtime.""" + kinds = {} + for module in ("hmath", "hmath_cffi"): + path = ROOT / "bindings/python" / f"{module}.py" + public = python_exports(path, module) + tree = ast.parse(path.read_text()) + for node in tree.body: + name = f"{module}.{getattr(node, 'name', '')}" + if name not in public: + continue + kinds[name] = "class" if isinstance(node, ast.ClassDef) else "callable" + if not isinstance(node, ast.ClassDef): + continue + for member in node.body: + member_name = f"{name}.{getattr(member, 'name', '')}" + if member_name in public: + property_ = any( + isinstance(item, ast.Name) and item.id == "property" + for item in member.decorator_list + ) + kinds[member_name] = "attribute" if property_ else "callable" + assigned = { + item.attr + for item in ast.walk(node) + if isinstance(item, ast.Attribute) + and isinstance(item.value, ast.Name) + and item.value.id == "self" + and isinstance(item.ctx, ast.Store) + } + for attribute in PYTHON_ATTRIBUTES: + owner, field = attribute.rsplit(".", 1) + if owner == name: + if field not in assigned: + raise ValueError( + f"{path}: documented attribute {attribute} is absent" + ) + kinds[attribute] = "attribute" + if not PYTHON_ATTRIBUTES <= kinds.keys(): + raise ValueError("A documented Python attribute owner is absent") + return kinds + + +def check_entries( + names, page, pattern, parameters, signatures, defaults=None, kinds=None +): text = page.read_text() sections = re.split(r"(?=^### )", text, flags=re.MULTILINE) covered = {} for section in sections: match = re.match(pattern, section) if match: + if kinds and match[1] in covered: + raise ValueError(f"{page}: duplicate entry: {match[1]}") covered[match[1]] = section missing = names - covered.keys() if missing: raise ValueError(f"{page}: missing entries: {', '.join(sorted(missing))}") + if kinds and covered.keys() - names: + unknown = sorted(covered.keys() - names) + raise ValueError(f"{page}: unknown Python entries: {unknown}") for name in sorted(names): section = re.split(r"^## ", covered[name], maxsplit=1, flags=re.MULTILINE)[0] - for title in ("Parameters", "Returns", "See also"): + kind = kinds.get(name, "callable") if kinds else "legacy" + required = { + "legacy": ("Parameters", "Returns", "See also"), + "callable": ("Parameters", "Returns", "See Also"), + "class": ("Parameters", "Attributes", "See Also"), + "attribute": ("Type", "See Also"), + }[kind] + for title in required: if not re.search(rf"^#### {title}\n\n\S", section, re.MULTILINE): raise ValueError(f"{page}: {name} lacks a nonempty {title} section") + if kinds: + titles = re.findall(r"^#### (.+)$", section, re.MULTILINE) + if len(titles) != len(set(titles)): + raise ValueError(f"{page}: {name} has duplicate sections") + unknown = set(titles) - set(PYTHON_SECTIONS) + if unknown: + raise ValueError( + f"{page}: {name} has unknown sections: {sorted(unknown)}" + ) + order = [PYTHON_SECTIONS.index(title) for title in titles] + if order != sorted(order): + raise ValueError(f"{page}: {name} sections are out of order") + if kind in ("class", "attribute") and "Returns" in titles: + raise ValueError(f"{page}: {name} is not a callable result entry") + if kind == "attribute" and "Parameters" in titles: + raise ValueError(f"{page}: {name} is an attribute, not a call") + if f'' not in section: + raise ValueError(f"{page}: {name} lacks its stable symbol anchor") + heading = re.match(r"### `([^`]+)`", section)[1] + if re.sub(r"\s+", "", signatures[name]) != re.sub(r"\s+", "", heading): + raise ValueError(f"{page}: {name} signature differs from source") + if kind == "attribute": + continue parameter_section = section.split("#### Parameters\n", 1)[1].split("#### ", 1)[ 0 ] @@ -55,7 +146,8 @@ def check_entries(names, page, pattern, parameters, signatures, defaults=None): ) if [row[0] for row in rows] != parameters[name]: raise ValueError( - f"{page}: {name} parameter names/order differ from source: {parameters[name]}" + f"{page}: {name} parameter names/order differ from source: " + f"{parameters[name]}" ) if not parameters[name] and parameter_section.strip() != "None.": raise ValueError(f"{page}: {name} must explicitly state no parameters") @@ -67,9 +159,10 @@ def check_entries(names, page, pattern, parameters, signatures, defaults=None): raise ValueError( f"{page}: {name}.{parameter} must document default {value}" ) - compact = re.sub(r"\s+", "", section) - if re.sub(r"\s+", "", signatures[name]) not in compact: - raise ValueError(f"{page}: {name} signature differs from source") + if not kinds: + compact = re.sub(r"\s+", "", section) + if re.sub(r"\s+", "", signatures[name]) not in compact: + raise ValueError(f"{page}: {name} signature differs from source") return len(names) @@ -105,8 +198,7 @@ def record(node, name): positional[-len(args.defaults) :], args.defaults ) } - if args.defaults - else {} + if args.defaults else {} ) defaults[name].update( { @@ -129,15 +221,15 @@ def record(node, name): if member.name == "__init__" else name + "." + member.name, ) + for name in PYTHON_ATTRIBUTES: + parameters[name], signatures[name], defaults[name] = [], name, {} return parameters, signatures, defaults def read_haddock(interface): result = subprocess.run( ["haddock", "--show-interface", str(interface)], - check=True, - capture_output=True, - text=True, + check=True, capture_output=True, text=True, ) # Haddock currently writes this JSON to stderr, despite a successful exit. return json.loads(result.stdout.strip() or result.stderr.strip()) @@ -166,8 +258,7 @@ def check_haskell(data): continue declarations = re.findall( r"^(?:data|newtype|type|class)\s+(?:\([^\n]*?\)\s*=>\s*)?([A-Z]\w*)", - source.read_text(), - re.MULTILINE, + source.read_text(), re.MULTILINE, ) for declaration in declarations: symbol = module["module"] + "$" + declaration @@ -187,45 +278,39 @@ def check_haskell(data): return len(public), len(public_types) -def check_reference(interface=None): - python = set() - for module in ("hmath", "hmath_cffi"): - python |= python_exports(ROOT / "bindings/python" / f"{module}.py", module) +def check_reference(interface=None, *, python_only=False): + kinds = python_kinds() counts = { "Python": check_entries( - python, - ROOT / "docs/reference/python.md", + set(kinds), ROOT / "docs/reference/python.md", r"### `(hmath(?:_cffi)?\.[\w.]+)", - *python_signatures(), + *python_signatures(), kinds=kinds, ) } + if python_only: + print(f"Python API reference coverage: {counts['Python']} entries") + return counts c = set() c_parameters, c_signatures = {}, {} for header in ("hmath.h", "hmath_dlpack.h"): c.update( re.findall( r"^HMATH_API\s+[\w\s*]+?\b(hmath_\w+)\s*\([^;]*\);", - (ROOT / "include" / header).read_text(), - re.MULTILINE, + (ROOT / "include" / header).read_text(), re.MULTILINE, ) ) for match in re.finditer( r"^HMATH_API\s+([\w\s*]+?)\b(hmath_\w+)\s*\(([^;]*)\);", - (ROOT / "include" / header).read_text(), - re.MULTILINE, + (ROOT / "include" / header).read_text(), re.MULTILINE, ): c_signatures[match[2]] = f"{match[1]}{match[2]}({match[3]})" c_parameters[match[2]] = [ re.search(r"(\w+)\s*$", arg)[1] - for arg in match[3].split(",") - if arg.strip() != "void" + for arg in match[3].split(",") if arg.strip() != "void" ] counts["C"] = check_entries( - c, - ROOT / "docs/reference/c.md", - r"### `(hmath_\w+)`", - c_parameters, - c_signatures, + c, ROOT / "docs/reference/c.md", r"### `(hmath_\w+)`", + c_parameters, c_signatures, ) header = (ROOT / "include/hmath.h").read_text() table = (ROOT / "docs/reference/c.md").read_text() @@ -237,14 +322,11 @@ def check_reference(interface=None): wave = set( re.findall( r"^pub fun (\w+)\(", - (ROOT / "bindings/wave/hmath.wave").read_text(), - re.MULTILINE, + (ROOT / "bindings/wave/hmath.wave").read_text(), re.MULTILINE, ) ) counts["Wave"] = check_entries( - wave, - ROOT / "docs/reference/wave.md", - r"### `(\w+)\(", + wave, ROOT / "docs/reference/wave.md", r"### `(\w+)\(", { match[1]: re.findall(r"(\w+)\s*:", match[2]) for match in re.finditer( @@ -257,8 +339,7 @@ def check_reference(interface=None): match[1]: match[0].removeprefix("pub fun ").strip() for match in re.finditer( r"^pub fun (\w+)\([^)]*\)(?:\s*->\s*[^\s{]+)?", - (ROOT / "bindings/wave/hmath.wave").read_text(), - re.MULTILINE, + (ROOT / "bindings/wave/hmath.wave").read_text(), re.MULTILINE, ) }, ) @@ -276,5 +357,8 @@ def check_reference(interface=None): if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--haddock-interface", type=Path) + parser.add_argument("--python-only", action="store_true") args = parser.parse_args() - check_reference(args.haddock_interface) + if args.python_only and args.haddock_interface: + parser.error("--python-only cannot be combined with --haddock-interface") + check_reference(args.haddock_interface, python_only=args.python_only) diff --git a/tools/check_docs_examples.py b/tools/check_docs_examples.py index 7870d57..0b2ef64 100644 --- a/tools/check_docs_examples.py +++ b/tools/check_docs_examples.py @@ -1,17 +1,115 @@ # SPDX-License-Identifier: MPL-2.0 -"""Execute Python examples from the documentation against an installed wheel.""" +"""Check documentation Python examples against an installed distribution. +Pages marked ```` run each Python block in +its own interpreter. Other pages retain a shared namespace within that page, +so sequential tutorials continue to work. Every page has its own process. +""" + +import argparse +import importlib.metadata +import json import re +import subprocess +import sys from pathlib import Path -root = Path(__file__).resolve().parent.parent -for path in sorted((root / "docs").rglob("*.md")): - namespace = {"__name__": "__main__"} - examples = re.findall( - r"^```python\n(.*?)^```", path.read_text(), re.MULTILINE | re.DOTALL +ROOT = Path(__file__).resolve().parent.parent +COMPLETE = "__HMATH_DOC_EXAMPLES_COMPLETED__" +INDEPENDENT = "" +FENCE = re.compile(r"^```python[ \t]*\n(.*?)^```[ \t]*$", re.MULTILINE | re.DOTALL) +WORKER = """ +import json +import sys + +payload = json.load(sys.stdin) +namespace = {"__name__": "__main__"} +for line, source in payload["examples"]: + code = "\\n" * (line - 1) + source + exec(compile(code, payload["path"], "exec"), namespace) +sys.stdout.write("\\n__HMATH_DOC_EXAMPLES_COMPLETED__\\n") +""" + + +def read_examples(path): + """Return (first source line, source) pairs and the page isolation mode.""" + text = path.read_text(encoding="utf-8") + examples = [ + (text.count("\n", 0, match.start(1)) + 1, match[1]) + for match in FENCE.finditer(text) + ] + openings = len(re.findall(r"^```python[ \t]*$", text, re.MULTILINE)) + if openings != len(examples): + raise ValueError(f"{path}: unclosed Python code fence") + independent = bool( + re.search(r"^" + re.escape(INDEPENDENT) + r"[ \t]*$", text, re.MULTILINE) ) - for example in examples: - # These are executable examples from the checked-out project itself. - exec(compile(example, str(path), "exec"), namespace) # noqa: S102 - if examples: - print(f"{path.relative_to(root)}: {len(examples)} Python examples pass") + return examples, independent + + +def check_page(path, *, syntax_only=False, timeout=60): + """Check a page, preserving source line numbers in failure diagnostics.""" + examples, independent = read_examples(path) + for line, source in examples: + compile("\n" * (line - 1) + source, str(path), "exec") + if syntax_only or not examples: + return len(examples) + groups = [[example] for example in examples] if independent else [examples] + for group in groups: + payload = {"path": str(path), "examples": group} + label = f"{path}:{group[0][0]}" + try: + result = subprocess.run( + [sys.executable, "-I", "-c", WORKER], + input=json.dumps(payload), + text=True, + capture_output=True, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired as error: + raise RuntimeError(f"{label}: example exceeded {timeout}s") from error + if result.returncode or not result.stdout.rstrip().endswith(COMPLETE): + raise RuntimeError( + f"{label}: example failed (exit {result.returncode})\n" + f"{result.stdout}{result.stderr}" + ) + return len(examples) + + +def check_installed_version(expected): + """Reject version drift before running any documentation examples.""" + actual = importlib.metadata.version("hmath-python") + if actual != expected: + raise ValueError(f"Expected hmath-python {expected}, found {actual}") + print(f"Documentation runtime: hmath-python {actual}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("paths", nargs="*", type=Path) + parser.add_argument("--check-syntax", action="store_true") + parser.add_argument("--expected-version") + parser.add_argument("--timeout", type=float, default=60) + args = parser.parse_args() + if args.timeout <= 0: + parser.error("--timeout must be positive") + if args.expected_version and args.check_syntax: + parser.error("--expected-version requires execution, not --check-syntax") + if args.expected_version: + check_installed_version(args.expected_version) + paths = args.paths or sorted((ROOT / "docs").rglob("*.md")) + total = 0 + for path in paths: + count = check_page(path, syntax_only=args.check_syntax, timeout=args.timeout) + total += count + if count: + mode = "syntax checked" if args.check_syntax else "executed" + print(f"{path}: {count} Python examples {mode}") + if not total: + raise SystemExit("No Python examples found in the selected paths") + print(f"Python documentation examples: {total}") + + +if __name__ == "__main__": + main() diff --git a/tools/test_docs_checks.py b/tools/test_docs_checks.py new file mode 100644 index 0000000..e100fb4 --- /dev/null +++ b/tools/test_docs_checks.py @@ -0,0 +1,241 @@ +# SPDX-License-Identifier: MPL-2.0 +"""Regression tests for documentation checks; no native runtime is required.""" + +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +import check_api_docs as api +import check_docs_examples as examples + + +class ReferenceChecks(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.page = Path(self.directory.name) / "reference.md" + self.name = "hmath.example" + self.signature = "hmath.example(value, copy=True)" + self.parameters = {self.name: ["value", "copy"]} + self.signatures = {self.name: self.signature} + self.defaults = {self.name: {"copy": "True"}} + self.document = '''### `hmath.example(value, copy=True)` + + + +Return an example value. + +#### Parameters + +| Name | Type / default | Description | +| --- | --- | --- | +| `value` | `float` | Input. | +| `copy` | `bool; default True` | Copy policy. | + +#### Returns + +`float`. + +#### See Also + +Related operations. + +#### Notes + +Example contract. + +#### Examples + +```python +assert 1 + 1 == 2 +``` +''' + + def check(self, document=None, kind="callable"): + self.page.write_text(self.document if document is None else document) + return api.check_entries( + {self.name}, self.page, r"### `(hmath\.[\w.]+)", + self.parameters, self.signatures, self.defaults, + kinds={self.name: kind}, + ) + + def test_callable(self): + self.assertEqual(self.check(), 1) + + def test_missing_entry(self): + with self.assertRaisesRegex(ValueError, "missing entries"): + self.check("") + + def test_duplicate_entry(self): + with self.assertRaisesRegex(ValueError, "duplicate entry"): + self.check(self.document * 2) + + def test_unknown_entry(self): + with self.assertRaisesRegex(ValueError, "unknown Python entries"): + self.check(self.document + "\n### `hmath.unknown()`\n") + + def test_wrong_parameter_order(self): + self.parameters[self.name].reverse() + with self.assertRaisesRegex(ValueError, "parameter names/order"): + self.check() + + def test_wrong_default(self): + with self.assertRaisesRegex(ValueError, "default True"): + self.check(self.document.replace("default True", "default False")) + + def test_wrong_heading_not_rescued_by_body(self): + document = self.document.replace(self.signature, "hmath.example(value)") + document += "\n" + self.signature + "\n" + with self.assertRaisesRegex(ValueError, "signature differs"): + self.check(document) + + def test_missing_anchor(self): + with self.assertRaisesRegex(ValueError, "stable symbol anchor"): + self.check(self.document.replace('', "")) + + def test_out_of_order_sections(self): + document = self.document.replace("#### Notes", "#### References") + document = document.replace("#### Examples", "#### Notes") + with self.assertRaisesRegex(ValueError, "out of order"): + self.check(document) + + def test_duplicate_section(self): + with self.assertRaisesRegex(ValueError, "duplicate sections"): + self.check(self.document + "\n#### Notes\n\nRepeated.\n") + + def test_class_without_returns(self): + document = self.document.replace("#### Returns", "#### Attributes") + self.assertEqual(self.check(document, kind="class"), 1) + + def test_class_rejects_returns(self): + document = self.document.replace( + "#### See Also", "#### Attributes\n\n`value` : float\n\n#### See Also" + ) + with self.assertRaisesRegex(ValueError, "not a callable result"): + self.check(document, kind="class") + + def test_attribute_without_call_parentheses(self): + self.parameters[self.name] = [] + self.signatures[self.name] = self.name + self.defaults[self.name] = {} + document = '''### `hmath.example` + + + +Read a value. + +#### Type + +`float`. + +#### See Also + +Other attributes. +''' + self.assertEqual(self.check(document, kind="attribute"), 1) + with self.assertRaisesRegex(ValueError, "signature differs"): + self.check( + document.replace("`hmath.example`", "`hmath.example()`"), kind="attribute" + ) + + def test_legacy_c_wave_format_is_preserved(self): + self.page.write_text(self.document.replace("#### See Also", "#### See also")) + self.assertEqual( + api.check_entries( + {self.name}, self.page, r"### `(hmath\.[\w.]+)", + self.parameters, self.signatures, self.defaults, + ), 1, + ) + + def test_source_inventory_includes_attributes(self): + kinds = api.python_kinds() + for name in api.PYTHON_ATTRIBUTES: + self.assertEqual(kinds[name], "attribute") + self.assertEqual(kinds["hmath.Tensor"], "class") + self.assertEqual(kinds["hmath.Tensor.shape"], "attribute") + self.assertEqual(kinds["hmath.Tensor.dot"], "callable") + self.assertNotIn("hmath.threading", kinds) + self.assertNotIn("hmath.Tensor._live", kinds) + + def test_actual_python_reference(self): + self.assertGreater(api.check_reference(python_only=True)["Python"], 0) + + +class ExampleChecks(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.page = Path(self.directory.name) / "examples.md" + + def check(self, text, **kwargs): + self.page.write_text(text) + return examples.check_page(self.page, **kwargs) + + def test_tutorial_keeps_page_namespace(self): + text = "```python\nx = 3\n```\n\n```python\nassert x == 3\n```\n" + self.assertEqual(self.check(text), 2) + + def test_inline_marker_does_not_change_tutorial_mode(self): + text = "Describe `" + examples.INDEPENDENT + "` here.\n" + text += "```python\nx = 3\n```\n```python\nassert x == 3\n```\n" + self.assertEqual(self.check(text), 2) + + def test_reference_blocks_have_no_hidden_imports(self): + text = examples.INDEPENDENT + "\n" + text += "```python\nx = 3\n```\n```python\nassert x == 3\n```\n" + with self.assertRaisesRegex(RuntimeError, "NameError"): + self.check(text) + + def test_reference_blocks_have_separate_processes(self): + text = examples.INDEPENDENT + ''' +```python +import builtins +builtins.hmath_example_sentinel = True +``` +```python +import builtins +assert not hasattr(builtins, "hmath_example_sentinel") +``` +''' + self.assertEqual(self.check(text), 2) + + def test_syntax_only_does_not_import_native_library(self): + text = "```python\nimport nonexistent_documentation_test_module\n```\n" + self.assertEqual(self.check(text, syntax_only=True), 1) + with self.assertRaisesRegex(RuntimeError, "ModuleNotFoundError"): + self.check(text) + + def test_failure_includes_markdown_location(self): + with self.assertRaisesRegex(RuntimeError, r"examples\.md:4"): + self.check("# Title\n\n```python\nassert False\n```\n") + + def test_syntax_error_uses_markdown_line(self): + with self.assertRaises(SyntaxError) as caught: + self.check("# Title\n\n```python\nif:\n```\n", syntax_only=True) + self.assertEqual(caught.exception.lineno, 4) + + def test_timeout(self): + with self.assertRaisesRegex(RuntimeError, "exceeded"): + self.check("```python\nimport time\ntime.sleep(5)\n```\n", timeout=0.05) + + def test_early_success_exit_is_not_a_pass(self): + with self.assertRaisesRegex(RuntimeError, "example failed"): + self.check("```python\nraise SystemExit(0)\n```\n") + + def test_unclosed_fence(self): + with self.assertRaisesRegex(ValueError, "unclosed Python"): + self.check("```python\nassert True\n", syntax_only=True) + + def test_installed_version_mismatch(self): + with patch.object(examples.importlib.metadata, "version", return_value="0.0.0"): + with self.assertRaisesRegex(ValueError, "Expected hmath-python 1.0.2"): + examples.check_installed_version("1.0.2") + + def test_installed_version_match(self): + with patch.object(examples.importlib.metadata, "version", return_value="1.0.2"): + examples.check_installed_version("1.0.2") + + +if __name__ == "__main__": + unittest.main() From 22ecfd1123eb626e4f265c3f62d3c6fa958aa898 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Thu, 29 Oct 2026 14:46:54 +0900 Subject: [PATCH 2/4] chore: keep documentation-only changes in PR #71 Restore documentation CI and Python validation tooling to upstream versions. Retain all changes under docs/ without further modification. --- .github/workflows/docs.yml | 34 +---- mkdocs.yml | 1 - tools/check_api_docs.py | 162 ++++++----------------- tools/check_docs_examples.py | 120 ++--------------- tools/test_docs_checks.py | 241 ----------------------------------- 5 files changed, 54 insertions(+), 504 deletions(-) delete mode 100644 tools/test_docs_checks.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4964f82..9ad78a0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,12 +6,6 @@ on: branches: [master] pull_request: workflow_dispatch: - inputs: - python-runtime: - description: Python package to execute the documentation against - type: choice - options: [checkout, released] - default: checkout permissions: contents: read @@ -30,11 +24,6 @@ jobs: python-version: '3.12' cache: pip cache-dependency-path: requirements-docs.txt - - name: Test documentation validators - run: | - python -m unittest discover -s tools -p 'test_docs_checks.py' - python tools/check_api_docs.py --python-only - python tools/check_docs_examples.py --check-syntax - uses: ./.github/actions/setup-haskell with: ghc-version: '9.10.3' @@ -47,29 +36,14 @@ jobs: run: | python -m pip install -r requirements-docs.txt python tools/build_docs.py - - name: Native dependencies for the checkout wheel - if: inputs.python-runtime != 'released' - run: | - sudo apt-get update - sudo apt-get install -y liblapacke-dev libopenblas-dev zlib1g-dev - - name: Verify Python documentation examples against the selected package - env: - DOCS_PYTHON_RUNTIME: ${{ inputs.python-runtime || 'checkout' }} + - name: Verify Python documentation examples against the released package run: | python - <<'PY' - import os - import pathlib - import subprocess - import sys - import tomllib - + import pathlib, subprocess, sys, tomllib version = tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'] - target = os.environ['DOCS_PYTHON_RUNTIME'] - package = '.[numpy,cffi]' if target == 'checkout' else f'hmath-python[numpy,cffi]=={version}' - print(f'Documentation package source: {target}', flush=True) - subprocess.run([sys.executable, '-m', 'pip', 'install', '--force-reinstall', package], check=True) - subprocess.run([sys.executable, 'tools/check_docs_examples.py', '--expected-version', version], check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', f'hmath-python[numpy,cffi]=={version}'], check=True) PY + python tools/check_docs_examples.py - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: site diff --git a/mkdocs.yml b/mkdocs.yml index 0a76ac6..5ffadb1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,7 +34,6 @@ plugins: - search markdown_extensions: - admonition - - def_list - pymdownx.arithmatex: generic: true - pymdownx.highlight: diff --git a/tools/check_api_docs.py b/tools/check_api_docs.py index d59c938..cd12a6f 100644 --- a/tools/check_api_docs.py +++ b/tools/check_api_docs.py @@ -10,17 +10,6 @@ ROOT = Path(__file__).resolve().parent.parent PROTOCOLS = {"__add__", "__matmul__", "__enter__", "__exit__", "__del__"} -# Intentional public data attributes, not raw FFI pointers or imported names. -PYTHON_ATTRIBUTES = { - "hmath.HMathError.status", - "hmath_cffi.HMathError.status", - "hmath_cffi.Tensor.shape", -} -PYTHON_SECTIONS = ( - "Parameters", "Type", "Returns", "Raises", "Warns", "Warnings", - "Other Parameters", "Attributes", "Methods", "See Also", "Notes", - "References", "Examples", -) def python_exports(path, module): @@ -42,102 +31,22 @@ def python_exports(path, module): return names -def python_kinds(): - """Classify public objects without importing the native runtime.""" - kinds = {} - for module in ("hmath", "hmath_cffi"): - path = ROOT / "bindings/python" / f"{module}.py" - public = python_exports(path, module) - tree = ast.parse(path.read_text()) - for node in tree.body: - name = f"{module}.{getattr(node, 'name', '')}" - if name not in public: - continue - kinds[name] = "class" if isinstance(node, ast.ClassDef) else "callable" - if not isinstance(node, ast.ClassDef): - continue - for member in node.body: - member_name = f"{name}.{getattr(member, 'name', '')}" - if member_name in public: - property_ = any( - isinstance(item, ast.Name) and item.id == "property" - for item in member.decorator_list - ) - kinds[member_name] = "attribute" if property_ else "callable" - assigned = { - item.attr - for item in ast.walk(node) - if isinstance(item, ast.Attribute) - and isinstance(item.value, ast.Name) - and item.value.id == "self" - and isinstance(item.ctx, ast.Store) - } - for attribute in PYTHON_ATTRIBUTES: - owner, field = attribute.rsplit(".", 1) - if owner == name: - if field not in assigned: - raise ValueError( - f"{path}: documented attribute {attribute} is absent" - ) - kinds[attribute] = "attribute" - if not PYTHON_ATTRIBUTES <= kinds.keys(): - raise ValueError("A documented Python attribute owner is absent") - return kinds - - -def check_entries( - names, page, pattern, parameters, signatures, defaults=None, kinds=None -): +def check_entries(names, page, pattern, parameters, signatures, defaults=None): text = page.read_text() sections = re.split(r"(?=^### )", text, flags=re.MULTILINE) covered = {} for section in sections: match = re.match(pattern, section) if match: - if kinds and match[1] in covered: - raise ValueError(f"{page}: duplicate entry: {match[1]}") covered[match[1]] = section missing = names - covered.keys() if missing: raise ValueError(f"{page}: missing entries: {', '.join(sorted(missing))}") - if kinds and covered.keys() - names: - unknown = sorted(covered.keys() - names) - raise ValueError(f"{page}: unknown Python entries: {unknown}") for name in sorted(names): section = re.split(r"^## ", covered[name], maxsplit=1, flags=re.MULTILINE)[0] - kind = kinds.get(name, "callable") if kinds else "legacy" - required = { - "legacy": ("Parameters", "Returns", "See also"), - "callable": ("Parameters", "Returns", "See Also"), - "class": ("Parameters", "Attributes", "See Also"), - "attribute": ("Type", "See Also"), - }[kind] - for title in required: + for title in ("Parameters", "Returns", "See also"): if not re.search(rf"^#### {title}\n\n\S", section, re.MULTILINE): raise ValueError(f"{page}: {name} lacks a nonempty {title} section") - if kinds: - titles = re.findall(r"^#### (.+)$", section, re.MULTILINE) - if len(titles) != len(set(titles)): - raise ValueError(f"{page}: {name} has duplicate sections") - unknown = set(titles) - set(PYTHON_SECTIONS) - if unknown: - raise ValueError( - f"{page}: {name} has unknown sections: {sorted(unknown)}" - ) - order = [PYTHON_SECTIONS.index(title) for title in titles] - if order != sorted(order): - raise ValueError(f"{page}: {name} sections are out of order") - if kind in ("class", "attribute") and "Returns" in titles: - raise ValueError(f"{page}: {name} is not a callable result entry") - if kind == "attribute" and "Parameters" in titles: - raise ValueError(f"{page}: {name} is an attribute, not a call") - if f'' not in section: - raise ValueError(f"{page}: {name} lacks its stable symbol anchor") - heading = re.match(r"### `([^`]+)`", section)[1] - if re.sub(r"\s+", "", signatures[name]) != re.sub(r"\s+", "", heading): - raise ValueError(f"{page}: {name} signature differs from source") - if kind == "attribute": - continue parameter_section = section.split("#### Parameters\n", 1)[1].split("#### ", 1)[ 0 ] @@ -146,8 +55,7 @@ def check_entries( ) if [row[0] for row in rows] != parameters[name]: raise ValueError( - f"{page}: {name} parameter names/order differ from source: " - f"{parameters[name]}" + f"{page}: {name} parameter names/order differ from source: {parameters[name]}" ) if not parameters[name] and parameter_section.strip() != "None.": raise ValueError(f"{page}: {name} must explicitly state no parameters") @@ -159,10 +67,9 @@ def check_entries( raise ValueError( f"{page}: {name}.{parameter} must document default {value}" ) - if not kinds: - compact = re.sub(r"\s+", "", section) - if re.sub(r"\s+", "", signatures[name]) not in compact: - raise ValueError(f"{page}: {name} signature differs from source") + compact = re.sub(r"\s+", "", section) + if re.sub(r"\s+", "", signatures[name]) not in compact: + raise ValueError(f"{page}: {name} signature differs from source") return len(names) @@ -198,7 +105,8 @@ def record(node, name): positional[-len(args.defaults) :], args.defaults ) } - if args.defaults else {} + if args.defaults + else {} ) defaults[name].update( { @@ -221,15 +129,15 @@ def record(node, name): if member.name == "__init__" else name + "." + member.name, ) - for name in PYTHON_ATTRIBUTES: - parameters[name], signatures[name], defaults[name] = [], name, {} return parameters, signatures, defaults def read_haddock(interface): result = subprocess.run( ["haddock", "--show-interface", str(interface)], - check=True, capture_output=True, text=True, + check=True, + capture_output=True, + text=True, ) # Haddock currently writes this JSON to stderr, despite a successful exit. return json.loads(result.stdout.strip() or result.stderr.strip()) @@ -258,7 +166,8 @@ def check_haskell(data): continue declarations = re.findall( r"^(?:data|newtype|type|class)\s+(?:\([^\n]*?\)\s*=>\s*)?([A-Z]\w*)", - source.read_text(), re.MULTILINE, + source.read_text(), + re.MULTILINE, ) for declaration in declarations: symbol = module["module"] + "$" + declaration @@ -278,39 +187,45 @@ def check_haskell(data): return len(public), len(public_types) -def check_reference(interface=None, *, python_only=False): - kinds = python_kinds() +def check_reference(interface=None): + python = set() + for module in ("hmath", "hmath_cffi"): + python |= python_exports(ROOT / "bindings/python" / f"{module}.py", module) counts = { "Python": check_entries( - set(kinds), ROOT / "docs/reference/python.md", + python, + ROOT / "docs/reference/python.md", r"### `(hmath(?:_cffi)?\.[\w.]+)", - *python_signatures(), kinds=kinds, + *python_signatures(), ) } - if python_only: - print(f"Python API reference coverage: {counts['Python']} entries") - return counts c = set() c_parameters, c_signatures = {}, {} for header in ("hmath.h", "hmath_dlpack.h"): c.update( re.findall( r"^HMATH_API\s+[\w\s*]+?\b(hmath_\w+)\s*\([^;]*\);", - (ROOT / "include" / header).read_text(), re.MULTILINE, + (ROOT / "include" / header).read_text(), + re.MULTILINE, ) ) for match in re.finditer( r"^HMATH_API\s+([\w\s*]+?)\b(hmath_\w+)\s*\(([^;]*)\);", - (ROOT / "include" / header).read_text(), re.MULTILINE, + (ROOT / "include" / header).read_text(), + re.MULTILINE, ): c_signatures[match[2]] = f"{match[1]}{match[2]}({match[3]})" c_parameters[match[2]] = [ re.search(r"(\w+)\s*$", arg)[1] - for arg in match[3].split(",") if arg.strip() != "void" + for arg in match[3].split(",") + if arg.strip() != "void" ] counts["C"] = check_entries( - c, ROOT / "docs/reference/c.md", r"### `(hmath_\w+)`", - c_parameters, c_signatures, + c, + ROOT / "docs/reference/c.md", + r"### `(hmath_\w+)`", + c_parameters, + c_signatures, ) header = (ROOT / "include/hmath.h").read_text() table = (ROOT / "docs/reference/c.md").read_text() @@ -322,11 +237,14 @@ def check_reference(interface=None, *, python_only=False): wave = set( re.findall( r"^pub fun (\w+)\(", - (ROOT / "bindings/wave/hmath.wave").read_text(), re.MULTILINE, + (ROOT / "bindings/wave/hmath.wave").read_text(), + re.MULTILINE, ) ) counts["Wave"] = check_entries( - wave, ROOT / "docs/reference/wave.md", r"### `(\w+)\(", + wave, + ROOT / "docs/reference/wave.md", + r"### `(\w+)\(", { match[1]: re.findall(r"(\w+)\s*:", match[2]) for match in re.finditer( @@ -339,7 +257,8 @@ def check_reference(interface=None, *, python_only=False): match[1]: match[0].removeprefix("pub fun ").strip() for match in re.finditer( r"^pub fun (\w+)\([^)]*\)(?:\s*->\s*[^\s{]+)?", - (ROOT / "bindings/wave/hmath.wave").read_text(), re.MULTILINE, + (ROOT / "bindings/wave/hmath.wave").read_text(), + re.MULTILINE, ) }, ) @@ -357,8 +276,5 @@ def check_reference(interface=None, *, python_only=False): if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--haddock-interface", type=Path) - parser.add_argument("--python-only", action="store_true") args = parser.parse_args() - if args.python_only and args.haddock_interface: - parser.error("--python-only cannot be combined with --haddock-interface") - check_reference(args.haddock_interface, python_only=args.python_only) + check_reference(args.haddock_interface) diff --git a/tools/check_docs_examples.py b/tools/check_docs_examples.py index 0b2ef64..7870d57 100644 --- a/tools/check_docs_examples.py +++ b/tools/check_docs_examples.py @@ -1,115 +1,17 @@ # SPDX-License-Identifier: MPL-2.0 -"""Check documentation Python examples against an installed distribution. +"""Execute Python examples from the documentation against an installed wheel.""" -Pages marked ```` run each Python block in -its own interpreter. Other pages retain a shared namespace within that page, -so sequential tutorials continue to work. Every page has its own process. -""" - -import argparse -import importlib.metadata -import json import re -import subprocess -import sys from pathlib import Path -ROOT = Path(__file__).resolve().parent.parent -COMPLETE = "__HMATH_DOC_EXAMPLES_COMPLETED__" -INDEPENDENT = "" -FENCE = re.compile(r"^```python[ \t]*\n(.*?)^```[ \t]*$", re.MULTILINE | re.DOTALL) -WORKER = """ -import json -import sys - -payload = json.load(sys.stdin) -namespace = {"__name__": "__main__"} -for line, source in payload["examples"]: - code = "\\n" * (line - 1) + source - exec(compile(code, payload["path"], "exec"), namespace) -sys.stdout.write("\\n__HMATH_DOC_EXAMPLES_COMPLETED__\\n") -""" - - -def read_examples(path): - """Return (first source line, source) pairs and the page isolation mode.""" - text = path.read_text(encoding="utf-8") - examples = [ - (text.count("\n", 0, match.start(1)) + 1, match[1]) - for match in FENCE.finditer(text) - ] - openings = len(re.findall(r"^```python[ \t]*$", text, re.MULTILINE)) - if openings != len(examples): - raise ValueError(f"{path}: unclosed Python code fence") - independent = bool( - re.search(r"^" + re.escape(INDEPENDENT) + r"[ \t]*$", text, re.MULTILINE) +root = Path(__file__).resolve().parent.parent +for path in sorted((root / "docs").rglob("*.md")): + namespace = {"__name__": "__main__"} + examples = re.findall( + r"^```python\n(.*?)^```", path.read_text(), re.MULTILINE | re.DOTALL ) - return examples, independent - - -def check_page(path, *, syntax_only=False, timeout=60): - """Check a page, preserving source line numbers in failure diagnostics.""" - examples, independent = read_examples(path) - for line, source in examples: - compile("\n" * (line - 1) + source, str(path), "exec") - if syntax_only or not examples: - return len(examples) - groups = [[example] for example in examples] if independent else [examples] - for group in groups: - payload = {"path": str(path), "examples": group} - label = f"{path}:{group[0][0]}" - try: - result = subprocess.run( - [sys.executable, "-I", "-c", WORKER], - input=json.dumps(payload), - text=True, - capture_output=True, - timeout=timeout, - check=False, - ) - except subprocess.TimeoutExpired as error: - raise RuntimeError(f"{label}: example exceeded {timeout}s") from error - if result.returncode or not result.stdout.rstrip().endswith(COMPLETE): - raise RuntimeError( - f"{label}: example failed (exit {result.returncode})\n" - f"{result.stdout}{result.stderr}" - ) - return len(examples) - - -def check_installed_version(expected): - """Reject version drift before running any documentation examples.""" - actual = importlib.metadata.version("hmath-python") - if actual != expected: - raise ValueError(f"Expected hmath-python {expected}, found {actual}") - print(f"Documentation runtime: hmath-python {actual}") - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("paths", nargs="*", type=Path) - parser.add_argument("--check-syntax", action="store_true") - parser.add_argument("--expected-version") - parser.add_argument("--timeout", type=float, default=60) - args = parser.parse_args() - if args.timeout <= 0: - parser.error("--timeout must be positive") - if args.expected_version and args.check_syntax: - parser.error("--expected-version requires execution, not --check-syntax") - if args.expected_version: - check_installed_version(args.expected_version) - paths = args.paths or sorted((ROOT / "docs").rglob("*.md")) - total = 0 - for path in paths: - count = check_page(path, syntax_only=args.check_syntax, timeout=args.timeout) - total += count - if count: - mode = "syntax checked" if args.check_syntax else "executed" - print(f"{path}: {count} Python examples {mode}") - if not total: - raise SystemExit("No Python examples found in the selected paths") - print(f"Python documentation examples: {total}") - - -if __name__ == "__main__": - main() + for example in examples: + # These are executable examples from the checked-out project itself. + exec(compile(example, str(path), "exec"), namespace) # noqa: S102 + if examples: + print(f"{path.relative_to(root)}: {len(examples)} Python examples pass") diff --git a/tools/test_docs_checks.py b/tools/test_docs_checks.py deleted file mode 100644 index e100fb4..0000000 --- a/tools/test_docs_checks.py +++ /dev/null @@ -1,241 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -"""Regression tests for documentation checks; no native runtime is required.""" - -import tempfile -import unittest -from pathlib import Path -from unittest.mock import patch - -import check_api_docs as api -import check_docs_examples as examples - - -class ReferenceChecks(unittest.TestCase): - def setUp(self): - self.directory = tempfile.TemporaryDirectory() - self.addCleanup(self.directory.cleanup) - self.page = Path(self.directory.name) / "reference.md" - self.name = "hmath.example" - self.signature = "hmath.example(value, copy=True)" - self.parameters = {self.name: ["value", "copy"]} - self.signatures = {self.name: self.signature} - self.defaults = {self.name: {"copy": "True"}} - self.document = '''### `hmath.example(value, copy=True)` - - - -Return an example value. - -#### Parameters - -| Name | Type / default | Description | -| --- | --- | --- | -| `value` | `float` | Input. | -| `copy` | `bool; default True` | Copy policy. | - -#### Returns - -`float`. - -#### See Also - -Related operations. - -#### Notes - -Example contract. - -#### Examples - -```python -assert 1 + 1 == 2 -``` -''' - - def check(self, document=None, kind="callable"): - self.page.write_text(self.document if document is None else document) - return api.check_entries( - {self.name}, self.page, r"### `(hmath\.[\w.]+)", - self.parameters, self.signatures, self.defaults, - kinds={self.name: kind}, - ) - - def test_callable(self): - self.assertEqual(self.check(), 1) - - def test_missing_entry(self): - with self.assertRaisesRegex(ValueError, "missing entries"): - self.check("") - - def test_duplicate_entry(self): - with self.assertRaisesRegex(ValueError, "duplicate entry"): - self.check(self.document * 2) - - def test_unknown_entry(self): - with self.assertRaisesRegex(ValueError, "unknown Python entries"): - self.check(self.document + "\n### `hmath.unknown()`\n") - - def test_wrong_parameter_order(self): - self.parameters[self.name].reverse() - with self.assertRaisesRegex(ValueError, "parameter names/order"): - self.check() - - def test_wrong_default(self): - with self.assertRaisesRegex(ValueError, "default True"): - self.check(self.document.replace("default True", "default False")) - - def test_wrong_heading_not_rescued_by_body(self): - document = self.document.replace(self.signature, "hmath.example(value)") - document += "\n" + self.signature + "\n" - with self.assertRaisesRegex(ValueError, "signature differs"): - self.check(document) - - def test_missing_anchor(self): - with self.assertRaisesRegex(ValueError, "stable symbol anchor"): - self.check(self.document.replace('', "")) - - def test_out_of_order_sections(self): - document = self.document.replace("#### Notes", "#### References") - document = document.replace("#### Examples", "#### Notes") - with self.assertRaisesRegex(ValueError, "out of order"): - self.check(document) - - def test_duplicate_section(self): - with self.assertRaisesRegex(ValueError, "duplicate sections"): - self.check(self.document + "\n#### Notes\n\nRepeated.\n") - - def test_class_without_returns(self): - document = self.document.replace("#### Returns", "#### Attributes") - self.assertEqual(self.check(document, kind="class"), 1) - - def test_class_rejects_returns(self): - document = self.document.replace( - "#### See Also", "#### Attributes\n\n`value` : float\n\n#### See Also" - ) - with self.assertRaisesRegex(ValueError, "not a callable result"): - self.check(document, kind="class") - - def test_attribute_without_call_parentheses(self): - self.parameters[self.name] = [] - self.signatures[self.name] = self.name - self.defaults[self.name] = {} - document = '''### `hmath.example` - - - -Read a value. - -#### Type - -`float`. - -#### See Also - -Other attributes. -''' - self.assertEqual(self.check(document, kind="attribute"), 1) - with self.assertRaisesRegex(ValueError, "signature differs"): - self.check( - document.replace("`hmath.example`", "`hmath.example()`"), kind="attribute" - ) - - def test_legacy_c_wave_format_is_preserved(self): - self.page.write_text(self.document.replace("#### See Also", "#### See also")) - self.assertEqual( - api.check_entries( - {self.name}, self.page, r"### `(hmath\.[\w.]+)", - self.parameters, self.signatures, self.defaults, - ), 1, - ) - - def test_source_inventory_includes_attributes(self): - kinds = api.python_kinds() - for name in api.PYTHON_ATTRIBUTES: - self.assertEqual(kinds[name], "attribute") - self.assertEqual(kinds["hmath.Tensor"], "class") - self.assertEqual(kinds["hmath.Tensor.shape"], "attribute") - self.assertEqual(kinds["hmath.Tensor.dot"], "callable") - self.assertNotIn("hmath.threading", kinds) - self.assertNotIn("hmath.Tensor._live", kinds) - - def test_actual_python_reference(self): - self.assertGreater(api.check_reference(python_only=True)["Python"], 0) - - -class ExampleChecks(unittest.TestCase): - def setUp(self): - self.directory = tempfile.TemporaryDirectory() - self.addCleanup(self.directory.cleanup) - self.page = Path(self.directory.name) / "examples.md" - - def check(self, text, **kwargs): - self.page.write_text(text) - return examples.check_page(self.page, **kwargs) - - def test_tutorial_keeps_page_namespace(self): - text = "```python\nx = 3\n```\n\n```python\nassert x == 3\n```\n" - self.assertEqual(self.check(text), 2) - - def test_inline_marker_does_not_change_tutorial_mode(self): - text = "Describe `" + examples.INDEPENDENT + "` here.\n" - text += "```python\nx = 3\n```\n```python\nassert x == 3\n```\n" - self.assertEqual(self.check(text), 2) - - def test_reference_blocks_have_no_hidden_imports(self): - text = examples.INDEPENDENT + "\n" - text += "```python\nx = 3\n```\n```python\nassert x == 3\n```\n" - with self.assertRaisesRegex(RuntimeError, "NameError"): - self.check(text) - - def test_reference_blocks_have_separate_processes(self): - text = examples.INDEPENDENT + ''' -```python -import builtins -builtins.hmath_example_sentinel = True -``` -```python -import builtins -assert not hasattr(builtins, "hmath_example_sentinel") -``` -''' - self.assertEqual(self.check(text), 2) - - def test_syntax_only_does_not_import_native_library(self): - text = "```python\nimport nonexistent_documentation_test_module\n```\n" - self.assertEqual(self.check(text, syntax_only=True), 1) - with self.assertRaisesRegex(RuntimeError, "ModuleNotFoundError"): - self.check(text) - - def test_failure_includes_markdown_location(self): - with self.assertRaisesRegex(RuntimeError, r"examples\.md:4"): - self.check("# Title\n\n```python\nassert False\n```\n") - - def test_syntax_error_uses_markdown_line(self): - with self.assertRaises(SyntaxError) as caught: - self.check("# Title\n\n```python\nif:\n```\n", syntax_only=True) - self.assertEqual(caught.exception.lineno, 4) - - def test_timeout(self): - with self.assertRaisesRegex(RuntimeError, "exceeded"): - self.check("```python\nimport time\ntime.sleep(5)\n```\n", timeout=0.05) - - def test_early_success_exit_is_not_a_pass(self): - with self.assertRaisesRegex(RuntimeError, "example failed"): - self.check("```python\nraise SystemExit(0)\n```\n") - - def test_unclosed_fence(self): - with self.assertRaisesRegex(ValueError, "unclosed Python"): - self.check("```python\nassert True\n", syntax_only=True) - - def test_installed_version_mismatch(self): - with patch.object(examples.importlib.metadata, "version", return_value="0.0.0"): - with self.assertRaisesRegex(ValueError, "Expected hmath-python 1.0.2"): - examples.check_installed_version("1.0.2") - - def test_installed_version_match(self): - with patch.object(examples.importlib.metadata, "version", return_value="1.0.2"): - examples.check_installed_version("1.0.2") - - -if __name__ == "__main__": - unittest.main() From 65d536c6f96b3038d55c1805ee51c79ab7964158 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Thu, 29 Oct 2026 14:57:25 +0900 Subject: [PATCH 3/4] docs: preserve existing reference and expand Python usage contracts Retain every line of the existing API reference, NumPy guide, contributor notes, and numerical conventions while supplementing verified contracts, worked examples, numerical behavior, and ownership explanations. Keep the original API heading signatures, required Markdown sections, existing Python documentation validator, and workflow configuration. Demonstrate irreversible shutdown as non-executable text so the existing sequential example runner remains usable. --- docs/contributing.md | 178 +-- docs/explanation/numerical-conventions.md | 2 - docs/guides/numpy.md | 49 +- docs/reference/python.md | 1315 ++++++++++----------- 4 files changed, 696 insertions(+), 848 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index 9507237..7c25ee9 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -26,148 +26,56 @@ python tools/build_docs.py python -m mkdocs serve --config-file build-docs/mkdocs.generated.yml ``` -The complete site build requires the Haskell toolchain and generates the -Haskell API reference. It validates reference coverage, Haskell example -programs, the strict MkDocs build, and local file/fragment links. Do not commit -generated HTML. CI deploys the generated `site/` after changes reach `master`. -The home-page version is substituted from `pyproject.toml` during generation; -documentation changes alone do not require a package release. - -For fast Python documentation checks without a native runtime or Haskell -compiler, run: - -```sh -python -m unittest discover -s tools -p 'test_docs_checks.py' -python tools/check_api_docs.py --python-only -python tools/check_docs_examples.py --check-syntax -``` - -Syntax checking is not execution. To validate the current checkout, install -its wheel (GHC and Cabal are required for the native source build) and run: +To build the site together with the complete Haskell API reference, install +the Haskell toolchain and run: ```sh -python -m pip install --force-reinstall '.[numpy,cffi]' -python tools/check_docs_examples.py +python tools/build_docs.py ``` -To validate a published version, use a clean environment and install that -exact version of `hmath-python[numpy,cffi]`, then pass the same version to -`python tools/check_docs_examples.py --expected-version VERSION`. Replace -`VERSION` with a real release identifier; it is not a literal command argument. -The checker rejects an installed distribution with a different version. -The Documentation workflow normally tests a wheel built from the checkout; -its manual `python-runtime` input selects `released` for explicit published- -package validation. The tested distribution version is printed in the log. -These documentation checks do not replace the portable wheel pipeline. +CI builds with strict validation and deploys the generated `site/` directory +to GitHub Pages after changes reach `master`. Do not commit generated HTML. +Use runnable examples with expected results; distinguish available interfaces +from future or Haskell-only features. ## Reference writing standard -Use the [numpydoc section conventions](https://numpydoc.readthedocs.io/en/stable/format.html) +Follow the [numpydoc section conventions](https://numpydoc.readthedocs.io/en/latest/format.html) for Python reference content and [Haddock conventions](https://haskell-haddock.readthedocs.io/latest/markup.html) -for Haskell source documentation. The website uses Markdown with definition -lists; it adopts numpydoc's content conventions, not its reStructuredText -syntax. This does not require changing the site generator. Python source -docstrings, when edited, follow [PEP 257](https://peps.python.org/pep-0257/), -and examples follow [PEP 8](https://peps.python.org/pep-0008/). - -### Python object kinds and sections - -Every entry has a one-line purpose, an exact source signature (or the name of -an attribute without call parentheses), and a stable symbol anchor, for -example ``. Preserve existing heading-derived -anchors when changing navigation. Link new references to the symbol anchor -rather than to a signature whose defaults may change. - -Use the following section order, omitting optional sections when irrelevant: -`Parameters`, `Returns`, `Raises`, `Warns`, `Warnings`, `Other Parameters`, -`Attributes`, `Methods`, `See Also`, `Notes`, `References`, `Examples`. -Keep mathematical definitions, algorithms, numerical behavior, and cost -explanations under `Notes`; use lower-level headings there when needed. -Do not invent a paper reference or an error estimate for a simple accessor. - -| Object | Required content | Distinction | -| --- | --- | --- | -| Function or method | `Parameters`, `Returns`, `See Also` | State `None.` for an empty parameter list and document a `None` return explicitly. | -| Class, including an exception | Constructor `Parameters`, public `Attributes`, `See Also` | Do not add a function-style `Returns` section to the class description. | -| Property or public data attribute | `Type`, `See Also` | No call parentheses, `Parameters`, or `Returns`; describe access/lifetime conditions as appropriate. | - -For `Parameters`, use a table with one row per argument in declaration order. -Include type, default value and its meaning, units when applicable, valid -shapes/ranges, and conversion behavior. Keep the heading signature identical -to the source; explanatory types do not justify adding nonexistent annotations -or arguments. A constructor's `shape=None` must describe how shape is chosen. - -`Returns` identifies type, meaning, shape, and whether a result is a new owned -handle, an independent copy, or a view. `Raises` names actual exception classes -and conditions, distinguishing wrapper validation, conversion, runtime -initialization, and checked native failures. Native status codes belong only -on paths where they can occur. Do not replace these conditions with "may fail" -or "constructor errors". Diagnostic text is not a string-matching guarantee. - -Public data attributes are part of reference coverage even when they are not -properties: `.status` and cffi `.shape` are intentional examples. Update the -explicit attribute inventory in `tools/check_api_docs.py` when that public -surface changes; raw FFI handles and incidental imports are not automatically -promoted to public APIs. Keep classes, methods, and attributes linked. - -The Python checker validates kinds, exact heading signatures, parameter order, -defaults, required/ordered sections, duplicate and unknown entries, and stable -symbol anchors. The C/Wave reference keeps its existing `Parameters`, -`Returns`, and `See also` convention and exact signature/status checks. - -### Numerical and memory contracts - -State the mathematical definition separately from implementation-specific -behavior. Check the actual Python-to-native-to-Haskell call path before naming -an algorithm, backend, complexity, or precision property. Distinguish arithmetic -work from input gathering, output allocation, and validation; zero-copy is not -synonymous with allocation-free or constant-time. Include empty and rank-zero -inputs where meaningful. Work budgets are not accuracy guarantees. - -Preserve the distinction between Python-exposed functionality and Haskell-only -or future functionality. Do not claim NumPy, Python Array API, ISO, or WCAG -conformance merely because documentation resembles another project's pages. -A compatibility or accuracy guarantee needs its own evidence. - -Document ownership for every returned handle/view, the effect of closing the -original handle, the caller's obligations for borrowed aliases, and the scope -of any synchronization. Thread safety of individual operations is not an -atomicity guarantee for arbitrary sequences or external array mutation. - -### Executable examples - -Reference examples explain usage; they are not an exhaustive replacement for -regression tests. Include imports, input data, observable/checked results, and -resource cleanup. Simple exact examples may use equality; general floating- -point comparisons need explicit, justified tolerances. An error example must -fail when the expected exception is not raised, and close unexpected results. - -Mark a page whose Python blocks are independent with -``. Each fenced `python` block then runs in -a fresh isolated interpreter, including shutdown examples. Pages without the -marker retain a shared namespace within a page for sequential tutorials, but -never share a process with another page. Keep each independent example complete; -no imports or objects may be borrowed from a preceding block. Use ordinary -`python` fences, not `text` fences to hide runnable examples from checks. - -The example checker reports original Markdown line numbers, times out hung -processes, and requires completion of every block. `--check-syntax` compiles -without importing HMath; `--expected-version` verifies the installed package -before execution. Run C/Wave example programs with the native build when -changing them. Complete Haskell example programs are checked against the -checkout during the full documentation build. - -### Navigation and review - -Keep task guides distinct from reference contracts and conceptual explanations, -following [Diátaxis](https://diataxis.fr/start-here/). The existing -`getting-started/`, `guides/`, `explanation/`, and `reference/` structure is the -starting point; do not split files merely to imitate another library. -Haskell indexes continue to be generated from declarations and authored +for Haskell source documentation. The website uses Markdown; adopting these +content conventions does not require changing its site generator. + +Each Python, C, or Wave entry needs a one-line purpose, exact signature, +`Parameters` table with one row per argument in declaration order, a `Returns` +section, and relevant `See also` links. Include types, defaults, units, shapes, +bounds, ownership, and failure conditions where they apply. Use `None.` for +an empty parameter list. Properties have no call parentheses. Explain errors +with the actual exception/status/constructor, rather than “may fail”. + +Add examples for operations that need an explanation of usage. Show expected +results, resource cleanup, and meaningful failure cases. For numerical +algorithms, explain the equation, assumptions, stopping criteria, and accuracy +limitations; keep implementation history out of user instructions. A simple +getter does not need a paper citation or an artificial standalone example. + +Haskell module and area indexes are generated from declarations and authored Haddock prose, including constructors, record fields, and class methods. +The API check includes symbolic function names as well as ordinary identifiers. +Keep standalone task guides distinct from reference contracts and conceptual +explanations, following [Diátaxis](https://diataxis.fr/start-here/). + +The documentation workflow checks source signatures and parameter order, +reference coverage, local file/fragment links, Python examples against the +released wheel, and complete Haskell example programs against this checkout. +Run the C/Wave example programs with the native build when changing them. +The home-page version is substituted from `pyproject.toml` during generation; +documentation changes alone do not require a package release. + + +## Additional Python documentation guidance + +The Python reference follows [numpydoc content conventions](https://numpydoc.readthedocs.io/en/stable/format.html) adapted to the existing Markdown layout. Keep the current `Parameters`, `Returns`, and `See also` headings required by the source-derived checker. For classes and properties, preserve these sections while documenting public attributes, ownership, and access behavior in the accompanying prose; do not change the build or checking tools for a documentation-only improvement. + +Document finite binary64 behavior, exact shape and rank restrictions, exception types and status codes, and the distinction between mathematical definitions and implementation-specific numerical properties. Derive claims from the actual public binding and native kernel path. Never infer Python features from Haskell-only APIs, and do not claim NumPy compatibility for operations with different semantics. -Review heading hierarchy, tables, definition lists, rendered formulas, code -copying, keyboard navigation, zoom, and contrast in the built site. Preserve -useful existing explanations and inbound links. PRs should identify the source -version, documentation scope, checks actually executed, and any validation -that still depends on CI or a toolchain unavailable locally. +Include standalone imports in extended examples and clearly state the expected result or checked failure. The existing Python documentation checker executes all `python` fences from a Markdown file sequentially in one shared namespace; use `text` rather than `python` for irreversible runtime-shutdown demonstrations. Existing source signatures and section headings must remain stable so documentation changes pass the unchanged CI checks. diff --git a/docs/explanation/numerical-conventions.md b/docs/explanation/numerical-conventions.md index 91d9fdd..a6a73bd 100644 --- a/docs/explanation/numerical-conventions.md +++ b/docs/explanation/numerical-conventions.md @@ -122,5 +122,3 @@ bounds on every possible callback. - [SciPy linear solve](https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.solve.html): shape contracts and singular/ill-conditioned systems. - [GNU Scientific Library linear algebra](https://www.gnu.org/software/gsl/doc/html/linalg.html): decompositions, rank, and numerical tolerances. - [Python `math.isclose`](https://docs.python.org/3/library/math.html#math.isclose): approximate comparison semantics. -- [NumPy sum](https://numpy.org/doc/stable/reference/generated/numpy.sum.html): an example of documenting implementation-specific reduction precision; its algorithm is not an HMath guarantee. -- [HMath native boundary](https://github.com/wavefnd/hmath/blob/master/cbits/native.c), [Haskell FFI kernels](https://github.com/wavefnd/hmath/blob/master/src/HMath/FFI.hs), and [linear operations](https://github.com/wavefnd/hmath/blob/master/src/HMath/Linear.hs): implementation sources for the Python numerical path. diff --git a/docs/guides/numpy.md b/docs/guides/numpy.md index 06646c3..6c47d7e 100644 --- a/docs/guides/numpy.md +++ b/docs/guides/numpy.md @@ -1,13 +1,6 @@ # Work with NumPy arrays - - -Use this guide to move data between NumPy and HMath and choose an ownership -policy. Install the `numpy` extra as described in -[installation](../getting-started/installation.md). Each example is independent. -The complete argument and failure contracts are in -[Tensor.from_numpy](../reference/python.md#hmath.Tensor.from_numpy) and -[Tensor.numpy](../reference/python.md#hmath.Tensor.numpy). +Install the `numpy` extra as described in [installation](../getting-started/installation.md). ## Choose whether to copy @@ -27,21 +20,14 @@ conversion. Start with the copying path when another component may mutate data. import numpy as np from hmath import Tensor -source = np.array([[1, 2], [3, 4]], dtype=np.int32) +source = np.array([[1, 2], [3, 4]], dtype=np.float64) with Tensor.from_numpy(source) as tensor: assert tensor.shape == (2, 2) - source[0, 0] = 99 - result = tensor.numpy() - assert result.dtype == np.float64 - assert result.tolist() == [[1.0, 2.0], [3.0, 4.0]] -result[0, 0] = -1 -assert source[0, 0] == 99 + assert np.array_equal(tensor.numpy(), source) ``` -The input is converted to float64 and copied in logical C order. A returned -copy is writable and remains independent of both the Tensor and the original -array. Converting large integers to float64 can round them; copying does not -provide arbitrary-precision arithmetic. +Both conversions copy by default. Later changes to the original array do not +change the copied tensor, and the returned NumPy array owns independent data. ## Borrow an array without copying @@ -57,20 +43,20 @@ with Tensor.from_numpy(source[:, ::2], copy=False) as tensor: assert tensor.sum() == 30.0 assert np.shares_memory(view, source) assert not view.flags.writeable -assert view.tolist() == [[0.0, 2.0], [4.0, 6.0], [8.0, 10.0]] -del view ``` -This non-contiguous, positive-stride view is eligible. The normalized array -must be aligned, native-endian, read-only float64, with nonnegative byte strides -that are multiples of eight. Logical count, stride, and reachable storage span -must also fit the native budgets. Negative-stride views and writable arrays are -rejected; there is no automatic copy fallback when `copy=False`. +Zero-copy input requires native-endian, aligned, read-only `float64` storage +and nonnegative strides that are multiples of eight bytes. Negative-stride +views and writable arrays are rejected with `ValueError`. + +The caller must keep all aliases immutable until every tensor and derived +view has been released. Setting one array to read-only cannot prevent writes +through another writable alias. + +A NumPy view retains a native reference and its borrowed array owner, so it +remains valid after the original tensor is closed. Release the view when it +is no longer needed. Empty arrays can use a separate empty NumPy allocation. -The Tensor retains the array owner. Every alias to borrowed storage must remain -immutable until all tensors and derived views are released. Setting one array -read-only cannot prevent writes through another writable alias. Use this path -only when the application controls those aliases. ## Handle a conversion that needs copying @@ -100,6 +86,7 @@ silently copying: non-finite data and oversized layouts can remain invalid even when copied. `numpy.asarray` normalization can allocate for array-like inputs before HMath reaches its borrowing checks. + ## Keep a result view after closing its handle ```python @@ -122,6 +109,7 @@ handle, not all references to the storage. Release views when no longer needed. For empty arrays, `numpy(copy=False)` can use a separate empty read-only allocation; there are no element bytes to share. + ## Reshape a borrowed tensor deliberately Borrowing accepts some layouts that `reshape()` does not. A non-contiguous @@ -143,6 +131,7 @@ See [Tensor.reshape](../reference/python.md#hmath.Tensor.reshape) for its exact contiguity and shape rules. Unlike NumPy's reshape, it does not infer `-1` or fall back to a copy. + ## Compare a calculation with NumPy Use the same operation on each side. HMath `dot()` reduces corresponding diff --git a/docs/reference/python.md b/docs/reference/python.md index 22aadff..9af1257 100644 --- a/docs/reference/python.md +++ b/docs/reference/python.md @@ -1,107 +1,65 @@ # Python API - - -This reference describes the Python interfaces in HMath **@HMATH_VERSION@**, -using native **ABI 1**. Install `hmath-python` and import `hmath`; the distribution -name and the import name are different. See [installation](../getting-started/installation.md) -for supported wheels and optional dependencies. - -`hmath` is the primary interface. `hmath_cffi` is an optional, smaller interface -to the same native runtime. This page documents their public Python operations, -not every function available in the C ABI or the Haskell library. Each Python -example below includes its own imports and runs in a separate process during -documentation checks, including the final-shutdown example. +`hmath` is the primary interface. Calculations use finite IEEE 754 binary64 +values. Tensors are immutable and do not broadcast implicitly. NumPy is needed +only for the conversion methods. Python code blocks run in documentation checks; +the final-shutdown example is displayed separately. ## Function index -| Area | Primary interface | Optional cffi interface | -| --- | --- | --- | -| Runtime | [initialize](#hmath.initialize), [shutdown](#hmath.shutdown) | Initialized when the module is imported. | -| Scalars | [add](#hmath.add) | [add](#hmath_cffi.add) | -| Exceptions | [HMathError](#hmath.HMathError), [status](#hmath.HMathError.status) | [HMathError](#hmath_cffi.HMathError), [status](#hmath_cffi.HMathError.status), [check](#hmath_cffi.check) | -| Construction | [Tensor](#hmath.Tensor), [scalar](#hmath.Tensor.scalar), [matrix](#hmath.Tensor.matrix), [from_numpy](#hmath.Tensor.from_numpy) | [Tensor](#hmath_cffi.Tensor) | -| Inspection | [shape](#hmath.Tensor.shape), [values](#hmath.Tensor.values), [numpy](#hmath.Tensor.numpy) | [shape](#hmath_cffi.Tensor.shape), [values](#hmath_cffi.Tensor.values) | -| Calculations | [sum](#hmath.Tensor.sum), [dot](#hmath.Tensor.dot), [+](#hmath.Tensor.__add__), [@](#hmath.Tensor.__matmul__) | [sum](#hmath_cffi.Tensor.sum), [dot](#hmath_cffi.Tensor.dot), [+](#hmath_cffi.Tensor.__add__), [@](#hmath_cffi.Tensor.__matmul__) | -| Shape changes | [reshape](#hmath.Tensor.reshape) | Not exposed. | -| Lifetime | [close](#hmath.Tensor.close), [with](#hmath.Tensor.__enter__), [exit](#hmath.Tensor.__exit__), [finalizer](#hmath.Tensor.__del__) | [close](#hmath_cffi.Tensor.close), [with](#hmath_cffi.Tensor.__enter__), [exit](#hmath_cffi.Tensor.__exit__) | +| API | Purpose | +| --- | --- | +| [hmath.initialize](#hmathinitialize) | Initialize the shared numerical runtime. | +| [hmath.add](#hmathadda-b) | Add two real scalars. | +| [hmath.shutdown](#hmathshutdown) | Permanently shut down the process-owned runtime. | +| [hmath.HMathError](#hmathhmatherrorstatus) | Represent a failed native checked operation. | +| [hmath.Tensor](#hmathtensorvalues-shapenone) | Copy flat values into a new immutable tensor. | +| [hmath.Tensor.scalar](#hmathtensorscalarvalue) | Construct a rank-zero tensor. | +| [hmath.Tensor.matrix](#hmathtensormatrixvalues-rows-columns) | Construct a rank-two matrix from flat row-major values. | +| [hmath.Tensor.from_numpy](#hmathtensorfrom_numpyarray-copytrue) | Convert an array to a tensor, copying or borrowing its storage. | +| [hmath.Tensor.shape](#hmathtensorshape) | Read the tensor’s dimensions. | +| [hmath.Tensor.values](#hmathtensorvalues) | Copy logical elements to a flat Python list. | +| [hmath.Tensor.numpy](#hmathtensornumpycopytrue) | Return a NumPy copy or a read-only view. | +| [hmath.Tensor.sum](#hmathtensorsum) | Sum all tensor elements to one scalar. | +| [hmath.Tensor.dot](#hmathtensordotother) | Reduce corresponding products of two identically shaped tensors. | +| [hmath.Tensor.__add__](#hmathtensor__add__other-a-b) | Add two tensors element by element. | +| [hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b) | Multiply two rank-two matrices. | +| [hmath.Tensor.reshape](#hmathtensorreshapeshape) | Create a new shape over the same contiguous storage. | +| [hmath.Tensor.close](#hmathtensorclose) | Release this tensor handle deterministically. | +| [hmath.Tensor.__enter__](#hmathtensor__enter__) | Enter a context that owns this tensor handle. | +| [hmath.Tensor.__exit__](#hmathtensor__exit__args) | Close the tensor when leaving a context. | +| [hmath.Tensor.__del__](#hmathtensor__del__) | Release the handle as a garbage-collection fallback. | +| [hmath_cffi.add](#hmath_cffiadda-b) | Add two real scalars. | +| [hmath_cffi.HMathError](#hmath_cffihmatherrorstatus) | Represent a failed native checked operation. | +| [hmath_cffi.check](#hmath_cfficheckstatus) | Convert a raw native status into a Python exception. | +| [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone) | Copy flat values into a new immutable tensor. | +| [hmath_cffi.Tensor.values](#hmath_cffitensorvalues) | Copy logical elements to a flat Python list. | +| [hmath_cffi.Tensor.sum](#hmath_cffitensorsum) | Sum all tensor elements to one scalar. | +| [hmath_cffi.Tensor.dot](#hmath_cffitensordotother) | Reduce corresponding products of two identically shaped tensors. | +| [hmath_cffi.Tensor.__add__](#hmath_cffitensor__add__other-a-b) | Add two tensors element by element. | +| [hmath_cffi.Tensor.__matmul__](#hmath_cffitensor__matmul__other-a-b) | Multiply two rank-two matrices. | +| [hmath_cffi.Tensor.close](#hmath_cffitensorclose) | Release this tensor handle deterministically. | +| [hmath_cffi.Tensor.__enter__](#hmath_cffitensor__enter__) | Enter a context that owns this tensor handle. | +| [hmath_cffi.Tensor.__exit__](#hmath_cffitensor__exit__args) | Close the tensor when leaving a context. | ## Common contracts -### Numeric values and shapes - -Tensor elements and scalar calculations use finite IEEE 754 binary64 values. -Python integers are converted to this representation; arbitrary-size integer -arithmetic is not provided. Rounding can occur during conversion and arithmetic. -NaN and infinity are rejected as native inputs, and a non-finite computed result -is reported as an error rather than returned as a successful value. - -| Shape | Meaning | Required number of values | -| --- | --- | --- | -| `()` | Rank-zero scalar | 1 | -| `(n,)` | Vector | n | -| `(m, n)` | Matrix | m × n | -| `(0, n)` | Empty matrix | 0 | - -Shapes contain at most **8** dimensions. Each dimension must be a built-in -Python `int` in `[0, 1_048_576]`; `bool`, integer subclasses, and NumPy integer -scalars are not accepted as dimensions by the Python shape validator. Convert -such dimensions explicitly with `int()` when appropriate. The logical element -count must not exceed **1,048,576**. Borrowed layouts also have bounded strides -and a bounded reachable storage span. A zero dimension makes the logical count -zero, but does not waive the other layout checks. `-1` dimension inference is -not supported. - -`values` inputs are flat, in logical row-major (C) order. Nested lists belong -in `Tensor.from_numpy(...)`, not in the flat `Tensor(...)` constructor. Shape -must match exactly for elementwise addition and `dot`; there is no implicit -broadcasting. `@` accepts compatible rank-two matrices only. - -### Ownership and concurrency - -Every returned Tensor owns a native handle. Use `with` or `close()` to release -that handle. `reshape()` and NumPy views can retain the same storage through -independent references; closing one handle does not close those references. -Tensor operations do not mutate their inputs. - -The primary interface locks handle operations, including `close()`. A sequence -of separate calls is not an atomic transaction, and locking cannot protect -against writes through external NumPy aliases. In particular, entering a -`with` block does not reserve a handle against another thread closing it. -The cffi interface has no corresponding per-handle locks: callers must prevent -concurrent close/use. Process-wide shutdown requires application-level -coordination for both interfaces. See [architecture and ownership](../explanation/architecture.md). - -### Errors - -| Layer | Exception or status | Interpretation | -| --- | --- | --- | -| Python shape/size validation | `ValueError` | A Python-side dimension, rank, size, or borrowing condition is invalid. | -| Input conversion | `TypeError`, `ValueError`, `OverflowError`, or `ctypes.ArgumentError` | Conversion failed before a native checked operation; the exact class depends on the wrapper and conversion path. | -| Closed primary/cffi handle | `RuntimeError` | An operation requires a live handle. | -| Native checked operation | The interface's `HMathError` | Inspect `.status` and `str(error)`. | -| Missing optional dependency | `ImportError` | The requested NumPy or cffi functionality is unavailable. | - -Frequently relevant native codes are 1 (invalid/non-finite input), 2 (shape), -3 (overflow, non-finite result, or a work/layout budget), 4 (allocation), -5 (runtime unavailable), 6 (contained Haskell exception), and 9 (shutdown busy). -The [C ABI status table](c.md) is authoritative for names and numeric values; -not every C status is reachable from every Python operation. Exception messages -provide diagnostics, not a stable string-matching API. Allocation of Python -objects can also raise `MemoryError`; iterator and conversion hooks can propagate -their own exceptions. The entries below describe the expected API-specific cases. - -Both wrappers require ABI 1. Module import can fail before any function call if -the native library cannot be loaded or the ABI is incompatible. See -[troubleshooting](../guides/troubleshooting.md). +- Shapes contain at most **8** nonnegative Python integers (not booleans). + Each dimension and the total size are bounded by **1,048,576**. Empty + dimensions are supported. Shape `()` is a scalar with exactly one value. +- Use `with` or `close()` to release each owned tensor. Closing one handle + preserves independent retained handles and NumPy views. +- Native failures raise `HMathError`. Shape validation raises `ValueError`; + use after close raises `RuntimeError`. Numeric conversion can also raise + Python `TypeError`, `ValueError`, or `OverflowError`. +- Primary-interface methods synchronize access to tensor handles. Prevent + concurrent close/use yourself when using the optional cffi interface. ## Module functions ### `hmath.initialize()` - - -Initialize the process-wide numerical runtime. +Initialize the shared numerical runtime. #### Parameters @@ -113,15 +71,18 @@ None. #### Raises -`RuntimeError` -: The native ABI is incompatible, or the runtime was permanently shut down. +`RuntimeError` for ABI incompatibility or +an irreversibly shut down runtime. -#### See Also +**Failure conditions by exception:** -[hmath.shutdown](#hmath.shutdown), [hmath.add](#hmath.add). +- **`RuntimeError`** — The native ABI is incompatible, or the runtime was permanently shut down. #### Notes +Initialize the process-wide runtime; repeated calls are +allowed. Constructors and `add()` already do this automatically. + Repeated initialization is allowed. Tensor construction and `add()` already initialize automatically. This does not create a private runtime or a resource that each caller should later shut down. @@ -130,44 +91,41 @@ that each caller should later shut down. ```python from hmath import initialize - initialize() -initialize() # Repeated initialization is allowed. +initialize() ``` -### `hmath.add(a, b)` +#### See also + +[hmath.shutdown](#hmathshutdown), [hmath.add](#hmathadda-b). - +### `hmath.add(a, b)` -Add two finite real scalars. +Add two real scalars. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `a` | `real scalar` | First operand, convertible to C `double` by ctypes. | -| `b` | `real scalar` | Second operand, convertible to C `double` by ctypes. | +| `a` | `real scalar` | First finite value, convertible to C double. | +| `b` | `real scalar` | Second finite value, convertible to C double. | #### Returns -`float` -: The binary64 sum of the converted operands. +their sum as a Python `float`, initializing the runtime if needed. #### Raises -`ctypes.ArgumentError` -: ctypes cannot convert an operand to the declared C argument type. +`HMathError` for non-finite input/result, or runtime/conversion errors. -`RuntimeError` -: Automatic initialization finds an incompatible or shut-down runtime. +**Failure conditions by exception:** -`HMathError` -: Non-finite input (status 1), non-finite result (3), runtime failure (5), - or a contained Haskell exception (6). +- **`ctypes.ArgumentError`** — ctypes cannot convert an operand to the declared C argument type. -#### See Also +- **`RuntimeError`** — Automatic initialization finds an incompatible or shut-down runtime. -[Tensor addition](#hmath.Tensor.__add__), [hmath.initialize](#hmath.initialize). +- **`HMathError`** — Non-finite input (status 1), non-finite result (3), runtime failure (5), + or a contained Haskell exception (6). #### Notes @@ -177,6 +135,11 @@ non-exact floating-point comparisons. #### Examples +```python +from hmath import add +assert add(2, 3.5) == 5.5 +``` + ```python import math from hmath import HMathError, add @@ -191,11 +154,13 @@ else: raise AssertionError("non-finite input must be rejected") ``` -### `hmath.shutdown()` +#### See also - +[hmath.Tensor.__add__](#hmathtensor__add__other-a-b), [hmath.initialize](#hmathinitialize). -Permanently shut down the process-owned numerical runtime. +### `hmath.shutdown()` + +Permanently shut down the process-owned runtime. #### Parameters @@ -203,20 +168,26 @@ None. #### Returns -`None` on successful shutdown. +`None`. #### Raises -`HMathError` -: Active work prevents shutdown (status 9). Join the numerical workers and - retry only after all users have finished. +`HMathError` with status 9 while work is active; +join workers and retry. Successful shutdown is **irreversible**. -#### See Also +Example, as the last operation of a dedicated process (not run in shared tests): -[hmath.initialize](#hmath.initialize), [hmath.Tensor.close](#hmath.Tensor.close). +**Failure conditions by exception:** + +- **`HMathError`** — Active work prevents shutdown (status 9). Join the numerical workers and + retry only after all users have finished. #### Notes +Only the process owner should call this after joining +all numerical users and releasing their resources. Applications can instead +allow process exit to clean up the runtime. + Only the process owner should call this, after joining numerical users and releasing their resources. It is not per-Tensor cleanup. A successful shutdown is irreversible: later `initialize()` calls fail. Applications may instead let @@ -224,22 +195,16 @@ process exit clean up the runtime. This example runs in its own process. #### Examples -```python -from hmath import add, initialize, shutdown - -assert add(1, 2) == 3.0 +```text +from hmath import shutdown shutdown() -try: - initialize() -except RuntimeError: - pass -else: - raise AssertionError("a shut-down runtime must not restart") ``` -### `hmath.HMathError(status)` +#### See also - +[hmath.initialize](#hmathinitialize), [hmath.Tensor.close](#hmathtensorclose). + +### `hmath.HMathError(status)` Represent a failed native checked operation. @@ -247,16 +212,13 @@ Represent a failed native checked operation. | Name | Type / default | Description | | --- | --- | --- | -| `status` | `int` | Native failure code captured by the wrapper. | - -#### Attributes - -`status` : `int` -: Native failure code; see [HMathError.status](#hmath.HMathError.status). +| `status` | `int` | Native status code; available as the exception’s status attribute. | -#### See Also +#### Returns -[hmath.HMathError.status](#hmath.HMathError.status), [hmath_cffi.HMathError](#hmath_cffi.HMathError). +a `RuntimeError` subclass instance with `.status` and a snapshot +of the calling thread's native diagnostic in `str(error)`. Catch exceptions +raised by operations rather than constructing this manually. #### Notes @@ -269,78 +231,58 @@ new diagnostic. The cffi exception is a different Python class. ```python from hmath import HMathError, Tensor - try: - with Tensor([1, 2], (3,)): - pass + Tensor([1, 2], (3,)) except HMathError as error: assert error.status == 2 - assert str(error) -else: - raise AssertionError("shape mismatch must be rejected") ``` -### `hmath.HMathError.status` - - - -Read the native status captured by an exception. +#### See also -#### Type - -`int`. - -#### See Also - -[hmath.HMathError](#hmath.HMathError), [native status definitions](c.md). - -#### Notes - -The value describes the failed native call, not a later operation. Prefer the -status and exception class over exact diagnostic text when classifying failures. +[hmath.add](#hmathadda-b), [hmath.Tensor](#hmathtensorvalues-shapenone). ## Construction ### `hmath.Tensor(values, shape=None)` - - -Own an immutable binary64 tensor copied from flat values. +Copy flat values into a new immutable tensor. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `values` | `iterable of real scalars` | Finite values in logical row-major order. Generators are accepted; nested sequences are not flat values. | -| `shape` | `iterable of int or None; default None` | Dimensions obeying the common shape contract. `None` chooses `(number_of_values,)`. | +| `values` | `iterable of real scalars` | Finite values in logical row-major order; at most 1,048,576 elements. | +| `shape` | `iterable of int or None; default None` | At most eight bounded nonnegative dimensions. None selects (len(values),). | -#### Raises +#### Returns -`ValueError` -: Python rank/dimension validation fails, or more than 1,048,576 values - are consumed. +a new owned tensor. -`TypeError`, `ValueError`, `OverflowError` -: The iterable or an element cannot be converted by the Python/ctypes path. +#### Raises -`RuntimeError` -: Automatic initialization cannot use the runtime. +`ValueError` for Python shape/size +bounds; `HMathError` for mismatched element count, non-finite input, or allocation +failure; ordinary numeric conversion errors may also occur. -`HMathError` -: The shape product exceeds a native budget (3), count differs from the shape - product (2), an element is non-finite (1), or native allocation fails (4). +**Failure conditions by exception:** -#### Attributes +- **`ValueError`** — Python rank/dimension validation fails, or more than 1,048,576 values + are consumed. -`shape` : `tuple[int, ...]` -: Read-only dimensions; see [Tensor.shape](#hmath.Tensor.shape). +- **`TypeError`, `ValueError`, `OverflowError`** — The iterable or an element cannot be converted by the Python/ctypes path. -#### See Also +- **`RuntimeError`** — Automatic initialization cannot use the runtime. -[hmath.Tensor.scalar](#hmath.Tensor.scalar), [hmath.Tensor.matrix](#hmath.Tensor.matrix), [hmath.Tensor.from_numpy](#hmath.Tensor.from_numpy). +- **`HMathError`** — The shape product exceeds a native budget (3), count differs from the shape + product (2), an element is non-finite (1), or native allocation fails (4). #### Notes +`values`: finite numeric iterable in flat row-major order; +`shape`: optional iterable of dimensions, defaulting to `(len(values),)`. +Values are copied, so later changes to the input do not affect the tensor. +At most 1,048,577 entries are consumed before oversized input is rejected. + The constructor consumes at most 1,048,577 values to detect oversized input. It copies values, so later changes to a source list do not affect the tensor. The object owns one handle; use a context manager or `close()`. @@ -352,6 +294,12 @@ for operations requiring equal shapes. #### Examples +```python +with Tensor([1, 2, 3, 4], (2, 2)) as matrix: + assert matrix.shape == (2, 2) + assert matrix.values() == [1., 2., 3., 4.] +``` + ```python from hmath import Tensor @@ -365,52 +313,50 @@ with Tensor([], (0, 3)) as empty: assert empty.values() == [] ``` -### `hmath.Tensor.scalar(value)` +#### See also - +[hmath.Tensor.matrix](#hmathtensormatrixvalues-rows-columns), [hmath.Tensor.scalar](#hmathtensorscalarvalue), [hmath.Tensor.from_numpy](#hmathtensorfrom_numpyarray-copytrue). -Construct a rank-zero tensor from one finite value. +### `hmath.Tensor.scalar(value)` + +Construct a rank-zero tensor. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `value` | `real scalar` | Value converted to binary64; integers do not remain integer-typed storage. | +| `value` | `real scalar` | Single finite value; the resulting shape is (). | #### Returns -`Tensor` -: New owned tensor with shape `()` and exactly one element. +a new owned rank-zero tensor +with shape `()`. #### Raises -`TypeError`, `ValueError`, `OverflowError` -: The value cannot be converted by the constructor. +the constructor's conversion and native errors. -`RuntimeError` -: Automatic initialization fails. +**Failure conditions by exception:** -`HMathError` -: The converted value is non-finite (1), or native allocation fails (4). +- **`TypeError`, `ValueError`, `OverflowError`** — The value cannot be converted by the constructor. -#### See Also +- **`RuntimeError`** — Automatic initialization fails. -[hmath.Tensor](#hmath.Tensor), [hmath.Tensor.shape](#hmath.Tensor.shape). +- **`HMathError`** — The converted value is non-finite (1), or native allocation fails (4). #### Examples ```python -from hmath import Tensor - -with Tensor.scalar(7) as scalar, Tensor([7]) as vector: +with Tensor.scalar(7) as scalar: assert scalar.shape == () - assert vector.shape == (1,) - assert scalar.values() == vector.values() == [7.0] + assert scalar.values() == [7.0] ``` -### `hmath.Tensor.matrix(values, rows, columns)` +#### See also + +[hmath.Tensor](#hmathtensorvalues-shapenone), [hmath.Tensor.matrix](#hmathtensormatrixvalues-rows-columns). - +### `hmath.Tensor.matrix(values, rows, columns)` Construct a rank-two matrix from flat row-major values. @@ -418,94 +364,92 @@ Construct a rank-two matrix from flat row-major values. | Name | Type / default | Description | | --- | --- | --- | -| `values` | `iterable of real scalars` | Exactly `rows * columns` finite values in row-major order. | -| `rows` | `int` | Built-in Python integer in `[0, 1_048_576]`; booleans are rejected. | -| `columns` | `int` | Built-in Python integer in `[0, 1_048_576]`; booleans are rejected. | +| `values` | `iterable of real scalars` | Exactly rows * columns finite values. | +| `rows` | `int` | Nonnegative row count, at most 1,048,576; bool is rejected. | +| `columns` | `int` | Nonnegative column count, at most 1,048,576; bool is rejected. | #### Returns -`Tensor` -: New owned tensor with shape `(rows, columns)`. +a new owned rank-two tensor. #### Raises -`ValueError` -: Invalid dimensions or an oversized value sequence. +constructor errors. -`TypeError`, `OverflowError` -: Values cannot be converted to the constructor's numeric representation. +**Failure conditions by exception:** -`RuntimeError` -: Automatic initialization fails. +- **`ValueError`** — Invalid dimensions or an oversized value sequence. -`HMathError` -: Count mismatch (2), native layout budget (3), non-finite values (1), - or allocation failure (4). Conversion hooks may also propagate `ValueError`. +- **`TypeError`, `OverflowError`** — Values cannot be converted to the constructor's numeric representation. -#### See Also +- **`RuntimeError`** — Automatic initialization fails. -[hmath.Tensor](#hmath.Tensor), [matrix multiplication](#hmath.Tensor.__matmul__). +- **`HMathError`** — Count mismatch (2), native layout budget (3), non-finite values (1), + or allocation failure (4). Conversion hooks may also propagate `ValueError`. #### Notes +flat row-major values and two nonnegative integer dimensions; +exactly `rows * columns` values are required. + Equivalent to `Tensor(values, (rows, columns))`. Zero rows or zero columns require an empty value sequence. This constructor does not infer dimensions. #### Examples ```python -from hmath import Tensor - with Tensor.matrix([1, 2, 3, 4, 5, 6], 2, 3) as matrix: assert matrix.shape == (2, 3) - assert matrix.values() == [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] ``` -### `hmath.Tensor.from_numpy(array, copy=True)` +#### See also - +[hmath.Tensor](#hmathtensorvalues-shapenone), [hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b). -Create a tensor by copying or borrowing NumPy-compatible data. +### `hmath.Tensor.from_numpy(array, copy=True)` + +Convert an array to a tensor, copying or borrowing its storage. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `array` | `numpy.ndarray or array-like` | Input normalized with `numpy.asarray`; shape is preserved. | -| `copy` | `bool; default True` | `True` converts to float64 and copies. `False` borrows only eligible read-only storage; there is no copy fallback. | +| `array` | `numpy.ndarray or array-like` | Input whose shape is retained; borrowing requirements are below. | +| `copy` | `bool; default True` | True converts to float64 and copies; False borrows eligible read-only storage. | #### Returns -`Tensor` -: New owned handle with the normalized array's shape. In borrowing mode the - handle retains the normalized NumPy array as its storage owner. +an owned tensor preserving shape. #### Raises -`ImportError` -: NumPy is not installed. +`ImportError` without +NumPy, `ValueError` for borrowing/rank/size violations, or `HMathError` for native +storage validation, non-finite data, or allocation failures. + +**Failure conditions by exception:** + +- **`ImportError`** — NumPy is not installed. -`ValueError` -: Rank/element or Python dimension bounds fail; borrowing receives a writable, +- **`ValueError`** — Rank/element or Python dimension bounds fail; borrowing receives a writable, misaligned, non-native-endian, or non-float64 array; or strides are negative or not whole multiples of eight bytes. NumPy conversion may also raise it. -`TypeError`, `OverflowError` -: NumPy/Python cannot convert the input. +- **`TypeError`, `OverflowError`** — NumPy/Python cannot convert the input. -`RuntimeError` -: Automatic initialization fails. +- **`RuntimeError`** — Automatic initialization fails. -`HMathError` -: Native layout/span budgets (3 or 8), non-finite reachable elements (1), +- **`HMathError`** — Native layout/span budgets (3 or 8), non-finite reachable elements (1), or native allocation failure (4). -#### See Also - -[hmath.Tensor.numpy](#hmath.Tensor.numpy), [hmath.Tensor.reshape](#hmath.Tensor.reshape), [NumPy guide](../guides/numpy.md). - #### Notes +`array`: NumPy array or array-like input; `copy`: whether to copy. +With `True`, convert to `float64` and copy in row-major order. With `False`, +borrow aligned, native-endian, **read-only** `float64` storage with nonnegative +strides divisible by eight bytes. The tensor keeps the array alive. The caller +must prevent writes through every alias until all borrowed tensors/views die. + With `copy=True`, NumPy converts to `float64`, the wrapper flattens in C order, and HMath copies the elements. Input strides need not be contiguous and the source may be writable. The resulting Tensor has independent contiguous storage. @@ -530,6 +474,14 @@ immutability cannot be coordinated. #### Examples +```python +import numpy as np +array = np.array([[1., 2.], [3., 4.]]) +array.setflags(write=False) +with Tensor.from_numpy(array, copy=False) as tensor: + assert tensor.sum() == 10.0 +``` + ```python import numpy as np from hmath import Tensor @@ -563,29 +515,35 @@ with Tensor.from_numpy(source) as copied: assert copied.values()[0] == 0.0 ``` +#### See also + +[hmath.Tensor.numpy](#hmathtensornumpycopytrue), [hmath.Tensor](#hmathtensorvalues-shapenone), [hmath.Tensor.close](#hmathtensorclose). + ## Inspection and conversion ### `hmath.Tensor.shape` - +Read the tensor’s dimensions. -Read the dimensions of a live tensor. +#### Parameters -#### Type +None. + +#### Returns -`tuple[int, ...]`; this is a read-only property, not a method. +a tuple of dimensions, +including `()` for scalars. #### Raises -`RuntimeError` -: The handle is closed. +`RuntimeError` after close or +`HMathError` if native metadata retrieval fails. -`HMathError` -: Native metadata retrieval fails. +**Failure conditions by exception:** -#### See Also +- **`RuntimeError`** — The handle is closed. -[hmath.Tensor](#hmath.Tensor), [hmath.Tensor.reshape](#hmath.Tensor.reshape). +- **`HMathError`** — Native metadata retrieval fails. #### Notes @@ -595,16 +553,15 @@ empty tensor. Reading the property does not copy the element buffer. #### Examples ```python -from hmath import Tensor - -with Tensor.scalar(2) as scalar, Tensor([], (0, 3)) as empty: - assert scalar.shape == () +with Tensor([], (0, 3)) as empty: assert empty.shape == (0, 3) ``` -### `hmath.Tensor.values()` +#### See also - +[hmath.Tensor.reshape](#hmathtensorreshapeshape), [hmath.Tensor.values](#hmathtensorvalues). + +### `hmath.Tensor.values()` Copy logical elements to a flat Python list. @@ -614,21 +571,18 @@ None. #### Returns -`list[float]` -: Independent values in logical row-major order. Empty tensors produce `[]`; - scalars produce a one-element list. +an independent flat `list[float]` in logical +row-major order, including for strided input; `[]` for empty tensors. #### Raises -`RuntimeError` -: The handle is closed. +closed-handle or native copy errors. -`HMathError` -: Native copy validation fails. +**Failure conditions by exception:** -#### See Also +- **`RuntimeError`** — The handle is closed. -[hmath.Tensor.numpy](#hmath.Tensor.numpy), [hmath.Tensor.shape](#hmath.Tensor.shape). +- **`HMathError`** — Native copy validation fails. #### Notes @@ -639,50 +593,48 @@ O(N) output space; this is not a zero-copy access method. #### Examples ```python -from hmath import Tensor - with Tensor([2, 4]) as tensor: values = tensor.values() - values[0] = 99.0 - assert tensor.values() == [2.0, 4.0] -assert values == [99.0, 4.0] +values[0] = 99 # Independent list remains usable after close. ``` -### `hmath.Tensor.numpy(copy=True)` +#### See also + +[hmath.Tensor.numpy](#hmathtensornumpycopytrue), [hmath.Tensor.shape](#hmathtensorshape). - +### `hmath.Tensor.numpy(copy=True)` -Return a NumPy copy or a read-only view of tensor storage. +Return a NumPy copy or a read-only view. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `copy` | `bool; default True` | `True` returns independent writable storage; `False` returns a read-only view for nonempty storage. | +| `copy` | `bool; default True` | True returns an independent writable copy; False returns a read-only view. | #### Returns -`numpy.ndarray` -: Float64 array with the tensor's logical shape. A non-copying view preserves - the tensor's strides and retains storage independently of this handle. +`numpy.ndarray` of `float64`. #### Raises -`ImportError` -: NumPy is unavailable. +`ImportError` without NumPy, +closed-handle errors, or native metadata/data errors. -`RuntimeError` -: The handle is closed. +**Failure conditions by exception:** -`HMathError` -: Native data, metadata, or copy retrieval fails. +- **`ImportError`** — NumPy is unavailable. -#### See Also +- **`RuntimeError`** — The handle is closed. -[hmath.Tensor.from_numpy](#hmath.Tensor.from_numpy), [hmath.Tensor.values](#hmath.Tensor.values), [hmath.Tensor.close](#hmath.Tensor.close). +- **`HMathError`** — Native data, metadata, or copy retrieval fails. #### Notes +`copy`: `True` for a writable independent copy, `False` for a +read-only native view with the tensor's shape and strides. Views retain storage +and borrowed owners even after this tensor closes. Do not mutate borrowed aliases. + A copy is writable and unaffected by closing the tensor. A view is read-only and retains both native storage and any borrowed NumPy owner. Closing the original Tensor therefore does not invalidate the view or its derived views. @@ -695,6 +647,14 @@ Python/NumPy metadata even when it does not copy elements. #### Examples +```python +with Tensor([1, 2, 3]) as tensor: + view = tensor.numpy(copy=False) +assert view.tolist() == [1., 2., 3.] +assert not view.flags.writeable +del view +``` + ```python import numpy as np from hmath import Tensor @@ -710,13 +670,15 @@ assert not view.flags.writeable del view ``` +#### See also + +[hmath.Tensor.from_numpy](#hmathtensorfrom_numpyarray-copytrue), [hmath.Tensor.values](#hmathtensorvalues), [hmath.Tensor.close](#hmathtensorclose). + ## Calculations ### `hmath.Tensor.sum()` - - -Sum every logical element to one scalar. +Sum all tensor elements to one scalar. #### Parameters @@ -724,21 +686,19 @@ None. #### Returns -`float` -: The sum of all elements, or `0.0` for an empty tensor. +a `float`, or `0.0` for an empty tensor. #### Raises -`RuntimeError` -: A required Tensor handle is closed. +closed-handle errors or native arithmetic/runtime errors, including +non-finite results. -`HMathError` -: Non-finite computed result (3), native scratch/result allocation failure (4), - unavailable runtime (5), or a contained Haskell exception (6). +**Failure conditions by exception:** -#### See Also +- **`RuntimeError`** — A required Tensor handle is closed. -[hmath.Tensor.dot](#hmath.Tensor.dot), [numerical conventions](../explanation/numerical-conventions.md). +- **`HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). #### Notes @@ -760,6 +720,11 @@ universal relative-error or correctly-rounded-result guarantee is made. #### Examples +```python +with Tensor([1, 2, 3]) as tensor: + assert tensor.sum() == 6.0 +``` + ```python import math from hmath import Tensor @@ -772,41 +737,43 @@ with Tensor([0.1, 0.2]) as samples: assert math.isclose(samples.sum(), 0.3, rel_tol=1e-12, abs_tol=1e-15) ``` -### `hmath.Tensor.dot(other)` +#### See also + +[hmath.Tensor.dot](#hmathtensordotother). - +### `hmath.Tensor.dot(other)` -Sum corresponding products of two identically shaped tensors. +Reduce corresponding products of two identically shaped tensors. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `Tensor` | Live primary-interface Tensor with exactly the same shape, including rank. | +| `other` | `Tensor` | Live tensor with exactly the same shape as this tensor. | #### Returns -`float` -: Scalar sum of corresponding products at every rank; `0.0` for matching empty tensors. +a `float`, or `0.0` for matching empty tensors. #### Raises -`RuntimeError` -: A required Tensor handle is closed. +`HMathError` for shape/arithmetic/runtime errors; closed operands +raise `RuntimeError`. -`HMathError` -: Non-finite computed result (3), native scratch/result allocation failure (4), - unavailable runtime (5), or a contained Haskell exception (6). +**Failure conditions by exception:** -`HMathError` with status 2 -: The operand shapes differ, even when their total element counts are equal. +- **`RuntimeError`** — A required Tensor handle is closed. -#### See Also +- **`HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). -[hmath.Tensor.sum](#hmath.Tensor.sum), [matrix multiplication](#hmath.Tensor.__matmul__). +- **`HMathError` with status 2** — The operand shapes differ, even when their total element counts are equal. #### Notes +a live tensor with exactly the same shape. Sum corresponding +products over the entire tensor; this is a scalar reduction at every rank. + For equal shapes and logical flattened elements, the mathematical definition is $$ @@ -823,13 +790,13 @@ forms elementwise products and sums them. Arithmetic work and gathered input storage are O(N), with ordinary binary64 rounding. It does not promise compensated accumulation or exact products. -#### References - -[NumPy dot](https://numpy.org/doc/stable/reference/generated/numpy.dot.html) -documents the different NumPy semantics; it is not a compatibility claim. - #### Examples +```python +with Tensor([1, 2, 3]) as a, Tensor([4, 5, 6]) as b: + assert a.dot(b) == 32.0 +``` + ```python from hmath import HMathError, Tensor @@ -847,9 +814,16 @@ with Tensor([1, 2, 3, 4], (2, 2)) as a: raise AssertionError("equal counts do not imply equal shapes") ``` -### `hmath.Tensor.__add__(other)` — `a + b` +#### References + +[NumPy dot](https://numpy.org/doc/stable/reference/generated/numpy.dot.html) +documents the different NumPy semantics; it is not a compatibility claim. + +#### See also - +[hmath.Tensor.sum](#hmathtensorsum), [hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b). + +### `hmath.Tensor.__add__(other)` — `a + b` Add two tensors element by element. @@ -857,29 +831,24 @@ Add two tensors element by element. | Name | Type / default | Description | | --- | --- | --- | -| `other` | `Tensor` | Live primary-interface Tensor with exactly the same shape. | +| `other` | `Tensor` | Live tensor of the same shape; broadcasting is unsupported. | #### Returns -`Tensor` -: New owned tensor with the same shape and newly allocated result storage. - Neither input is changed. +a new owned tensor of elementwise sums. Inputs are unchanged. #### Raises -`RuntimeError` -: A required Tensor handle is closed. +native shape/allocation/arithmetic/runtime or closed-handle errors. -`HMathError` -: Non-finite computed result (3), native scratch/result allocation failure (4), - unavailable runtime (5), or a contained Haskell exception (6). +**Failure conditions by exception:** -`HMathError` with status 2 -: The shapes do not match. +- **`RuntimeError`** — A required Tensor handle is closed. -#### See Also +- **`HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). -[hmath.add](#hmath.add), [hmath.Tensor.dot](#hmath.Tensor.dot). +- **`HMathError` with status 2** — The shapes do not match. #### Notes @@ -891,52 +860,51 @@ with additional O(N) scratch storage in the current native boundary. #### Examples ```python -from hmath import Tensor - with Tensor([1, 2]) as a, Tensor([3, 4]) as b: with a + b as result: - assert result.values() == [4.0, 6.0] - assert a.values() == [1.0, 2.0] + assert result.values() == [4., 6.] ``` -### `hmath.Tensor.__matmul__(other)` — `a @ b` +#### See also + +[hmath.Tensor.dot](#hmathtensordotother), [hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b). - +### `hmath.Tensor.__matmul__(other)` — `a @ b` -Multiply two compatible rank-two matrices. +Multiply two rank-two matrices. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `Tensor` | Live matrix of shape `(k, n)` when this matrix has shape `(m, k)`. | +| `other` | `Tensor` | Live (k, n) matrix when this tensor has shape (m, k). | #### Returns -`Tensor` -: New owned matrix of shape `(m, n)` with independent result storage. +a new owned matrix. #### Raises -`RuntimeError` -: A required Tensor handle is closed. +native rank/inner-dimension, +size/work budget, allocation, arithmetic/runtime, or closed-handle errors. -`HMathError` -: Non-finite computed result (3), native scratch/result allocation failure (4), - unavailable runtime (5), or a contained Haskell exception (6). +**Failure conditions by exception:** -`HMathError` with status 2 -: An operand is not rank two or the inner dimensions differ. +- **`RuntimeError`** — A required Tensor handle is closed. -`HMathError` with status 3 -: Output size or the scalar-operation work budget is exceeded. +- **`HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). -#### See Also +- **`HMathError` with status 2** — An operand is not rank two or the inner dimensions differ. -[hmath.Tensor.matrix](#hmath.Tensor.matrix), [hmath.Tensor.dot](#hmath.Tensor.dot), [matrix calculation guide](../guides/matrix-calculation.md). +- **`HMathError` with status 3** — Output size or the scalar-operation work budget is exceeded. #### Notes +a live rank-two tensor. Shapes `(m, k)` and `(k, n)` produce +`(m, n)`. Work is bounded by **16,777,216** scalar multiply-add steps (`m*k*n`). +This binding does not expose batched multiplication. + The mathematical definition is $$ @@ -959,6 +927,12 @@ BLAS acceleration from other Haskell modules. #### Examples +```python +with Tensor.matrix([1, 2, 3, 4], 2, 2) as a: + with a @ a as squared: + assert squared.values() == [7., 10., 15., 22.] +``` + ```python from hmath import Tensor @@ -973,45 +947,45 @@ with Tensor([], (2, 0)) as a, Tensor([], (0, 3)) as b: assert result.values() == [0.0] * 6 ``` -### `hmath.Tensor.reshape(shape)` +#### See also - +[hmath.Tensor.matrix](#hmathtensormatrixvalues-rows-columns), [hmath.Tensor.dot](#hmathtensordotother). -Create an independently owned handle with a new shape over shared storage. +### `hmath.Tensor.reshape(shape)` + +Create a new shape over the same contiguous storage. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `shape` | `iterable of int` | Built-in nonnegative dimensions with the same total element count; `-1` inference is unsupported. | +| `shape` | `iterable of int` | Nonnegative dimensions with unchanged element count; -1 inference is unsupported. | #### Returns -`Tensor` -: New handle sharing the contiguous element storage, not a new element copy. - Original and result handles can be closed independently. +a new owned handle sharing storage; both handles can close independently. #### Raises -`ValueError` -: The Python shape/rank contract is violated. +`ValueError` for invalid dimensions, `HMathError` for incompatible +count or non-contiguous input, or `RuntimeError` after close. -`TypeError` -: `shape` is not iterable. +**Failure conditions by exception:** -`RuntimeError` -: This handle is closed. +- **`ValueError`** — The Python shape/rank contract is violated. -`HMathError` -: Count mismatch or non-contiguous storage (2), native layout budget (3), - or new-handle allocation failure (4). +- **`TypeError`** — `shape` is not iterable. -#### See Also +- **`RuntimeError`** — This handle is closed. -[hmath.Tensor.shape](#hmath.Tensor.shape), [hmath.Tensor.from_numpy](#hmath.Tensor.from_numpy). +- **`HMathError`** — Count mismatch or non-contiguous storage (2), native layout budget (3), + or new-handle allocation failure (4). #### Notes +new dimensions with unchanged element count. Storage must be +contiguous. Negative dimensions, including `-1` for inference, are unsupported. + No element reordering or copy fallback is performed. Contiguity is checked by the native layout validator: singleton dimensions do not constrain strides, and empty tensors are considered contiguous. A positive-stride borrowed view @@ -1021,6 +995,13 @@ copying all elements. #### Examples +```python +with Tensor([1, 2, 3, 4]) as flat: + reshaped = flat.reshape((2, 2)) +with reshaped: + assert reshaped.shape == (2, 2) +``` + ```python from hmath import Tensor @@ -1047,13 +1028,15 @@ with Tensor.from_numpy(source[::2], copy=False) as strided: raise AssertionError("non-contiguous reshape must be rejected") ``` +#### See also + +[hmath.Tensor.shape](#hmathtensorshape), [hmath.Tensor](#hmathtensorvalues-shapenone). + ## Lifetime protocol ### `hmath.Tensor.close()` - - -Release this Tensor handle deterministically. +Release this tensor handle deterministically. #### Parameters @@ -1061,11 +1044,8 @@ None. #### Returns -`None`; repeated calls are harmless. - -#### See Also - -[hmath.Tensor.__enter__](#hmath.Tensor.__enter__), [hmath.Tensor.__exit__](#hmath.Tensor.__exit__), [hmath.Tensor.__del__](#hmath.Tensor.__del__). +`None`; repeated calls are harmless. Other retained handles/views +remain valid. Using this closed handle raises `RuntimeError`. #### Notes @@ -1076,6 +1056,12 @@ operations, not with arbitrary external writes to borrowed storage. #### Examples +```python +tensor = Tensor([1]) +tensor.close() +tensor.close() +``` + ```python from hmath import Tensor @@ -1090,11 +1076,13 @@ else: raise AssertionError("closed handles must not be used") ``` -### `hmath.Tensor.__enter__()` +#### See also - +[hmath.Tensor.__enter__](#hmathtensor__enter__), [hmath.Tensor.__exit__](#hmathtensor__exit__args), [hmath.Tensor.__del__](#hmathtensor__del__). -Enter a context for this live Tensor handle. +### `hmath.Tensor.__enter__()` + +Enter a context that owns this tensor handle. #### Parameters @@ -1102,17 +1090,21 @@ None. #### Returns -`Tensor` -: The same object, not an additional retained handle. +the same live tensor for a `with` block; +this creates no additional reference. #### Raises -`RuntimeError` -: The handle is already closed. +`RuntimeError` after close. +See the context-manager examples above. + +**Failure conditions by exception:** + +- **`RuntimeError`** — The handle is already closed. -#### See Also +#### See also -[hmath.Tensor.__exit__](#hmath.Tensor.__exit__), [hmath.Tensor.close](#hmath.Tensor.close). +[hmath.Tensor.close](#hmathtensorclose), [hmath.Tensor.__exit__](#hmathtensor__exit__args). #### Notes @@ -1120,50 +1112,30 @@ Use `with tensor:` or `with Tensor(...) as tensor:`. Nested contexts over the same object do not create nested ownership: the inner exit closes that handle. Create an independent retained result when independent lifetimes are needed. -### `hmath.Tensor.__exit__(*args)` - -Close the handle when leaving a context. +### `hmath.Tensor.__exit__(*args)` + +Close the tensor when leaving a context. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `args` | `exception triple` | Python supplies exception type, exception value, and traceback, or three `None` values on normal exit. | +| `args` | `exception triple` | Python supplies (exception type, exception value, traceback); exceptions are not suppressed. | #### Returns -`None`. The original exception is not suppressed. +`None`, closing the tensor on normal or exceptional block exit +without suppressing the exception. See the context-manager examples above. -#### See Also +#### See also -[hmath.Tensor.__enter__](#hmath.Tensor.__enter__), [hmath.Tensor.close](#hmath.Tensor.close). - -#### Examples - -```python -from hmath import Tensor - -tensor = Tensor([1]) -try: - with tensor: - raise ValueError("example failure") -except ValueError: - pass -try: - tensor.sum() -except RuntimeError: - pass -else: - raise AssertionError("exceptional exit must close the handle") -``` +[hmath.Tensor.close](#hmathtensorclose), [hmath.Tensor.__enter__](#hmathtensor__enter__). ### `hmath.Tensor.__del__()` - - -Release the handle as a finalization fallback. +Release the handle as a garbage-collection fallback. #### Parameters @@ -1171,61 +1143,53 @@ None. #### Returns -`None`. +`None`, calling `close()` as a finalization +fallback. Collection timing is unspecified; use `with` for deterministic cleanup. -#### See Also +#### See also -[hmath.Tensor.close](#hmath.Tensor.close), [hmath.Tensor.__enter__](#hmath.Tensor.__enter__). +[hmath.Tensor.close](#hmathtensorclose). #### Notes Calls `close()` as a fallback. Do not rely on collection timing or call this method directly as the usual cleanup API. Use `with` or `close()`. -## Optional cffi interface -Install `hmath-python[cffi]` and import `hmath_cffi`. Import verifies ABI 1 and -initializes the shared runtime. ABI/runtime problems can raise `RuntimeError` -at import; missing cffi raises `ImportError`. -Use only cffi Tensor operands with cffi operations. These objects are not -interchangeable with primary-interface Tensor objects. The mathematical -contracts and native budgets are shared, but Python conversion paths, -exception classes, initialization timing, and handle synchronization differ. -NumPy conversion, `reshape`, `scalar`, and `matrix` are not exposed here. +## Optional cffi interface -The common import alias used below is `hc`. Each example defines it independently. +Install `hmath-python[cffi]` to use `hmath_cffi`. Both wrappers share the native +runtime. Prevent concurrent close/use of a cffi tensor. NumPy conversion, +`reshape`, `scalar`, and `matrix` constructors belong to the primary interface. ### `hmath_cffi.add(a, b)` - - -Add two finite scalars through the cffi binding. +Add two real scalars. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `a` | `real scalar` | First operand, convertible to C `double` by cffi. | -| `b` | `real scalar` | Second operand, convertible to C `double` by cffi. | +| `a` | `real scalar` | First finite value, convertible to C double. | +| `b` | `real scalar` | Second finite value, convertible to C double. | #### Returns -`float` -: Binary64 sum. +their binary64 sum as a +Python float. #### Raises -`TypeError`, `OverflowError` -: cffi cannot convert an operand; conversion hooks can propagate other exceptions. +`hmath_cffi.HMathError` for native failures or ordinary +conversion errors. Import initializes the runtime and checks ABI 1. -`hmath_cffi.HMathError` -: Non-finite input (1), non-finite result (3), unavailable runtime (5), - or contained Haskell exception (6). +**Failure conditions by exception:** -#### See Also +- **`TypeError`, `OverflowError`** — cffi cannot convert an operand; conversion hooks can propagate other exceptions. -[hmath.add](#hmath.add), [hmath_cffi.HMathError](#hmath_cffi.HMathError). +- **`hmath_cffi.HMathError`** — Non-finite input (1), non-finite result (3), unavailable runtime (5), + or contained Haskell exception (6). #### Notes @@ -1236,37 +1200,28 @@ Initialization happens during module import. ```python import hmath_cffi as hc - -assert hc.add(2, 3.5) == 5.5 +assert hc.add(2, 3) == 5.0 ``` -### `hmath_cffi.HMathError(status)` +#### See also - +[hmath.add](#hmathadda-b), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). -Represent a failed native operation through the cffi interface. +### `hmath_cffi.HMathError(status)` + +Represent a failed native checked operation. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `status` | `int` | Native failure code captured by this wrapper. | +| `status` | `int` | Native status code; available as the exception’s status attribute. | -#### Attributes - -`status` : `int` -: Captured code; see [cffi HMathError.status](#hmath_cffi.HMathError.status). - -#### See Also - -[hmath.HMathError](#hmath.HMathError), [hmath_cffi.check](#hmath_cffi.check). - -#### Notes +#### Returns -Inherits from `RuntimeError`, but is not `hmath.HMathError`. Catch the -exception class belonging to the interface being used. The message snapshots -the calling thread's diagnostic; manual construction reads the current native -message rather than performing a new operation. +an exception with `.status` +and a snapshot of the current native error message. Catch this class for cffi +errors; it is a separate class from `hmath.HMathError`. #### Examples @@ -1282,49 +1237,45 @@ else: raise AssertionError("shape mismatch must be rejected") ``` -### `hmath_cffi.HMathError.status` +#### See also - - -Read the native status captured by a cffi exception. +[hmath.HMathError](#hmathhmatherrorstatus), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). -#### Type - -`int`. - -#### See Also +#### Notes -[hmath_cffi.HMathError](#hmath_cffi.HMathError), [native status definitions](c.md). +Inherits from `RuntimeError`, but is not `hmath.HMathError`. Catch the +exception class belonging to the interface being used. The message snapshots +the calling thread's diagnostic; manual construction reads the current native +message rather than performing a new operation. -#### Notes -The same native numeric code definitions apply to both wrappers, even -though their Python exception classes differ. ### `hmath_cffi.check(status)` - - -Raise a cffi exception for a nonzero native status. +Convert a raw native status into a Python exception. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `status` | `int` | Status from the immediately preceding checked native operation. | +| `status` | `int` | Status returned by the immediately preceding checked C call. | #### Returns -`None` for status zero. +`None` for zero. #### Raises -`hmath_cffi.HMathError` -: The supplied status is nonzero; `.status` is the supplied code. +`hmath_cffi.HMathError` for nonzero. Call immediately after the raw +call so its thread-local error message has not been replaced. -#### See Also +**Failure conditions by exception:** -[hmath_cffi.HMathError](#hmath_cffi.HMathError), [hmath_cffi.add](#hmath_cffi.add). +- **`hmath_cffi.HMathError`** — The supplied status is nonzero; `.status` is the supplied code. + +#### See also + +[hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). #### Notes @@ -1333,47 +1284,37 @@ call, invoke it immediately on the same thread so another operation cannot replace the diagnostic. High-level operations already call it; users do not need to wrap `Tensor.sum()` or `add()` in another `check()`. -#### Examples -```python -import hmath_cffi as hc - -assert hc.check(0) is None -``` ### `hmath_cffi.Tensor(values, shape=None)` - - -Own an immutable tensor through the optional cffi binding. +Copy flat values into a new immutable tensor. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `values` | `iterable of real scalars` | Flat finite row-major values; at most 1,048,576 elements. | -| `shape` | `iterable of int or None; default None` | Built-in Python dimensions under the common shape contract. `None` selects `(number_of_values,)`. | +| `values` | `iterable of real scalars` | Finite values in logical row-major order; at most 1,048,576 elements. | +| `shape` | `iterable of int or None; default None` | At most eight bounded nonnegative dimensions. None selects (len(values),). | -#### Raises +#### Returns -`ValueError` -: Rank, Python dimension, or input-count validation fails. +an owned cffi tensor with `.shape` +stored as a tuple. -`TypeError`, `OverflowError` -: cffi cannot convert values; conversion hooks can propagate other exceptions. +#### Raises -`hmath_cffi.HMathError` -: Count mismatch (2), layout budget (3), non-finite data (1), or native - allocation failure (4). +`ValueError` for Python bounds, +`hmath_cffi.HMathError` for native input/allocation errors, or conversion errors. -#### Attributes +**Failure conditions by exception:** -`shape` : `tuple[int, ...]` -: Cached dimensions; see [cffi Tensor.shape](#hmath_cffi.Tensor.shape). +- **`ValueError`** — Rank, Python dimension, or input-count validation fails. -#### See Also +- **`TypeError`, `OverflowError`** — cffi cannot convert values; conversion hooks can propagate other exceptions. -[hmath.Tensor](#hmath.Tensor), [hmath_cffi.Tensor.close](#hmath_cffi.Tensor.close). +- **`hmath_cffi.HMathError`** — Count mismatch (2), layout budget (3), non-finite data (1), or native + allocation failure (4). #### Notes @@ -1385,6 +1326,13 @@ replacement for explicit cleanup. #### Examples +```python +with hc.Tensor([1, 2, 3, 4], (2, 2)) as a: + assert a.shape == (2, 2) + with a @ a as result: + assert result.values() == [7., 10., 15., 22.] +``` + ```python import hmath_cffi as hc @@ -1394,41 +1342,13 @@ with hc.Tensor([1, 2, 3, 4], (2, 2)) as matrix: assert squared.values() == [7.0, 10.0, 15.0, 22.0] ``` -### `hmath_cffi.Tensor.shape` - - - -Read the cached shape of a cffi Tensor. - -#### Type - -`tuple[int, ...]`. - -#### See Also - -[hmath_cffi.Tensor](#hmath_cffi.Tensor), [hmath.Tensor.shape](#hmath.Tensor.shape). - -#### Notes - -Unlike the primary interface's property, this is a stored Python attribute -and remains readable after `close()`. Treat it as read-only: assigning to it -does not reshape native storage and can make Python metadata inconsistent. - -#### Examples - -```python -import hmath_cffi as hc +#### See also -with hc.Tensor([1, 2]) as tensor: - assert tensor.shape == (2,) -assert tensor.shape == (2,) # Cached metadata, not evidence of a live handle. -``` +[hmath.Tensor](#hmathtensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose), [hmath_cffi.check](#hmath_cfficheckstatus). ### `hmath_cffi.Tensor.values()` - - -Copy a cffi Tensor to a flat Python list. +Copy logical elements to a flat Python list. #### Parameters @@ -1436,36 +1356,26 @@ None. #### Returns -`list[float]` -: Independent row-major values; `[]` for an empty tensor. +an independent row-major `list[float]`. #### Raises -`RuntimeError` -: The handle is closed. +`RuntimeError` after close or `hmath_cffi.HMathError` on copy failure. +Use `.values()` inside the context-manager example above. -`hmath_cffi.HMathError` -: Native copy validation fails. +**Failure conditions by exception:** -#### See Also +- **`RuntimeError`** — The handle is closed. -[hmath.Tensor.values](#hmath.Tensor.values), [hmath_cffi.Tensor.close](#hmath_cffi.Tensor.close). +- **`hmath_cffi.HMathError`** — Native copy validation fails. -#### Examples +#### See also -```python -import hmath_cffi as hc - -with hc.Tensor([2, 4]) as tensor: - values = tensor.values() -assert values == [2.0, 4.0] -``` +[hmath.Tensor.values](#hmathtensorvalues), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). ### `hmath_cffi.Tensor.sum()` - - -Sum every element of a cffi Tensor. +Sum all tensor elements to one scalar. #### Parameters @@ -1473,159 +1383,135 @@ None. #### Returns -`float` -: Scalar sum, or `0.0` for an empty tensor. +scalar sum, or `0.0` for an empty tensor. #### Raises -`RuntimeError` -: A required Tensor handle is closed. +native arithmetic/runtime or closed-handle errors. For example, +`hc.Tensor([1, 2])` sums to `3.0`; close the owner after use. -`hmath_cffi.HMathError` -: Non-finite computed result (3), native scratch/result allocation failure (4), +**Failure conditions by exception:** + +- **`RuntimeError`** — A required Tensor handle is closed. + +- **`hmath_cffi.HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4), unavailable runtime (5), or a contained Haskell exception (6). -#### See Also +#### See also -[hmath.Tensor.sum](#hmath.Tensor.sum), [hmath_cffi.Tensor.dot](#hmath_cffi.Tensor.dot). +[hmath.Tensor.sum](#hmathtensorsum), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). #### Notes The whole-tensor reduction, numerical behavior, and native scratch costs are the same as primary `Tensor.sum()`. No axis argument is exposed. -#### Examples -```python -import hmath_cffi as hc - -with hc.Tensor([1, 2, 3]) as tensor, hc.Tensor([]) as empty: - assert tensor.sum() == 6.0 - assert empty.sum() == 0.0 -``` ### `hmath_cffi.Tensor.dot(other)` - - -Sum corresponding products of identically shaped cffi tensors. +Reduce corresponding products of two identically shaped tensors. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `hmath_cffi.Tensor` | Live cffi Tensor with exactly the same shape. | +| `other` | `Tensor` | Live tensor with exactly the same shape as this tensor. | #### Returns -`float` -: Sum of corresponding products; `0.0` for matching empty tensors. +scalar sum +of elementwise products. #### Raises -`RuntimeError` -: A required Tensor handle is closed. +native shape/arithmetic/runtime or +closed-handle errors. The primary interface's dot example also applies to cffi. -`hmath_cffi.HMathError` -: Non-finite computed result (3), native scratch/result allocation failure (4), +**Failure conditions by exception:** + +- **`RuntimeError`** — A required Tensor handle is closed. + +- **`hmath_cffi.HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4), unavailable runtime (5), or a contained Haskell exception (6). -`hmath_cffi.HMathError` with status 2 -: Shapes differ. +- **`hmath_cffi.HMathError` with status 2** — Shapes differ. -#### See Also +#### See also -[hmath.Tensor.dot](#hmath.Tensor.dot), [hmath_cffi.Tensor.__matmul__](#hmath_cffi.Tensor.__matmul__). +[hmath.Tensor.dot](#hmathtensordotother), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). #### Notes This is the same whole-tensor scalar reduction as primary `dot`, not matrix multiplication. See that entry for the equation and numerical behavior. -#### Examples - -```python -import hmath_cffi as hc -with hc.Tensor([1, 2, 3]) as a, hc.Tensor([4, 5, 6]) as b: - assert a.dot(b) == 32.0 -``` ### `hmath_cffi.Tensor.__add__(other)` — `a + b` - - -Add two cffi tensors element by element. +Add two tensors element by element. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `hmath_cffi.Tensor` | Live cffi Tensor with exactly the same shape; no broadcasting. | +| `other` | `Tensor` | Live tensor of the same shape; broadcasting is unsupported. | #### Returns -`hmath_cffi.Tensor` -: New owned result with independent storage and unchanged input tensors. +a new owned +elementwise sum without broadcasting. #### Raises -`RuntimeError` -: A required Tensor handle is closed. +native shape/allocation/ +arithmetic/runtime or closed-handle errors. Use with `with a + b as result:`. -`hmath_cffi.HMathError` -: Non-finite computed result (3), native scratch/result allocation failure (4), - unavailable runtime (5), or a contained Haskell exception (6). +**Failure conditions by exception:** -`hmath_cffi.HMathError` with status 2 -: Shapes differ. +- **`RuntimeError`** — A required Tensor handle is closed. -#### See Also +- **`hmath_cffi.HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4), + unavailable runtime (5), or a contained Haskell exception (6). -[hmath.Tensor.__add__](#hmath.Tensor.__add__), [hmath_cffi.add](#hmath_cffi.add). +- **`hmath_cffi.HMathError` with status 2** — Shapes differ. -#### Examples +#### See also -```python -import hmath_cffi as hc - -with hc.Tensor([1, 2]) as a, hc.Tensor([3, 4]) as b: - with a + b as result: - assert result.values() == [4.0, 6.0] -``` +[hmath.Tensor.__add__](#hmathtensor__add__other-a-b), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). ### `hmath_cffi.Tensor.__matmul__(other)` — `a @ b` - - -Multiply two compatible rank-two cffi matrices. +Multiply two rank-two matrices. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `other` | `hmath_cffi.Tensor` | Live shape `(k, n)` matrix when this matrix has shape `(m, k)`. | +| `other` | `Tensor` | Live (k, n) matrix when this tensor has shape (m, k). | #### Returns -`hmath_cffi.Tensor` -: New owned matrix with shape `(m, n)` and independent storage. +a new owned matrix. #### Raises -`RuntimeError` -: A required Tensor handle is closed. +the same shape/work-bound and +native/lifecycle errors as primary `@`. See the executable cffi example above. + +**Failure conditions by exception:** + +- **`RuntimeError`** — A required Tensor handle is closed. -`hmath_cffi.HMathError` -: Non-finite computed result (3), native scratch/result allocation failure (4), +- **`hmath_cffi.HMathError`** — Non-finite computed result (3), native scratch/result allocation failure (4), unavailable runtime (5), or a contained Haskell exception (6). -`hmath_cffi.HMathError` -: Rank/inner-dimension mismatch (2), or output/work budget exceeded (3). +- **`hmath_cffi.HMathError`** — Rank/inner-dimension mismatch (2), or output/work budget exceeded (3). -#### See Also +#### See also -[hmath.Tensor.__matmul__](#hmath.Tensor.__matmul__), [hmath_cffi.Tensor.dot](#hmath_cffi.Tensor.dot). +[hmath.Tensor.__matmul__](#hmathtensor__matmul__other-a-b), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). #### Notes @@ -1633,22 +1519,11 @@ The primary matrix-product definition and budgets apply: at most 1,048,576 output elements and 16,777,216 scalar multiply-add steps. There is no batching or vector promotion. -#### Examples - -```python -import hmath_cffi as hc -with hc.Tensor([1, 2, 3, 4], (2, 2)) as a: - with a @ a as result: - assert result.shape == (2, 2) - assert result.values() == [7.0, 10.0, 15.0, 22.0] -``` ### `hmath_cffi.Tensor.close()` - - -Release this cffi Tensor handle. +Release this tensor handle deterministically. #### Parameters @@ -1656,11 +1531,13 @@ None. #### Returns -`None`. Repeated calls are harmless. +`None`, releasing its cffi-managed reference. +Repeated calls are harmless. Do not close concurrently with an operation on +this tensor. Other independent owners remain valid. -#### See Also +#### See also -[hmath_cffi.Tensor.__enter__](#hmath_cffi.Tensor.__enter__), [hmath_cffi.Tensor.shape](#hmath_cffi.Tensor.shape). +[hmath.Tensor.close](#hmathtensorclose), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.check](#hmath_cfficheckstatus). #### Notes @@ -1668,21 +1545,11 @@ Do not call concurrently with an operation using this handle. Numerical operations and `values()` fail after close; cached `.shape` remains readable. The shared runtime is not shut down. -#### Examples -```python -import hmath_cffi as hc - -tensor = hc.Tensor([1]) -tensor.close() -tensor.close() -``` ### `hmath_cffi.Tensor.__enter__()` - - -Enter a context for a live cffi Tensor. +Enter a context that owns this tensor handle. #### Parameters @@ -1690,40 +1557,126 @@ None. #### Returns -`hmath_cffi.Tensor` -: The same object, without an extra native reference. +the same live tensor in `with`. #### Raises -`RuntimeError` -: The handle is closed. +`RuntimeError` after close. No new native owner is created. + +**Failure conditions by exception:** -#### See Also +- **`RuntimeError`** — The handle is closed. -[hmath_cffi.Tensor.__exit__](#hmath_cffi.Tensor.__exit__), [hmath_cffi.Tensor.close](#hmath_cffi.Tensor.close). +#### See also + +[hmath.Tensor.__enter__](#hmathtensor__enter__), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). #### Notes As with the primary interface, nested contexts on the same object do not create independent handle lifetimes. -### `hmath_cffi.Tensor.__exit__(*args)` - -Release a cffi Tensor on normal or exceptional context exit. +### `hmath_cffi.Tensor.__exit__(*args)` + +Close the tensor when leaving a context. #### Parameters | Name | Type / default | Description | | --- | --- | --- | -| `args` | `exception triple` | Exception type, value, and traceback supplied by Python; all are `None` on normal exit. | +| `args` | `exception triple` | Python supplies (exception type, exception value, traceback); exceptions are not suppressed. | #### Returns -`None`. Exceptions are not suppressed. +`None`, closing this +tensor on block exit without suppressing exceptions. + +#### See also + +[hmath.Tensor.__exit__](#hmathtensor__exit__args), [hmath_cffi.Tensor](#hmath_cffitensorvalues-shapenone), [hmath_cffi.Tensor.close](#hmath_cffitensorclose). + +## Public exception and cffi attributes + +The following public attributes are also available on the Python objects. + +### `hmath.HMathError.status` + + + +Read the native status captured by an exception. + +#### Type + +`int`. + +#### See also + +[hmath.HMathError](#hmath.HMathError), [native status definitions](c.md). + +#### Notes + +The value describes the failed native call, not a later operation. Prefer the +status and exception class over exact diagnostic text when classifying failures. + +### `hmath_cffi.HMathError.status` + + + +Read the native status captured by a cffi exception. + +#### Type + +`int`. + +#### See also + +[hmath_cffi.HMathError](#hmath_cffi.HMathError), [native status definitions](c.md). + +#### Notes + +The same native numeric code definitions apply to both wrappers, even +though their Python exception classes differ. + +### `hmath_cffi.Tensor.shape` + + + +Read the cached shape of a cffi Tensor. + +#### Type + +`tuple[int, ...]`. + +#### See also + +[hmath_cffi.Tensor](#hmath_cffi.Tensor), [hmath.Tensor.shape](#hmath.Tensor.shape). + +#### Notes + +Unlike the primary interface's property, this is a stored Python attribute +and remains readable after `close()`. Treat it as read-only: assigning to it +does not reshape native storage and can make Python metadata inconsistent. + +#### Examples + +```python +import hmath_cffi as hc + +with hc.Tensor([1, 2]) as tensor: + assert tensor.shape == (2,) +assert tensor.shape == (2,) # Cached metadata, not evidence of a live handle. +``` + +## Low-level cffi attributes -#### See Also +`hmath_cffi.ffi` is the configured `cffi.FFI` object; `hmath_cffi.lib` is the +loaded native handle exposing the declarations used by this wrapper. Raw calls +follow the [C ABI](c.md), including status and ownership rules. `tensor.ptr` is +borrowed: do not release it separately or use it after close. `tensor.shape` +is the construction/result shape tuple and must not be reassigned by callers. -[hmath_cffi.Tensor.__enter__](#hmath_cffi.Tensor.__enter__), [hmath_cffi.Tensor.close](#hmath_cffi.Tensor.close). +## Native status codes +See the canonical [C status table](c.md#native-status-codes). From bb3ad5e5fcad586bff399d1ffd8c3446369cd48d Mon Sep 17 00:00:00 2001 From: LunaStev Date: Thu, 29 Oct 2026 14:59:56 +0900 Subject: [PATCH 4/4] docs: add stable targets for expanded Python reference links Retain existing headings and source-derived signatures; add Markdown-only symbol anchors so reference and NumPy-guide links resolve under MkDocs. --- docs/reference/python.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/reference/python.md b/docs/reference/python.md index 9af1257..0ec29e8 100644 --- a/docs/reference/python.md +++ b/docs/reference/python.md @@ -206,6 +206,8 @@ shutdown() ### `hmath.HMathError(status)` + + Represent a failed native checked operation. #### Parameters @@ -523,6 +525,8 @@ with Tensor.from_numpy(source) as copied: ### `hmath.Tensor.shape` + + Read the tensor’s dimensions. #### Parameters @@ -953,6 +957,8 @@ with Tensor([], (2, 0)) as a, Tensor([], (0, 3)) as b: ### `hmath.Tensor.reshape(shape)` + + Create a new shape over the same contiguous storage. #### Parameters @@ -1209,6 +1215,8 @@ assert hc.add(2, 3) == 5.0 ### `hmath_cffi.HMathError(status)` + + Represent a failed native checked operation. #### Parameters @@ -1288,6 +1296,8 @@ need to wrap `Tensor.sum()` or `add()` in another `check()`. ### `hmath_cffi.Tensor(values, shape=None)` + + Copy flat values into a new immutable tensor. #### Parameters