Binding architecture
xtalforge has one Rust engine and four surfaces. Data crosses a boundary in one of two shapes:
- Plain data for small values: cell parameters, options, results and errors. These are
serde-serializable structs or primitive arrays. - Opaque handles for large objects:
Structure,Document, trajectories and grids are created, queried and dropped by handle; the data is not copied on every call.
The Rust API is the source of truth. The CLI, Python and WASM bindings call the same functions; no algorithm is reimplemented in a binding.
Per-surface mapping
| Concern | Rust | CLI | Python | WASM |
|---|---|---|---|---|
| Large object | struct | path or stdin | opaque pyclass | opaque class |
| Coordinates | [f64; 3], Vec<[f64; 3]> | JSON / text | NumPy array | Float64Array |
| Options | typed struct | flags | keyword arguments | object |
| Success | Result | stdout / JSON | return value | return value |
| Input error | Error::InvalidInput | exit 3 | InputError | Error with code |
| Usage error | n/a | exit 2 (clap) | TypeError | TypeError |
| Any other error (computation, limit, engine, data) | typed variant | exit 3 | XtalforgeError | Error with code |
The CLI uses exit 3 for every library error and exit 2 for a clap usage error; the specific cause is the stable code string in the message or in the --json object. Python raises InputError for InvalidInput and the base XtalforgeError for every other variant, with (code, message) as arguments, so callers branch on exc.args[0]. Dedicated limit and engine exception classes and distinct exit codes are not part of the 1.0 surface.
Error contract
Every crate has one Error enum with a stable code() string and a message() string. Codes are snake_case and part of the public API. The initial core codes are:
| Code | Meaning |
|---|---|
invalid_input | a value or geometry violates the preconditions |
data_compression | a gzip bundle is corrupt |
data_checksum | a bundle failed its SHA-256 check |
data_digest | a bundle manifest digest is malformed |
data_json | a bundle payload is not valid JSON for its type |
limit_exceeded | a size check refused the operation |
symmetry | the symmetry backend failed |
The Python module exposes XtalforgeError as the base class and InputError as the first domain subclass. The exception arguments are (code, message). The WASM module raises a JavaScript Error whose code property is the same string. The CLI prints { "error": { "code", "message" } } with --json and exits with the code in the table.
Handles and lifetimes
- Python: pyo3 classes own the Rust value; methods borrow it for the duration of the call. No Rust reference escapes to Python.
- WASM: wasm-bindgen classes own the value; the JavaScript object must be dropped explicitly or by scope. No pointer is exposed.
- CLI: one process, one document; the file is read at the start and written at the end, with a temporary file and an atomic rename.
WASM builds
wasm-pack builds the package for web, nodejs and bundler. The default package has no threads. A threaded build is reserved for a later task and will use wasm-bindgen-rayon; it requires cross-origin isolation and must stay opt-in. console_error_panic_hook is installed only in debug builds, so the release .wasm does not carry the hook.
Versioning
All four surfaces share the workspace version. scripts/sync-version.sh --check fails on drift. The Python package uses a dynamic version from Cargo; the WASM package.json is generated by wasm-pack and checked by the same script.
Testing
tests/vectors/ contains shared JSON vectors. A capability is not at parity until the Rust tests, the CLI runner, pytest and the WASM node tests execute the same vectors. scripts/check-parity.sh compares docs/api/parity.md with the symbols each surface exports and with the API pages.