diff --git a/CHANGELOG.md b/CHANGELOG.md index fb816cc..64d4d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Functions cannot be `async`. * Functions cannot have generic parameters. * Functions cannot be variadic. +- `ocaml-interop-inspect` crate to inspect runtime OCaml values. ## [0.12.0] - 2025-05-16 diff --git a/Cargo.toml b/Cargo.toml index dfa39ed..8e7856b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,8 @@ members = [ ".", "derive", "dune-builder", + "inspect", + "inspect/examples/inspect_runtime_example", "testing/rust-caller", "testing/ocaml-caller/rust", "docs/examples/tuples/rust", diff --git a/Makefile b/Makefile index b420cb6..8f7d322 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ test: cargo test @echo "Running derive crate tests..." cargo test -p ocaml-interop-derive + @echo "Running OCaml interop inspect tests..." + cargo test -p ocaml-interop-inspect test-examples: @echo "Running rust-caller tests..." @@ -23,6 +25,9 @@ test-examples: @echo "Running Noalloc Primitives example (docs/examples/noalloc_primitives)..." cd docs/examples/noalloc_primitives; opam exec -- dune test -f @echo "--- Finished Documentation Examples ---" + @echo "Running OCaml interop inspect examples..." + cargo test -p ocaml-interop-inspect --example basic_usage + cd inspect/examples/inspect_runtime_example; cargo run test-all: test test-examples @echo "All tests (crate and examples) completed." diff --git a/inspect/Cargo.toml b/inspect/Cargo.toml new file mode 100644 index 0000000..bca958c --- /dev/null +++ b/inspect/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ocaml-interop-inspect" +version = "0.12.0" +authors = ["Bruno Deferrari "] +license = "MIT" +description = "Runtime value inspection utilities for OCaml interop debugging" +homepage = "https://github.com/tizoc/ocaml-interop" +repository = "https://github.com/tizoc/ocaml-interop" +keywords = ["ocaml", "rust", "ffi", "interop", "debug"] +edition = "2021" + +[dependencies] +ocaml-sys = { version = "0.26", features = ["ocaml5"] } + +[dev-dependencies] +ocaml-interop = { path = ".." } + +[[example]] +name = "basic_usage" +path = "examples/basic_usage.rs" + +[[example]] +name = "inspect_runtime_example" +path = "examples/inspect_runtime_example/src/main.rs" +required-features = ["inspect-runtime-example"] + +[features] +default = [] +inspect-runtime-example = [] diff --git a/inspect/examples/basic_usage.rs b/inspect/examples/basic_usage.rs new file mode 100644 index 0000000..bed6d53 --- /dev/null +++ b/inspect/examples/basic_usage.rs @@ -0,0 +1,140 @@ +// Copyright (c) Viable Systems and TezEdge Contributors +// SPDX-License-Identifier: MIT + +//! Basic usage examples of the OCaml value inspector. + +#[cfg(test)] +use ocaml_interop::*; +use ocaml_interop_inspect::inspect_raw_value; + +// Example function that demonstrates how to use the inspector for debugging +#[cfg(test)] +fn debug_ocaml_conversion(cr: &mut OCamlRuntime, value: OCaml<'_, T>, description: &str) { + println!("=== Debugging OCaml value: {} ===", description); + + // Get the raw OCaml value and inspect it + let inspection = unsafe { inspect_raw_value(value.raw()) }; + + // Show the full detailed representation + println!("Full details: {}", inspection); + + // Show the compact representation for quick viewing + println!("Compact: {}", inspection.compact()); + + // Show the Debug representation with extra details + println!("Debug: {:?}", inspection); + + // Check the type + println!("Type: {}", inspection.repr().type_name()); + + println!(); +} + +// Example function showing how to use the inspector in error handling +#[cfg(test)] +fn safe_conversion_with_debugging( + cr: &mut OCamlRuntime, + value: OCaml<'_, T>, +) -> Result +where + R: FromOCaml, +{ + // First, inspect the value to understand its structure + let inspection = unsafe { inspect_raw_value(value.raw()) }; + + // Try the conversion + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| value.to_rust::())) { + Ok(result) => { + println!("✓ Conversion successful for: {}", inspection.compact()); + Ok(result) + } + Err(_) => { + let error_msg = format!( + "✗ Conversion failed for value with structure: {}\nFull details: {}", + inspection.compact(), + inspection + ); + Err(error_msg) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn example_inspect_integer() { + // This example shows how to inspect OCaml integers + let inspection = unsafe { + inspect_raw_value((42_isize << 1) | 1) // OCaml integer encoding + }; + + assert!(inspection.repr().is_immediate()); + assert!(inspection.compact().contains("integer 42")); + println!("Integer inspection: {}", inspection); + } + + #[test] + fn example_inspect_unit() { + let inspection = unsafe { inspect_raw_value(ocaml_sys::UNIT) }; + + assert!(inspection.repr().is_immediate()); + assert!(inspection.compact().contains("unit")); + println!("Unit inspection: {}", inspection); + } + + #[test] + fn example_inspect_boolean() { + let true_inspection = unsafe { inspect_raw_value(ocaml_sys::TRUE) }; + + let false_inspection = unsafe { inspect_raw_value(ocaml_sys::FALSE) }; + + assert!(true_inspection.repr().is_immediate()); + assert!(false_inspection.repr().is_immediate()); + assert!(true_inspection.compact().contains("true")); + assert!(false_inspection.compact().contains("false")); + + println!("True inspection: {}", true_inspection); + println!("False inspection: {}", false_inspection); + } +} + +fn main() { + println!("OCaml Value Inspector Examples"); + println!("=============================="); + + // Examples of inspecting different types of OCaml values + + // Integer inspection + println!("1. Integer values:"); + let int_inspection = unsafe { inspect_raw_value((123_isize << 1) | 1) }; + println!(" {}", int_inspection); + println!(" Compact: {}", int_inspection.compact()); + + // Boolean inspection + println!("\n2. Boolean values:"); + let true_inspection = unsafe { inspect_raw_value(ocaml_sys::TRUE) }; + let false_inspection = unsafe { inspect_raw_value(ocaml_sys::FALSE) }; + println!(" True: {}", true_inspection); + println!(" False: {}", false_inspection); + + // Unit inspection + println!("\n3. Unit value:"); + let unit_inspection = unsafe { inspect_raw_value(ocaml_sys::UNIT) }; + println!(" {}", unit_inspection); + + // Empty list inspection + println!("\n4. Empty list:"); + let empty_list_inspection = unsafe { inspect_raw_value(ocaml_sys::EMPTY_LIST) }; + println!(" {}", empty_list_inspection); + + // None option inspection + println!("\n5. None option:"); + let none_inspection = unsafe { inspect_raw_value(ocaml_sys::NONE) }; + println!(" {}", none_inspection); + + println!("\nFor block values (tuples, records, variants), you'll need to"); + println!("create them through the OCaml runtime within a proper context."); + println!("This example shows the basic immediate value inspection capabilities."); +} diff --git a/inspect/examples/inspect_runtime_example/Cargo.toml b/inspect/examples/inspect_runtime_example/Cargo.toml new file mode 100644 index 0000000..8b1a574 --- /dev/null +++ b/inspect/examples/inspect_runtime_example/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "inspect-runtime-example" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +ocaml-interop = { path = "../../.." } +ocaml-interop-inspect = { path = "../.." } + +[build-dependencies] +cc = "1" +ocaml-interop-dune-builder = { path = "../../../dune-builder" } diff --git a/inspect/examples/inspect_runtime_example/build.rs b/inspect/examples/inspect_runtime_example/build.rs new file mode 100644 index 0000000..87683a2 --- /dev/null +++ b/inspect/examples/inspect_runtime_example/build.rs @@ -0,0 +1,23 @@ +// Copyright (c) Viable Systems and TezEdge Contributors +// SPDX-License-Identifier: MIT + +use ocaml_interop_dune_builder::DuneBuilder; + +fn main() { + let ocaml_dir = "ocaml"; + + // Rebuild if the OCaml source code changes + println!("cargo:rerun-if-changed={}/test_types.ml", ocaml_dir); + println!("cargo:rerun-if-changed={}/dune", ocaml_dir); + + // Build the OCaml code using dune + let dune_builder = DuneBuilder::new(ocaml_dir); + let objects = dune_builder.build("test_types.exe.o"); + + let mut build = cc::Build::new(); + for object in objects { + build.object(object); + } + + build.compile("callable_ocaml"); +} diff --git a/inspect/examples/inspect_runtime_example/ocaml/dune b/inspect/examples/inspect_runtime_example/ocaml/dune new file mode 100644 index 0000000..75e7eaf --- /dev/null +++ b/inspect/examples/inspect_runtime_example/ocaml/dune @@ -0,0 +1,4 @@ +(executables + (names test_types) + (libraries threads) + (modes object)) diff --git a/inspect/examples/inspect_runtime_example/ocaml/test_types.ml b/inspect/examples/inspect_runtime_example/ocaml/test_types.ml new file mode 100644 index 0000000..e1b3838 --- /dev/null +++ b/inspect/examples/inspect_runtime_example/ocaml/test_types.ml @@ -0,0 +1,77 @@ +(* Copyright (c) Viable Systems and TezEdge Contributors + SPDX-License-Identifier: MIT *) + +(* Define various OCaml types to test the inspector *) + +(* Simple record with different field types *) +type test_record = { + int_field: int; + float_field: float; + string_field: string; + bool_field: bool; + tuple_field: int * string * float; +} + +(* Variant type *) +type test_variant = + | Empty + | WithInt of int + | WithString of string + | WithTuple of int * float + | Complex of test_record + +(* List of variants *) +type nested_structure = test_variant list + +(* Polymorphic variant *) +type poly_variant = [ + | `None + | `Int of int + | `Tuple of int * string +] + +(* Function to create test instances *) +let create_test_record () = + { + int_field = 42; + float_field = 3.14159; + string_field = "Hello, OCaml!"; + bool_field = true; + tuple_field = (123, "tuple element", 45.67) + } + +let create_empty_variant () = Empty + +let create_int_variant () = WithInt 42 + +let create_string_variant () = WithString "variant string" + +let create_tuple_variant () = WithTuple (42, 3.14) + +let create_complex_variant () = Complex (create_test_record ()) + +let create_list_of_variants () = [ + Empty; + WithInt 42; + WithString "variant in list"; + WithTuple (99, 99.99) +] + +let create_poly_none () = `None + +let create_poly_int () = `Int 42 + +let create_poly_tuple () = `Tuple (42, "poly tuple") + +(* Register all functions for Rust to call *) +let () = + Callback.register "create_test_record" create_test_record; + Callback.register "create_empty_variant" create_empty_variant; + Callback.register "create_int_variant" create_int_variant; + Callback.register "create_string_variant" create_string_variant; + Callback.register "create_tuple_variant" create_tuple_variant; + Callback.register "create_complex_variant" create_complex_variant; + Callback.register "create_list_of_variants" create_list_of_variants; + Callback.register "create_poly_none" create_poly_none; + Callback.register "create_poly_int" create_poly_int; + Callback.register "create_poly_tuple" create_poly_tuple diff --git a/inspect/examples/inspect_runtime_example/src/main.rs b/inspect/examples/inspect_runtime_example/src/main.rs new file mode 100644 index 0000000..1271285 --- /dev/null +++ b/inspect/examples/inspect_runtime_example/src/main.rs @@ -0,0 +1,153 @@ +// Copyright (c) Viable Systems and TezEdge Contributors +// SPDX-License-Identifier: MIT + +//! This example demonstrates how to use the ocaml-interop-inspect crate +//! with an actual OCaml runtime. +//! +//! It shows how to: +//! 1. Initialize the OCaml runtime +//! 2. Call OCaml functions that create various data types +//! 3. Inspect the values returned from OCaml using the inspect_raw_value function +//! +//! The OCaml code defines various types (records, variants, lists, polymorphic variants) +//! that are created and returned to Rust for inspection. +//! +//! For each OCaml value, the example shows: +//! - The OCaml literal representation (as shown in the INSPECTING: line) +//! - The detailed memory structure (blocks, tags, fields) +//! - A compact view summarizing the structure +//! - A debug view showing the internal representation +//! +//! This is useful for understanding how OCaml values are represented in memory +//! and how the ocaml-interop-inspect crate helps visualize this representation. + +use ocaml_interop::*; +use ocaml_interop_inspect::inspect_raw_value; + +// Define a struct to represent unknown OCaml values +// This is used as a placeholder return type for OCaml functions +struct UnknownOCamlValue; + +mod ocaml { + use super::UnknownOCamlValue; + use ocaml_interop::*; + + ocaml! { + pub fn create_test_record(unit: ()) -> UnknownOCamlValue; + pub fn create_empty_variant(unit: ()) -> UnknownOCamlValue; + pub fn create_int_variant(unit: ()) -> UnknownOCamlValue; + pub fn create_string_variant(unit: ()) -> UnknownOCamlValue; + pub fn create_tuple_variant(unit: ()) -> UnknownOCamlValue; + pub fn create_complex_variant(unit: ()) -> UnknownOCamlValue; + pub fn create_list_of_variants(unit: ()) -> UnknownOCamlValue; + pub fn create_poly_none(unit: ()) -> UnknownOCamlValue; + pub fn create_poly_int(unit: ()) -> UnknownOCamlValue; + pub fn create_poly_tuple(unit: ()) -> UnknownOCamlValue; + } +} + +fn print_separator() { + println!("\n{}", "=".repeat(80)); +} + +fn inspect_value(raw_value: RawOCaml, name: &str) { + print_separator(); + println!("INSPECTING: {}", name); + println!(); + + // Inspect the raw OCaml value directly + let inspection = unsafe { inspect_raw_value(raw_value) }; + + // Print the detailed representation + println!("DETAILED STRUCTURE:"); + println!("{}", inspection); + + // Print the compact view + println!("\nCOMPACT VIEW:"); + println!("{}", inspection.compact()); + + // Print the debug representation + println!("\nDEBUG VIEW:"); + println!("{:?}", inspection); +} + +fn main() { + // Step 1: Initialize the OCaml runtime + // This starts the OCaml runtime and allows us to call OCaml functions from Rust + // The guard ensures that the OCaml runtime is properly shut down when we're done + let _guard = OCamlRuntime::init().expect("Failed to initialize OCaml runtime"); + + println!("OCaml Interop Inspect - Runtime Example"); + println!("======================================"); + println!("This example shows how to inspect various OCaml values using ocaml-interop-inspect."); + + // Step 2: Acquire the domain lock before interacting with OCaml + // This is necessary for thread safety when calling OCaml functions + OCamlRuntime::with_domain_lock(|cr| { + // Test record + let test_record = ocaml::create_test_record(cr, &OCaml::unit()); + unsafe { + inspect_value(test_record.get_raw(), + "{ int_field = 42; float_field = 3.14159; string_field = \"Hello, OCaml!\"; bool_field = true; tuple_field = (123, \"tuple element\", 45.67) }"); + } + + // Empty variant + let empty_variant = ocaml::create_empty_variant(cr, &OCaml::unit()); + unsafe { + inspect_value(empty_variant.get_raw(), "Empty"); + } + + // Int variant + let int_variant = ocaml::create_int_variant(cr, &OCaml::unit()); + unsafe { + inspect_value(int_variant.get_raw(), "WithInt 42"); + } + + // String variant + let string_variant = ocaml::create_string_variant(cr, &OCaml::unit()); + unsafe { + inspect_value(string_variant.get_raw(), "WithString \"variant string\""); + } + + // Tuple variant + let tuple_variant = ocaml::create_tuple_variant(cr, &OCaml::unit()); + unsafe { + inspect_value(tuple_variant.get_raw(), "WithTuple (42, 3.14)"); + } + + // Complex variant + let complex_variant = ocaml::create_complex_variant(cr, &OCaml::unit()); + unsafe { + inspect_value(complex_variant.get_raw(), + "Complex { int_field = 42; float_field = 3.14159; string_field = \"Hello, OCaml!\"; bool_field = true; tuple_field = (123, \"tuple element\", 45.67) }"); + } + + // List of variants + let list_of_variants = ocaml::create_list_of_variants(cr, &OCaml::unit()); + unsafe { + inspect_value( + list_of_variants.get_raw(), + "[Empty; WithInt 42; WithString \"variant in list\"; WithTuple (99, 99.99)]", + ); + } + + // Polymorphic variants + let poly_none = ocaml::create_poly_none(cr, &OCaml::unit()); + unsafe { + inspect_value(poly_none.get_raw(), "`None"); + } + + let poly_int = ocaml::create_poly_int(cr, &OCaml::unit()); + unsafe { + inspect_value(poly_int.get_raw(), "`Int 42"); + } + + let poly_tuple = ocaml::create_poly_tuple(cr, &OCaml::unit()); + unsafe { + inspect_value(poly_tuple.get_raw(), "`Tuple (42, \"poly tuple\")"); + } + }); + + print_separator(); + println!("Example completed successfully!"); +} diff --git a/inspect/src/inspector.rs b/inspect/src/inspector.rs new file mode 100644 index 0000000..c7c1576 --- /dev/null +++ b/inspect/src/inspector.rs @@ -0,0 +1,368 @@ +// Copyright (c) Viable Systems and TezEdge Contributors +// SPDX-License-Identifier: MIT + +//! Core value inspection functionality. + +use crate::value_repr::ValueRepr; +use ocaml_sys::{ + field as field_val, is_block, is_long, tag_val, wosize_val, Value as RawOCaml, CLOSURE, + DOUBLE_ARRAY, NO_SCAN, STRING, TAG_CONS, TAG_SOME, +}; +use std::fmt; + +/// Maximum depth for recursive inspection to avoid infinite loops +const MAX_DEPTH: usize = 8; + +/// Maximum number of fields to display for blocks to keep output readable +const MAX_FIELDS_DISPLAY: usize = 10; + +/// A value inspector that can analyze and display OCaml runtime values. +#[derive(Clone)] +pub struct ValueInspector { + repr: ValueRepr, +} + +impl ValueInspector { + /// Inspect a raw OCaml value and create a new inspector. + /// + /// This function analyzes the given raw OCaml value and creates a + /// representation that shows its internal structure. + /// + /// # Safety + /// + /// The caller must ensure that the `RawOCaml` value is valid and points + /// to a properly initialized OCaml value. + pub unsafe fn inspect(raw: RawOCaml) -> Self { + let repr = Self::inspect_value_recursive(raw, 0); + ValueInspector { repr } + } + + /// Get the value representation. + pub fn repr(&self) -> &ValueRepr { + &self.repr + } + + /// Get a compact single-line representation of the value. + pub fn compact(&self) -> String { + self.compact_repr(&self.repr, 0) + } + + /// Recursively inspect an OCaml value with depth tracking. + unsafe fn inspect_value_recursive(raw: RawOCaml, depth: usize) -> ValueRepr { + if depth > MAX_DEPTH { + return ValueRepr::Error { + message: format!("Maximum inspection depth ({}) exceeded", MAX_DEPTH), + }; + } + + if is_long(raw) { + // Immediate value + Self::inspect_immediate(raw) + } else if is_block(raw) { + // Block value + Self::inspect_block(raw, depth) + } else { + ValueRepr::Error { + message: "Invalid OCaml value: neither immediate nor block".to_string(), + } + } + } + + /// Inspect an immediate (non-block) value. + unsafe fn inspect_immediate(raw: RawOCaml) -> ValueRepr { + let value = raw as isize; + + // OCaml integers are represented as (value << 1) | 1 + let interpretation = if value & 1 == 1 { + // This is a tagged integer + let int_val = value >> 1; + + // Special handling for known OCaml constants + if raw == ocaml_sys::TRUE { + format!("integer {} (likely boolean true)", int_val) + } else if raw == 0x1 { + // UNIT, FALSE, NONE, EMPTY_LIST all have value 0x1 + // These all decode to integer 0, so we list all possibilities + "integer 0 (could be: unit, boolean false, empty list, None)".to_string() + } else { + format!("integer {}", int_val) + } + } else { + // This should not happen for immediate values in practice + format!("raw immediate value {:#x}", value) + }; + + ValueRepr::Immediate { + value, + interpretation, + } + } + + /// Inspect a block value. + unsafe fn inspect_block(raw: RawOCaml, depth: usize) -> ValueRepr { + let tag = tag_val(raw); + let size = wosize_val(raw); + + match tag { + // String tag + t if t == STRING => Self::inspect_string(raw), + + // Double array tag (float array) + t if t == DOUBLE_ARRAY => Self::inspect_float_array(raw), + + // Closure tag + t if t == CLOSURE => ValueRepr::Custom { + tag, + size, + description: "function closure".to_string(), + }, + + // Custom blocks (for custom C types, bigarrays, etc.) + t if t >= NO_SCAN => ValueRepr::Custom { + tag, + size, + description: Self::describe_custom_block(tag), + }, + + // Regular block (variant, tuple, record, etc.) + _ => Self::inspect_regular_block(raw, tag, size, depth), + } + } + + /// Inspect a string value. + unsafe fn inspect_string(raw: RawOCaml) -> ValueRepr { + // For string blocks, the length is stored in the header + // We'll get the word size and estimate the string length + let size = wosize_val(raw); + let estimated_byte_length = size * std::mem::size_of::(); + + // For safety, we'll just indicate it's a string without trying to read the content + // This avoids calling OCaml runtime functions that might not be available + let content = format!("", estimated_byte_length); + + ValueRepr::String { + content, + byte_length: estimated_byte_length, + } + } + + /// Inspect a float array (double array). + unsafe fn inspect_float_array(raw: RawOCaml) -> ValueRepr { + let size = wosize_val(raw); + let interpretation = format!("float array of {} elements", size); + + // For float arrays, we could extract the actual values but for now + // we'll just show it as a custom block + ValueRepr::Block { + tag: DOUBLE_ARRAY, + size, + fields: vec![], // We don't recurse into float array elements for now + interpretation, + } + } + + /// Inspect a regular block (variant, tuple, record, etc.). + unsafe fn inspect_regular_block( + raw: RawOCaml, + tag: u8, + size: usize, + depth: usize, + ) -> ValueRepr { + let interpretation = Self::interpret_block_type(tag, size); + + // Recursively inspect fields, but limit the number to avoid excessive output + let fields_to_inspect = std::cmp::min(size, MAX_FIELDS_DISPLAY); + let mut fields = Vec::with_capacity(fields_to_inspect); + + for i in 0..fields_to_inspect { + let field_raw = unsafe { *(field_val(raw, i) as *const RawOCaml) }; + let field_repr = Self::inspect_value_recursive(field_raw, depth + 1); + fields.push(field_repr); + } + + // If we truncated fields, add a note + if size > MAX_FIELDS_DISPLAY { + fields.push(ValueRepr::Error { + message: format!("... and {} more fields", size - MAX_FIELDS_DISPLAY), + }); + } + + ValueRepr::Block { + tag, + size, + fields, + interpretation, + } + } + + /// Interpret the type of a block based on its tag and size. + fn interpret_block_type(tag: u8, size: usize) -> String { + match tag { + 0 => match size { + 0 => "variant constructor or unit tuple".to_string(), + 1 => "single-field record or tuple".to_string(), + 2 => "pair tuple or record".to_string(), + n => format!("tuple/record with {} fields", n), + }, + t if t == TAG_CONS => "list cons cell (::)".to_string(), + t if t == TAG_SOME => "option Some".to_string(), + 1..=255 => format!("variant constructor with tag {}", tag), + } + } + + /// Describe a custom block based on its tag. + fn describe_custom_block(tag: u8) -> String { + match tag { + t if t == NO_SCAN => "abstract/custom block".to_string(), + t if t == 255 => "bigarray or similar".to_string(), + _ => format!("custom block (tag {})", tag), + } + } + + /// Create a compact representation that fits on one line. + fn compact_repr(&self, repr: &ValueRepr, depth: usize) -> String { + const MAX_COMPACT_DEPTH: usize = 3; + + if depth > MAX_COMPACT_DEPTH { + return "...".to_string(); + } + + match repr { + ValueRepr::Immediate { interpretation, .. } => interpretation.clone(), + ValueRepr::String { + content, + byte_length, + } => { + if content.len() > 20 { + format!("\"{}...\" ({} bytes)", &content[..20], byte_length) + } else { + format!("\"{}\"", content) + } + } + ValueRepr::Block { + tag, + size, + fields, + interpretation, + } => { + if fields.is_empty() { + format!("{} (tag={}, size={})", interpretation, tag, size) + } else if fields.len() == 1 { + format!( + "{} [{}]", + interpretation, + self.compact_repr(&fields[0], depth + 1) + ) + } else { + let field_reprs: Vec = fields + .iter() + .take(3) + .map(|f| self.compact_repr(f, depth + 1)) + .collect(); + let fields_str = if fields.len() > 3 { + format!("{}, ...", field_reprs.join(", ")) + } else { + field_reprs.join(", ") + }; + format!("{} [{}]", interpretation, fields_str) + } + } + ValueRepr::Custom { + description, + tag, + size, + } => { + format!("{} (tag={}, size={})", description, tag, size) + } + ValueRepr::Error { message } => { + format!("Error: {}", message) + } + } + } +} + +impl fmt::Display for ValueInspector { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.repr) + } +} + +impl fmt::Debug for ValueInspector { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ValueInspector") + .field("repr", &self.repr) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_immediate_value_interpretation() { + // Test OCaml integer encoding + // OCaml integers are encoded as (value << 1) | 1 + let ocaml_int_5 = (5 << 1) | 1; // Should be 11 in binary + + unsafe { + let inspector = ValueInspector::inspect(ocaml_int_5 as RawOCaml); + match inspector.repr() { + ValueRepr::Immediate { + value, + interpretation, + } => { + assert_eq!(*value, ocaml_int_5 as isize); + assert!(interpretation.contains("integer 5")); + } + _ => panic!("Expected immediate value"), + } + } + } + + #[test] + fn test_unit_value() { + unsafe { + let inspector = ValueInspector::inspect(ocaml_sys::UNIT); + match inspector.repr() { + ValueRepr::Immediate { interpretation, .. } => { + // Unit is one of the possibilities for 0x1 + assert!(interpretation.contains("unit")); + } + _ => panic!("Expected immediate value for unit"), + } + } + } + + #[test] + fn test_boolean_values() { + unsafe { + let true_inspector = ValueInspector::inspect(ocaml_sys::TRUE); + match true_inspector.repr() { + ValueRepr::Immediate { interpretation, .. } => { + assert!(interpretation.contains("true")); + } + _ => panic!("Expected immediate value for true"), + } + + let false_inspector = ValueInspector::inspect(ocaml_sys::FALSE); + match false_inspector.repr() { + ValueRepr::Immediate { interpretation, .. } => { + // False is one of the possibilities for 0x1 + assert!(interpretation.contains("false")); + } + _ => panic!("Expected immediate value for false"), + } + } + } + + #[test] + fn test_compact_representation() { + unsafe { + let inspector = ValueInspector::inspect((42 << 1) | 1); + let compact = inspector.compact(); + assert!(compact.contains("42")); + assert!(!compact.contains('\n')); // Should be single line + } + } +} diff --git a/inspect/src/lib.rs b/inspect/src/lib.rs new file mode 100644 index 0000000..29eb7ec --- /dev/null +++ b/inspect/src/lib.rs @@ -0,0 +1,72 @@ +// Copyright (c) Viable Systems and TezEdge Contributors +// SPDX-License-Identifier: MIT + +//! # OCaml Interop Inspect +//! +//! This crate provides utilities for inspecting OCaml runtime values to help debug +//! conversions between OCaml and Rust. It offers human-readable representations +//! of OCaml values that show their internal structure including whether they are +//! immediate values or blocks, their tags, sizes, and field contents. +//! +//! ## Usage +//! +//! ```rust,ignore +//! use ocaml_sys::Value as RawOCaml; +//! use ocaml_interop_inspect::inspect_raw_value; +//! +//! // When you have access to a raw OCaml value: +//! unsafe { +//! let raw_value: RawOCaml = /* some OCaml value */; +//! let inspection = inspect_raw_value(raw_value); +//! println!("OCaml value structure: {}", inspection); +//! // Or for debugging output: +//! println!("OCaml value structure: {:?}", inspection); +//! } +//! ``` +//! +//! When using with ocaml-interop, you can get the raw value using `value.raw()`: +//! +//! ```rust,ignore +//! use ocaml_interop::*; +//! use ocaml_interop_inspect::inspect_raw_value; +//! +//! fn debug_conversion(value: OCaml<'_, T>) { +//! let inspection = unsafe { inspect_raw_value(value.raw()) }; +//! println!("OCaml value structure: {}", inspection); +//! } +//! ``` + +// We only need RawOCaml from ocaml-sys +use ocaml_sys::Value as RawOCaml; + +pub mod inspector; +pub mod value_repr; + +pub use inspector::ValueInspector; +pub use value_repr::ValueRepr; + +/// Inspect a raw OCaml value directly. +/// +/// This function analyzes the raw representation of an OCaml value and returns +/// a `ValueInspector` that can be displayed to show the value's structure, +/// including whether it's immediate or a block, its tag, size, and contents. +/// +/// # Example +/// +/// ```rust,ignore +/// use ocaml_sys::Value as RawOCaml; +/// use ocaml_interop_inspect::inspect_raw_value; +/// +/// unsafe { +/// let raw_value: RawOCaml = /* some OCaml value */; +/// let inspection = inspect_raw_value(raw_value); +/// println!("Value structure: {}", inspection); +/// } +/// ``` +/// +/// # Safety +/// +/// The caller must ensure that the `RawOCaml` value is valid. +pub unsafe fn inspect_raw_value(raw: RawOCaml) -> ValueInspector { + ValueInspector::inspect(raw) +} diff --git a/inspect/src/value_repr.rs b/inspect/src/value_repr.rs new file mode 100644 index 0000000..1b0ee97 --- /dev/null +++ b/inspect/src/value_repr.rs @@ -0,0 +1,142 @@ +// Copyright (c) Viable Systems and TezEdge Contributors +// SPDX-License-Identifier: MIT + +//! Value representation types for OCaml value inspection. + +use std::fmt; + +/// Represents the different types of OCaml values. +#[derive(Debug, Clone, PartialEq)] +pub enum ValueRepr { + /// An immediate value (integer, boolean, unit, etc.) + Immediate { + /// The raw value as an integer + value: isize, + /// Human-readable interpretation of the value + interpretation: String, + }, + /// A block value (tuple, record, variant, array, etc.) + Block { + /// The tag of the block + tag: u8, + /// The size (number of fields) of the block + size: usize, + /// The fields of the block + fields: Vec, + /// Human-readable interpretation of the block type + interpretation: String, + }, + /// A string value (special case of block with string tag) + String { + /// The actual string content + content: String, + /// The byte length of the string + byte_length: usize, + }, + /// A custom block (used for boxed values, bigarrays, etc.) + Custom { + /// The tag of the custom block + tag: u8, + /// The size of the custom block + size: usize, + /// A description of what this custom block likely represents + description: String, + }, + /// An error occurred while trying to inspect the value + Error { + /// Description of the error + message: String, + }, +} + +impl ValueRepr { + /// Get a short description of the value type + pub fn type_name(&self) -> &'static str { + match self { + ValueRepr::Immediate { .. } => "immediate", + ValueRepr::Block { .. } => "block", + ValueRepr::String { .. } => "string", + ValueRepr::Custom { .. } => "custom", + ValueRepr::Error { .. } => "error", + } + } + + /// Check if this is an immediate value + pub fn is_immediate(&self) -> bool { + matches!(self, ValueRepr::Immediate { .. }) + } + + /// Check if this is a block value + pub fn is_block(&self) -> bool { + matches!(self, ValueRepr::Block { .. }) + } + + /// Check if this is a string value + pub fn is_string(&self) -> bool { + matches!(self, ValueRepr::String { .. }) + } + + /// Check if this is a custom block + pub fn is_custom(&self) -> bool { + matches!(self, ValueRepr::Custom { .. }) + } + + /// Check if this represents an error + pub fn is_error(&self) -> bool { + matches!(self, ValueRepr::Error { .. }) + } +} + +impl fmt::Display for ValueRepr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ValueRepr::Immediate { + value, + interpretation, + } => { + write!(f, "Immediate({}): {}", value, interpretation) + } + ValueRepr::Block { + tag, + size, + fields, + interpretation, + } => { + write!(f, "Block(tag={}, size={}): {}", tag, size, interpretation)?; + if !fields.is_empty() { + write!(f, " [")?; + for (i, field) in fields.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", i, field)?; + } + write!(f, "]")?; + } + Ok(()) + } + ValueRepr::String { + content, + byte_length, + } => { + // Show escaped string content, truncated if too long + let display_content = if content.len() > 50 { + format!("{}...", &content[..50]) + } else { + content.clone() + }; + write!(f, "String({} bytes): {:?}", byte_length, display_content) + } + ValueRepr::Custom { + tag, + size, + description, + } => { + write!(f, "Custom(tag={}, size={}): {}", tag, size, description) + } + ValueRepr::Error { message } => { + write!(f, "Error: {}", message) + } + } + } +} diff --git a/inspect/tests/integration_tests.rs b/inspect/tests/integration_tests.rs new file mode 100644 index 0000000..4e203a5 --- /dev/null +++ b/inspect/tests/integration_tests.rs @@ -0,0 +1,211 @@ +// Copyright (c) Viable Systems and TezEdge Contributors +// SPDX-License-Identifier: MIT + +use ocaml_interop_inspect::{inspect_raw_value, ValueRepr}; + +#[test] +fn test_immediate_values() { + // Test various immediate values + + // Integer values + let int_zero = unsafe { inspect_raw_value((0_isize << 1) | 1) }; + match int_zero.repr() { + ValueRepr::Immediate { + value, + interpretation, + } => { + assert_eq!(*value, 1); // OCaml encoding: (0 << 1) | 1 = 1 + assert!(interpretation.contains("integer 0")); + } + _ => panic!("Expected immediate value for zero"), + } + + let int_positive = unsafe { inspect_raw_value((42_isize << 1) | 1) }; + match int_positive.repr() { + ValueRepr::Immediate { + value, + interpretation, + } => { + assert_eq!(*value, 85); // OCaml encoding: (42 << 1) | 1 = 85 + assert!(interpretation.contains("integer 42")); + } + _ => panic!("Expected immediate value for 42"), + } + + let int_negative = unsafe { inspect_raw_value((-10_isize << 1) | 1) }; + match int_negative.repr() { + ValueRepr::Immediate { + value, + interpretation, + } => { + assert_eq!(*value, -19); // OCaml encoding: (-10 << 1) | 1 = -19 + assert!(interpretation.contains("integer -10")); + } + _ => panic!("Expected immediate value for -10"), + } +} + +#[test] +fn test_special_values() { + // Test special OCaml values + + let unit_val = unsafe { inspect_raw_value(ocaml_sys::UNIT) }; + match unit_val.repr() { + ValueRepr::Immediate { interpretation, .. } => { + assert!(interpretation.contains("unit")); + } + _ => panic!("Expected immediate value for unit"), + } + + let true_val = unsafe { inspect_raw_value(ocaml_sys::TRUE) }; + match true_val.repr() { + ValueRepr::Immediate { interpretation, .. } => { + assert!(interpretation.contains("true")); + } + _ => panic!("Expected immediate value for true"), + } + + let false_val = unsafe { inspect_raw_value(ocaml_sys::FALSE) }; + match false_val.repr() { + ValueRepr::Immediate { interpretation, .. } => { + assert!(interpretation.contains("false")); + } + _ => panic!("Expected immediate value for false"), + } + + let empty_list = unsafe { inspect_raw_value(ocaml_sys::EMPTY_LIST) }; + match empty_list.repr() { + ValueRepr::Immediate { interpretation, .. } => { + assert!(interpretation.contains("empty list")); + } + _ => panic!("Expected immediate value for empty list"), + } + + let none_val = unsafe { inspect_raw_value(ocaml_sys::NONE) }; + match none_val.repr() { + ValueRepr::Immediate { interpretation, .. } => { + assert!(interpretation.contains("None")); + } + _ => panic!("Expected immediate value for None"), + } +} + +#[test] +fn test_value_type_checks() { + let int_val = unsafe { inspect_raw_value((123_isize << 1) | 1) }; + assert!(int_val.repr().is_immediate()); + assert!(!int_val.repr().is_block()); + assert!(!int_val.repr().is_string()); + assert!(!int_val.repr().is_custom()); + assert!(!int_val.repr().is_error()); + assert_eq!(int_val.repr().type_name(), "immediate"); +} + +#[test] +fn test_compact_representation() { + let int_val = unsafe { inspect_raw_value((999_isize << 1) | 1) }; + let compact = int_val.compact(); + + // Compact representation should be concise and contain the essential info + assert!(compact.contains("999")); + assert!(!compact.contains('\n')); // Should be single line + + // Test that compact is shorter than full representation + let full = format!("{}", int_val); + assert!(compact.len() <= full.len()); +} + +#[test] +fn test_display_and_debug() { + let int_val = unsafe { inspect_raw_value((456_isize << 1) | 1) }; + + // Test Display trait + let display_str = format!("{}", int_val); + assert!(display_str.contains("456")); + + // Test Debug trait + let debug_str = format!("{:?}", int_val); + assert!(debug_str.contains("ValueInspector")); +} + +#[test] +fn test_value_repr_display() { + let int_val = unsafe { inspect_raw_value((789_isize << 1) | 1) }; + let display_str = format!("{}", int_val.repr()); + assert!(display_str.contains("Immediate")); + assert!(display_str.contains("789")); +} + +#[test] +fn test_large_integers() { + // Test edge cases for OCaml integer representation + + // Maximum positive value that fits in OCaml int (platform dependent, but test a reasonably large one) + let large_positive = unsafe { inspect_raw_value((1000000_isize << 1) | 1) }; + match large_positive.repr() { + ValueRepr::Immediate { interpretation, .. } => { + assert!(interpretation.contains("integer 1000000")); + } + _ => panic!("Expected immediate value for large positive"), + } + + // Large negative value + let large_negative = unsafe { inspect_raw_value((-500000_isize << 1) | 1) }; + match large_negative.repr() { + ValueRepr::Immediate { interpretation, .. } => { + assert!(interpretation.contains("integer -500000")); + } + _ => panic!("Expected immediate value for large negative"), + } +} + +#[test] +fn test_error_representation() { + // Test the error representation type + let error_repr = ValueRepr::Error { + message: "Test error message".to_string(), + }; + + assert!(error_repr.is_error()); + assert!(!error_repr.is_immediate()); + assert_eq!(error_repr.type_name(), "error"); + + let display_str = format!("{}", error_repr); + assert!(display_str.contains("Error: Test error message")); +} + +#[test] +fn test_inspector_clone() { + let int_val = unsafe { inspect_raw_value((42_isize << 1) | 1) }; + let cloned = int_val.clone(); + + // Ensure the clone has the same representation + match (int_val.repr(), cloned.repr()) { + ( + ValueRepr::Immediate { + value: v1, + interpretation: i1, + }, + ValueRepr::Immediate { + value: v2, + interpretation: i2, + }, + ) => { + assert_eq!(v1, v2); + assert_eq!(i1, i2); + } + _ => panic!("Expected both to be immediate values"), + } +} + +#[test] +fn test_multiple_inspections() { + // Test that multiple inspections of the same value produce consistent results + let raw_val = (100_isize << 1) | 1; + + let inspect1 = unsafe { inspect_raw_value(raw_val) }; + let inspect2 = unsafe { inspect_raw_value(raw_val) }; + + assert_eq!(format!("{}", inspect1), format!("{}", inspect2)); + assert_eq!(inspect1.compact(), inspect2.compact()); +}