Skip to main content

runmat_runtime/
value_fact.rs

1use std::collections::BTreeMap;
2
3use runmat_types::{
4    AliasFact, CallableFact, CallableIdentity, CellFact, CertaintyFact, ContiguityFact,
5    DimensionFact, DynamicReason, ExceptionFact, ExecutionFact, ForeignAffinity,
6    ForeignAffinityFact, ForeignFact, ForeignLifetime, ForeignLifetimeFact, ForeignOwnership,
7    ForeignOwnershipFact, InvalidationVector, LayoutFact, MutationFact, NumericClass,
8    NumericDomain, NumericFact, ObjectFact, OutputListFact, QualifiedName, ResidencyFact,
9    ShapeFact, StorageFact, StructFact, SymbolName, ValueFact, ValueKindFact, ViewFact,
10};
11use runmat_value::{IntValue, NumericDType, Value};
12
13/// Adapt one live runtime value into the dependency-neutral static fact schema.
14///
15/// This is deliberately owned by the runtime: `runmat-types` never imports or
16/// inspects live values. The exhaustive match is also the coverage guard for
17/// additions to `Value`.
18pub fn value_fact(value: &Value) -> ValueFact {
19    match value {
20        Value::Int(value) => numeric_scalar(int_class(value), NumericDomain::Real),
21        Value::Num(_) => numeric_scalar(NumericClass::Double, NumericDomain::Real),
22        Value::Complex(_, _) => numeric_scalar(NumericClass::Double, NumericDomain::Complex),
23        Value::Bool(_) => scalar(ValueKindFact::Logical),
24        Value::LogicalArray(value) => dense(ValueKindFact::Logical, &value.shape),
25        Value::String(_) => scalar(ValueKindFact::String),
26        Value::StringArray(value) => dense(ValueKindFact::String, &value.shape),
27        Value::CharArray(value) => dense(ValueKindFact::Character, &value.shape),
28        Value::Tensor(value) => dense(
29            numeric_kind(dtype_class(value.numeric_dtype()), NumericDomain::Real),
30            &value.shape,
31        ),
32        Value::SparseTensor(value) => {
33            let kind = value
34                .numeric_dtype()
35                .map_or(ValueKindFact::Logical, |dtype| {
36                    numeric_kind(dtype_class(dtype), NumericDomain::Real)
37                });
38            sparse(kind, &value.shape())
39        }
40        Value::ComplexTensor(value) => dense(
41            numeric_kind(dtype_class(value.numeric_dtype()), NumericDomain::Complex),
42            &value.shape,
43        ),
44        Value::Symbolic(_) => scalar(ValueKindFact::Symbolic),
45        Value::SymbolicArray(value) => dense(ValueKindFact::Symbolic, &value.shape),
46        Value::Cell(value) => {
47            let elements = value.data.iter().map(value_fact).collect::<Vec<_>>();
48            let element = elements
49                .iter()
50                .cloned()
51                .reduce(|left, right| runmat_types::FactJoin::join(&left, &right))
52                .unwrap_or_else(|| ValueFact::unknown(DynamicReason::RuntimeValue));
53            dense(
54                ValueKindFact::Cell(CellFact {
55                    element: Box::new(element),
56                    elements,
57                    elements_complete: true,
58                }),
59                &value.shape,
60            )
61        }
62        Value::Struct(value) => scalar(ValueKindFact::Struct(StructFact {
63            fields: value
64                .fields
65                .iter()
66                .map(|(name, value)| (name.clone(), value_fact(value)))
67                .collect(),
68            fields_complete: true,
69        })),
70        Value::GpuTensor(value) => {
71            let kind = gpu_kind(value);
72            ValueFact {
73                kind: kind.clone().unwrap_or(ValueKindFact::Unknown),
74                shape: shape(&value.shape),
75                storage: StorageFact::Opaque,
76                layout: LayoutFact::Unknown,
77                contiguity: ContiguityFact::Unknown,
78                view: ViewFact::Unknown,
79                residency: ResidencyFact::Device {
80                    provider: Some(format!("device:{}", value.device_id)),
81                },
82                alias: AliasFact::Identity,
83                mutation: MutationFact::HandleSemantics,
84                certainty: if kind.is_some() {
85                    CertaintyFact::Proven
86                } else {
87                    CertaintyFact::Dynamic(DynamicReason::UnsupportedRepresentation)
88                },
89                invalidation: InvalidationVector::default(),
90            }
91        }
92        Value::Object(value) => object(
93            &value.class_name,
94            value
95                .properties
96                .iter()
97                .map(|(name, value)| (name.clone(), value_fact(value)))
98                .collect(),
99            true,
100            false,
101            ShapeFact::Scalar,
102        ),
103        Value::ObjectArray(value) => object(
104            value.class_name(),
105            BTreeMap::new(),
106            false,
107            value
108                .data()
109                .iter()
110                .any(|element| matches!(element, Value::HandleObject(_))),
111            shape(value.shape()),
112        ),
113        Value::HandleObject(value) => object(
114            &value.class_name,
115            BTreeMap::new(),
116            false,
117            true,
118            ShapeFact::Scalar,
119        ),
120        Value::Listener(value) => object(
121            &value.target_class_name,
122            BTreeMap::new(),
123            false,
124            true,
125            ShapeFact::Scalar,
126        ),
127        Value::OutputList(values) => scalar(ValueKindFact::OutputList(OutputListFact {
128            outputs: values.iter().map(value_fact).collect(),
129            variadic: false,
130        })),
131        Value::FunctionHandle(name) => {
132            callable(CallableIdentity::DynamicName(SymbolName(name.clone())))
133        }
134        Value::ExternalFunctionHandle(name) => {
135            callable(CallableIdentity::ExternalName(qualified_name(name)))
136        }
137        Value::MethodFunctionHandle(name) => callable(CallableIdentity::Method(
138            runmat_types::MethodId(name.clone()),
139        )),
140        Value::BoundFunctionHandle { function, .. } => callable(CallableIdentity::BoundFunction(
141            runmat_types::FunctionId(*function),
142        )),
143        Value::Closure(value) => {
144            let mut fact = callable_kind(CallableIdentity::DynamicName(SymbolName(
145                value.function_name.clone(),
146            )));
147            fact.captures = value.captures.iter().map(value_fact).collect();
148            scalar(ValueKindFact::Callable(fact))
149        }
150        Value::ClassRef(name) => scalar(ValueKindFact::ClassReference(
151            runmat_types::ClassReferenceFact {
152                class: None,
153                runtime_class: Some(qualified_name(name)),
154            },
155        )),
156        Value::MException(value) => scalar(ValueKindFact::Exception(ExceptionFact {
157            identifier: Some(value.identifier.clone()),
158        })),
159        Value::Future(_) => execution(ExecutionFact::Future {
160            output: Box::new(ValueFact::unknown(DynamicReason::RuntimeValue)),
161            state: runmat_types::FutureStateFact::Unknown,
162        }),
163        Value::Task(_) => execution(ExecutionFact::Task {
164            output: Box::new(ValueFact::unknown(DynamicReason::RuntimeValue)),
165            spawn_safety: runmat_types::SpawnSafetyFact::RequiresIsolation,
166        }),
167        Value::Pool(_) => execution(ExecutionFact::Pool),
168        Value::Job(_) => execution(ExecutionFact::Job {
169            output: Box::new(ValueFact::unknown(DynamicReason::RuntimeValue)),
170        }),
171        Value::Foreign(reference) => {
172            let mut fact = fact(
173                ValueKindFact::Foreign(ForeignFact {
174                    family: reference.type_identity.family.clone(),
175                    type_name: Some(reference.type_identity.name.clone()),
176                    type_version: Some(reference.type_identity.version),
177                    ownership: match reference.ownership {
178                        ForeignOwnership::Borrowed => ForeignOwnershipFact::Borrowed,
179                        ForeignOwnership::Owned => ForeignOwnershipFact::Owned,
180                        ForeignOwnership::Shared => ForeignOwnershipFact::Shared,
181                    },
182                    affinity: match reference.affinity {
183                        ForeignAffinity::AnyThread => ForeignAffinityFact::AnyThread,
184                        ForeignAffinity::OriginThread => ForeignAffinityFact::OriginThread,
185                        ForeignAffinity::OriginProcess => ForeignAffinityFact::OriginProcess,
186                        ForeignAffinity::RemoteHost => ForeignAffinityFact::RemoteHost,
187                    },
188                    lifetime: match reference.lifetime {
189                        ForeignLifetime::Call => ForeignLifetimeFact::Call,
190                        ForeignLifetime::Session => ForeignLifetimeFact::Session,
191                        ForeignLifetime::Persistent => ForeignLifetimeFact::Persistent,
192                        ForeignLifetime::External => ForeignLifetimeFact::External,
193                    },
194                }),
195                ShapeFact::Scalar,
196                StorageFact::Opaque,
197            );
198            fact.alias = AliasFact::Identity;
199            fact.mutation = MutationFact::HandleSemantics;
200            fact
201        }
202    }
203}
204
205fn numeric_scalar(class: NumericClass, domain: NumericDomain) -> ValueFact {
206    scalar(numeric_kind(class, domain))
207}
208
209fn numeric_kind(class: NumericClass, domain: NumericDomain) -> ValueKindFact {
210    ValueKindFact::Numeric(NumericFact { class, domain })
211}
212
213fn scalar(kind: ValueKindFact) -> ValueFact {
214    fact(kind, ShapeFact::Scalar, StorageFact::Scalar)
215}
216
217fn dense(kind: ValueKindFact, dimensions: &[usize]) -> ValueFact {
218    fact(kind, shape(dimensions), StorageFact::Dense)
219}
220
221fn sparse(kind: ValueKindFact, dimensions: &[usize]) -> ValueFact {
222    fact(kind, shape(dimensions), StorageFact::Sparse)
223}
224
225fn fact(kind: ValueKindFact, shape: ShapeFact, storage: StorageFact) -> ValueFact {
226    ValueFact {
227        kind,
228        shape,
229        storage,
230        layout: LayoutFact::ColumnMajor,
231        contiguity: ContiguityFact::Contiguous,
232        view: ViewFact::Materialized,
233        residency: ResidencyFact::Host,
234        alias: AliasFact::Unique,
235        mutation: MutationFact::ValueSemantics,
236        certainty: CertaintyFact::Proven,
237        invalidation: InvalidationVector::default(),
238    }
239}
240
241fn object(
242    class_name: &str,
243    properties: BTreeMap<String, ValueFact>,
244    properties_complete: bool,
245    handle_semantics: bool,
246    shape: ShapeFact,
247) -> ValueFact {
248    let mut fact = fact(
249        ValueKindFact::Object(ObjectFact {
250            class: None,
251            runtime_class: Some(qualified_name(class_name)),
252            properties,
253            properties_complete,
254            handle_semantics: Some(handle_semantics),
255        }),
256        shape,
257        StorageFact::Opaque,
258    );
259    if handle_semantics {
260        fact.alias = AliasFact::Identity;
261        fact.mutation = MutationFact::HandleSemantics;
262    }
263    fact
264}
265
266fn callable(identity: CallableIdentity) -> ValueFact {
267    scalar(ValueKindFact::Callable(callable_kind(identity)))
268}
269
270fn callable_kind(identity: CallableIdentity) -> CallableFact {
271    CallableFact {
272        identity: Some(identity),
273        parameters: Vec::new(),
274        parameters_complete: false,
275        outputs: Vec::new(),
276        outputs_complete: false,
277        variadic_inputs: true,
278        variadic_outputs: true,
279        captures: Vec::new(),
280        captures_complete: true,
281    }
282}
283
284fn execution(execution: ExecutionFact) -> ValueFact {
285    let mut fact = scalar(ValueKindFact::Execution(execution));
286    fact.alias = AliasFact::Identity;
287    fact.mutation = MutationFact::HandleSemantics;
288    fact
289}
290
291fn shape(dimensions: &[usize]) -> ShapeFact {
292    ShapeFact::Shaped {
293        dims: dimensions
294            .iter()
295            .copied()
296            .map(DimensionFact::Known)
297            .collect(),
298    }
299}
300
301fn qualified_name(name: &str) -> QualifiedName {
302    QualifiedName(
303        name.split('.')
304            .map(|segment| SymbolName(segment.to_owned()))
305            .collect(),
306    )
307}
308
309fn int_class(value: &IntValue) -> NumericClass {
310    match value {
311        IntValue::I8(_) => NumericClass::Int8,
312        IntValue::I16(_) => NumericClass::Int16,
313        IntValue::I32(_) => NumericClass::Int32,
314        IntValue::I64(_) => NumericClass::Int64,
315        IntValue::U8(_) => NumericClass::UInt8,
316        IntValue::U16(_) => NumericClass::UInt16,
317        IntValue::U32(_) => NumericClass::UInt32,
318        IntValue::U64(_) => NumericClass::UInt64,
319    }
320}
321
322fn dtype_class(value: NumericDType) -> NumericClass {
323    match value {
324        NumericDType::F64 => NumericClass::Double,
325        NumericDType::F32 => NumericClass::Single,
326        NumericDType::I8 => NumericClass::Int8,
327        NumericDType::I16 => NumericClass::Int16,
328        NumericDType::I32 => NumericClass::Int32,
329        NumericDType::I64 => NumericClass::Int64,
330        NumericDType::U8 => NumericClass::UInt8,
331        NumericDType::U16 => NumericClass::UInt16,
332        NumericDType::U32 => NumericClass::UInt32,
333        NumericDType::U64 => NumericClass::UInt64,
334    }
335}
336
337fn gpu_kind(handle: &runmat_accelerate_api::GpuTensorHandle) -> Option<ValueKindFact> {
338    use runmat_accelerate_api::{GpuTensorStorage, IntegerElementType, ProviderPrecision};
339
340    if runmat_accelerate_api::handle_is_logical(handle) {
341        return Some(ValueKindFact::Logical);
342    }
343    let class = runmat_accelerate_api::handle_integer_type(handle)
344        .map(|integer| match integer {
345            IntegerElementType::I8 => NumericClass::Int8,
346            IntegerElementType::I16 => NumericClass::Int16,
347            IntegerElementType::I32 => NumericClass::Int32,
348            IntegerElementType::I64 => NumericClass::Int64,
349            IntegerElementType::U8 => NumericClass::UInt8,
350            IntegerElementType::U16 => NumericClass::UInt16,
351            IntegerElementType::U32 => NumericClass::UInt32,
352            IntegerElementType::U64 => NumericClass::UInt64,
353        })
354        .or_else(|| match runmat_accelerate_api::handle_precision(handle) {
355            Some(ProviderPrecision::F32) => Some(NumericClass::Single),
356            Some(ProviderPrecision::F64) => Some(NumericClass::Double),
357            None => match runmat_accelerate_api::handle_class_name(handle).as_deref() {
358                Some("single") => Some(NumericClass::Single),
359                Some("double") => Some(NumericClass::Double),
360                _ => None,
361            },
362        })?;
363    let domain = match runmat_accelerate_api::handle_storage(handle) {
364        GpuTensorStorage::Real => NumericDomain::Real,
365        GpuTensorStorage::ComplexInterleaved => NumericDomain::Complex,
366    };
367    Some(numeric_kind(class, domain))
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    use runmat_accelerate_api::{
374        clear_handle_metadata, GpuTensorHandle, GpuTensorStorage, NumericElementType,
375    };
376    use runmat_value::{CellArray, ForeignRef, Tensor};
377
378    #[test]
379    fn preserves_recursive_numeric_class_and_shape() {
380        let value = Value::Cell(
381            CellArray::new_with_shape(
382                vec![
383                    Value::Tensor(Tensor::from_f32(vec![1.0, 2.0], vec![1, 2]).unwrap()),
384                    Value::Tensor(Tensor::from_f32(vec![3.0, 4.0], vec![1, 2]).unwrap()),
385                ],
386                vec![1, 2],
387            )
388            .unwrap(),
389        );
390        let fact = value_fact(&value);
391        let ValueKindFact::Cell(cell) = fact.kind else {
392            panic!("expected cell fact");
393        };
394        assert!(matches!(
395            cell.element.kind,
396            ValueKindFact::Numeric(NumericFact {
397                class: NumericClass::Single,
398                domain: NumericDomain::Real
399            })
400        ));
401        assert_eq!(fact.shape, shape(&[1, 2]));
402        assert_eq!(cell.elements.len(), 2);
403        assert!(cell.elements_complete);
404    }
405
406    #[test]
407    fn preserves_device_integer_class_domain_shape_and_residency() {
408        let handle = GpuTensorHandle::new(vec![2, 3], 71, 9).with_numeric_descriptor(
409            NumericElementType::U64,
410            GpuTensorStorage::ComplexInterleaved,
411        );
412        let fact = value_fact(&Value::GpuTensor(handle.clone()));
413        assert_eq!(
414            fact.kind,
415            ValueKindFact::Numeric(NumericFact {
416                class: NumericClass::UInt64,
417                domain: NumericDomain::Complex,
418            })
419        );
420        assert_eq!(fact.shape, shape(&[2, 3]));
421        assert_eq!(
422            fact.residency,
423            ResidencyFact::Device {
424                provider: Some("device:71".into())
425            }
426        );
427        assert_eq!(fact.certainty, CertaintyFact::Proven);
428        clear_handle_metadata(&handle);
429    }
430
431    #[test]
432    fn unknown_device_element_type_is_an_explicit_dynamic_boundary() {
433        let handle = GpuTensorHandle::new(vec![1, 4], 73, 11);
434        clear_handle_metadata(&handle);
435        let fact = value_fact(&Value::GpuTensor(handle));
436        assert_eq!(fact.kind, ValueKindFact::Unknown);
437        assert_eq!(
438            fact.certainty,
439            CertaintyFact::Dynamic(DynamicReason::UnsupportedRepresentation)
440        );
441    }
442
443    #[test]
444    fn foreign_reference_preserves_static_type_and_lifecycle_without_live_identity() {
445        let value = Value::Foreign(ForeignRef {
446            host_identity: "worker-7".into(),
447            handle: 91,
448            generation: 4,
449            type_identity: runmat_types::ForeignTypeIdentity {
450                family: "java".into(),
451                name: "java.lang.StringBuilder".into(),
452                version: 2,
453            },
454            ownership: ForeignOwnership::Shared,
455            affinity: ForeignAffinity::OriginProcess,
456            lifetime: ForeignLifetime::Session,
457        });
458
459        let fact = value_fact(&value);
460        assert_eq!(
461            fact.kind,
462            ValueKindFact::Foreign(ForeignFact {
463                family: "java".into(),
464                type_name: Some("java.lang.StringBuilder".into()),
465                type_version: Some(2),
466                ownership: ForeignOwnershipFact::Shared,
467                affinity: ForeignAffinityFact::OriginProcess,
468                lifetime: ForeignLifetimeFact::Session,
469            })
470        );
471        assert_eq!(fact.shape, ShapeFact::Scalar);
472        assert_eq!(fact.storage, StorageFact::Opaque);
473        assert_eq!(fact.alias, AliasFact::Identity);
474        assert_eq!(fact.mutation, MutationFact::HandleSemantics);
475    }
476}