Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/pyxlog/python/pyxlog/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions crates/pyxlog/python/pyxlog/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions crates/pyxlog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,27 @@ fn dlpack_is_cuda(obj: &Bound<'_, PyAny>) -> PyResult<bool> {
Ok(managed.dl_tensor.device.device_type == xlog_cuda::dlpack::K_DLCUDA)
}

#[pyfunction]
fn intern_symbols(symbols: Vec<String>) -> Vec<u32> {
symbols
.iter()
.map(|symbol| xlog_core::symbol::intern(symbol))
.collect()
}

#[pyfunction]
fn resolve_symbols(symbol_ids: Vec<u32>) -> PyResult<Vec<String>> {
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,
Expand Down Expand Up @@ -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")]
Expand Down
24 changes: 24 additions & 0 deletions python/tests/test_pyxlog_symbol_registry.py
Original file line number Diff line number Diff line change
@@ -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])
Loading