diff --git a/crates/pyxlog/python/pyxlog/__init__.pyi b/crates/pyxlog/python/pyxlog/__init__.pyi index 772a7388c..d241af90c 100644 --- a/crates/pyxlog/python/pyxlog/__init__.pyi +++ b/crates/pyxlog/python/pyxlog/__init__.pyi @@ -48,6 +48,8 @@ from pyxlog._native import ( # DLPack / Arrow utilities dlpack_roundtrip as dlpack_roundtrip, dlpack_is_cuda as dlpack_is_cuda, + intern_symbols as intern_symbols, + resolve_symbols as resolve_symbols, ) # Arrow imports are feature-gated; expose them for type checkers but they may diff --git a/crates/pyxlog/python/pyxlog/_native.pyi b/crates/pyxlog/python/pyxlog/_native.pyi index d2a09c34d..403578aa9 100644 --- a/crates/pyxlog/python/pyxlog/_native.pyi +++ b/crates/pyxlog/python/pyxlog/_native.pyi @@ -1538,6 +1538,14 @@ def dlpack_is_cuda(tensor: Any) -> bool: """Return True when a DLPack capsule is backed by CUDA memory.""" ... +def intern_symbols(symbols: list[str]) -> list[int]: + """Intern strings in XLOG's canonical registry and return their IDs.""" + ... + +def resolve_symbols(symbol_ids: list[int]) -> list[str]: + """Resolve canonical symbol IDs, rejecting any unknown identifier.""" + ... + # The following two functions are only present when pyxlog is compiled with # ``--features arrow-device-import``. They are included here unconditionally # so that type checkers can reference them; at runtime they may be absent. diff --git a/crates/pyxlog/src/lib.rs b/crates/pyxlog/src/lib.rs index 52ec3343d..9cdf615ff 100644 --- a/crates/pyxlog/src/lib.rs +++ b/crates/pyxlog/src/lib.rs @@ -356,6 +356,27 @@ fn dlpack_is_cuda(obj: &Bound<'_, PyAny>) -> PyResult { Ok(managed.dl_tensor.device.device_type == xlog_cuda::dlpack::K_DLCUDA) } +#[pyfunction] +fn intern_symbols(symbols: Vec) -> Vec { + symbols + .iter() + .map(|symbol| xlog_core::symbol::intern(symbol)) + .collect() +} + +#[pyfunction] +fn resolve_symbols(symbol_ids: Vec) -> PyResult> { + symbol_ids + .into_iter() + .enumerate() + .map(|(index, symbol_id)| { + xlog_core::symbol::resolve_checked(symbol_id).ok_or_else(|| { + PyValueError::new_err(format!("unknown symbol ID {symbol_id} at index {index}")) + }) + }) + .collect() +} + #[pyclass(name = "DifferentiableProofTraceMap")] pub struct PyDifferentiableProofTraceMap { inner: xlog_logic::DifferentiableProofTraceMap, @@ -856,6 +877,8 @@ fn pyxlog(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(training::train_model_tensor, m)?)?; m.add_function(wrap_pyfunction!(dlpack::dlpack_roundtrip, m)?)?; m.add_function(wrap_pyfunction!(dlpack_is_cuda, m)?)?; + m.add_function(wrap_pyfunction!(intern_symbols, m)?)?; + m.add_function(wrap_pyfunction!(resolve_symbols, m)?)?; #[cfg(feature = "arrow-device-import")] m.add_function(wrap_pyfunction!(dlpack::export_arrow_device, m)?)?; #[cfg(feature = "arrow-device-import")] diff --git a/python/tests/test_pyxlog_symbol_registry.py b/python/tests/test_pyxlog_symbol_registry.py new file mode 100644 index 000000000..ae0624047 --- /dev/null +++ b/python/tests/test_pyxlog_symbol_registry.py @@ -0,0 +1,24 @@ +"""Public Python bindings for XLOG's canonical symbol registry.""" + +import pytest + +pyxlog = pytest.importorskip("pyxlog") + + +def test_symbol_batches_round_trip_through_the_canonical_registry() -> None: + symbols = ["authority:maran", "case:milk-meat", "authority:maran", ""] + + symbol_ids = pyxlog.intern_symbols(symbols) + + assert len(symbol_ids) == len(symbols) + assert symbol_ids[0] == symbol_ids[2] + assert symbol_ids[0] != symbol_ids[1] + assert pyxlog.resolve_symbols(symbol_ids) == symbols + + +def test_resolve_symbols_rejects_unknown_identifiers() -> None: + with pytest.raises( + ValueError, + match=r"^unknown symbol ID 4294967295 at index 1$", + ): + pyxlog.resolve_symbols([pyxlog.intern_symbols(["known"])[0], 2**32 - 1])