Skip to main content

runmat_runtime/builtins/logical/
ops.rs

1//! MATLAB-compatible `logical` builtin with GPU-aware semantics for RunMat.
2
3use log::trace;
4use runmat_accelerate_api::{self, AccelProvider, GpuTensorHandle, HostTensorView};
5use runmat_builtins::{
6    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
7    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
8    CharArray, ComplexTensor, LogicalArray, ResolveContext, StringArray, Tensor, Type, Value,
9};
10use runmat_macros::runtime_builtin;
11
12use crate::builtins::common::{
13    gpu_helpers,
14    shape::{canonical_scalar_shape, normalize_scalar_shape},
15    spec::{
16        BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
17        ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
18    },
19    tensor,
20};
21use crate::builtins::logical::type_resolvers::logical_like;
22
23use crate::{build_runtime_error, BuiltinResult, RuntimeError};
24
25#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::logical::ops")]
26pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
27    name: "logical",
28    op_kind: GpuOpKind::Elementwise,
29    supported_precisions: &[ScalarType::F32, ScalarType::F64],
30    broadcast: BroadcastSemantics::Matlab,
31    provider_hooks: &[ProviderHook::Binary {
32        name: "elem_ne",
33        commutative: true,
34    }],
35    constant_strategy: ConstantStrategy::InlineLiteral,
36    residency: ResidencyPolicy::NewHandle,
37    nan_mode: ReductionNaN::Include,
38    two_pass_threshold: None,
39    workgroup_size: None,
40    accepts_nan_mode: false,
41    notes: "Preferred path issues elem_ne(X, 0) on the device; missing hooks trigger a gather → host cast → re-upload sequence flagged as logical.",
42};
43
44#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::logical::ops")]
45pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
46    name: "logical",
47    shape: ShapeRequirements::BroadcastCompatible,
48    constant_strategy: ConstantStrategy::InlineLiteral,
49    elementwise: None,
50    reduction: None,
51    emits_nan: false,
52    notes: "Fusion support will arrive alongside a dedicated WGSL template; today the builtin executes outside fusion plans.",
53};
54
55const BUILTIN_NAME: &str = "logical";
56
57const LOGICAL_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
58    name: "tf",
59    ty: BuiltinParamType::LogicalArray,
60    arity: BuiltinParamArity::Required,
61    default: None,
62    description: "Logical-converted result.",
63}];
64
65const LOGICAL_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
66    name: "A",
67    ty: BuiltinParamType::Any,
68    arity: BuiltinParamArity::Required,
69    default: None,
70    description: "Input value to convert.",
71}];
72
73const LOGICAL_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
74    label: "tf = logical(A)",
75    inputs: &LOGICAL_INPUTS,
76    outputs: &LOGICAL_OUTPUT,
77}];
78
79const LOGICAL_ERROR_TOO_MANY_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
80    code: "RM.LOGICAL.TOO_MANY_INPUTS",
81    identifier: Some("RunMat:logical:TooManyInputs"),
82    when: "More than one input argument is provided.",
83    message: "logical: too many input arguments",
84};
85
86const LOGICAL_ERROR_CONVERSION_NOT_POSSIBLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
87    code: "RM.LOGICAL.CONVERSION_NOT_POSSIBLE",
88    identifier: Some("RunMat:logical:ConversionNotPossible"),
89    when: "Input type cannot be converted to logical.",
90    message: "logical: conversion to logical is not possible for this input type",
91};
92
93const LOGICAL_ERROR_GPU_GATHER_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
94    code: "RM.LOGICAL.GPU_GATHER_FAILED",
95    identifier: Some("RunMat:logical:GpuGatherFailed"),
96    when: "GPU input gather fails during host fallback.",
97    message: "logical: failed to gather gpuArray input",
98};
99
100const LOGICAL_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
101    code: "RM.LOGICAL.INTERNAL",
102    identifier: Some("RunMat:logical:InternalError"),
103    when: "Internal logical buffer materialization fails.",
104    message: "logical: internal conversion error",
105};
106
107const LOGICAL_ERRORS: [BuiltinErrorDescriptor; 4] = [
108    LOGICAL_ERROR_TOO_MANY_INPUTS,
109    LOGICAL_ERROR_CONVERSION_NOT_POSSIBLE,
110    LOGICAL_ERROR_GPU_GATHER_FAILED,
111    LOGICAL_ERROR_INTERNAL,
112];
113
114pub const LOGICAL_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
115    signatures: &LOGICAL_SIGNATURES,
116    output_mode: BuiltinOutputMode::Fixed,
117    completion_policy: BuiltinCompletionPolicy::Public,
118    errors: &LOGICAL_ERRORS,
119};
120
121fn logical_type(args: &[Type], _context: &ResolveContext) -> Type {
122    args.first().map(logical_like).unwrap_or(Type::logical())
123}
124
125fn logical_error_with_message(
126    message: impl Into<String>,
127    error: &'static BuiltinErrorDescriptor,
128) -> RuntimeError {
129    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
130    if let Some(identifier) = error.identifier {
131        builder = builder.with_identifier(identifier);
132    }
133    builder.build()
134}
135
136#[runtime_builtin(
137    name = "logical",
138    category = "logical",
139    summary = "Convert scalars, arrays, and gpuArray values to logical outputs.",
140    keywords = "logical,boolean,gpuArray,mask,conversion",
141    accel = "unary",
142    type_resolver(logical_type),
143    descriptor(crate::builtins::logical::ops::LOGICAL_DESCRIPTOR),
144    builtin_path = "crate::builtins::logical::ops"
145)]
146async fn logical_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
147    if !rest.is_empty() {
148        return Err(logical_error_with_message(
149            LOGICAL_ERROR_TOO_MANY_INPUTS.message,
150            &LOGICAL_ERROR_TOO_MANY_INPUTS,
151        ));
152    }
153    convert_value_to_logical(value).await
154}
155
156async fn convert_value_to_logical(value: Value) -> BuiltinResult<Value> {
157    match value {
158        Value::Bool(_) | Value::LogicalArray(_) => Ok(value),
159        Value::Num(n) => Ok(Value::Bool(n != 0.0)),
160        Value::Int(i) => Ok(Value::Bool(!i.is_zero())),
161        Value::Complex(re, im) => Ok(Value::Bool(!complex_is_zero(re, im))),
162        Value::Tensor(tensor) => logical_from_tensor(tensor),
163        Value::SparseTensor(sparse) => logical_from_sparse_tensor(sparse),
164        Value::ComplexTensor(tensor) => logical_from_complex_tensor(tensor),
165        Value::CharArray(chars) => logical_from_char_array(chars),
166        Value::StringArray(strings) => logical_from_string_array(strings),
167        Value::GpuTensor(handle) => logical_from_gpu(handle).await,
168        Value::String(_) => Err(conversion_error("string")),
169        Value::Symbolic(expr) => expr
170            .numeric_constant_value()
171            .map(|value| Value::Bool(value != 0.0))
172            .ok_or_else(|| conversion_error("sym")),
173        Value::SymbolicArray(_) => Err(conversion_error("sym")),
174        Value::Cell(_) => Err(conversion_error("cell")),
175        Value::Struct(_) => Err(conversion_error("struct")),
176        Value::Object(obj) => Err(conversion_error(&obj.class_name)),
177        Value::HandleObject(handle) => Err(conversion_error(&handle.class_name)),
178        Value::Listener(_) => Err(conversion_error("event.listener")),
179        Value::FunctionHandle(_)
180        | Value::ExternalFunctionHandle(_)
181        | Value::MethodFunctionHandle(_)
182        | Value::BoundFunctionHandle { .. }
183        | Value::Closure(_) => Err(conversion_error("function_handle")),
184        Value::ClassRef(_) => Err(conversion_error("meta.class")),
185        Value::MException(_) => Err(conversion_error("MException")),
186        Value::OutputList(_) => Err(conversion_error("OutputList")),
187    }
188}
189
190fn logical_from_tensor(tensor: Tensor) -> BuiltinResult<Value> {
191    let buffer = LogicalBuffer::from_real_tensor(&tensor);
192    logical_buffer_to_host(buffer)
193}
194
195fn logical_from_sparse_tensor(sparse: runmat_builtins::SparseTensor) -> BuiltinResult<Value> {
196    let tensor = sparse.to_dense().map_err(|err| {
197        logical_error_with_message(
198            format!("logical: failed to densify sparse input: {err}"),
199            &LOGICAL_ERROR_INTERNAL,
200        )
201    })?;
202    logical_from_tensor(tensor)
203}
204
205fn logical_from_complex_tensor(tensor: ComplexTensor) -> BuiltinResult<Value> {
206    let buffer = LogicalBuffer::from_complex_tensor(&tensor);
207    logical_buffer_to_host(buffer)
208}
209
210fn logical_from_char_array(chars: CharArray) -> BuiltinResult<Value> {
211    let buffer = LogicalBuffer::from_char_array(&chars);
212    logical_buffer_to_host(buffer)
213}
214
215fn logical_from_string_array(strings: StringArray) -> BuiltinResult<Value> {
216    let bits: Vec<u8> = strings
217        .data
218        .iter()
219        .map(|s| if s.is_empty() { 0 } else { 1 })
220        .collect();
221    let shape = canonical_shape(&strings.shape, bits.len());
222    logical_buffer_to_host(LogicalBuffer { bits, shape })
223}
224
225async fn logical_from_gpu(handle: GpuTensorHandle) -> BuiltinResult<Value> {
226    if runmat_accelerate_api::handle_is_logical(&handle) {
227        return Ok(Value::GpuTensor(handle));
228    }
229
230    let provider = runmat_accelerate_api::provider();
231
232    if let Some(p) = provider {
233        match p.logical_islogical(&handle) {
234            Ok(true) => {
235                runmat_accelerate_api::set_handle_logical(&handle, true);
236                return Ok(Value::GpuTensor(handle));
237            }
238            Ok(false) => {}
239            Err(err) => {
240                trace!("logical: provider logical_islogical hook unavailable, falling back ({err})")
241            }
242        }
243        if let Some(result) = try_gpu_cast(p, &handle).await {
244            return Ok(gpu_helpers::logical_gpu_value(result));
245        } else {
246            trace!(
247                "logical: provider elem_ne/zeros_like unavailable for buffer {} – gathering",
248                handle.buffer_id
249            );
250        }
251    }
252
253    let tensor = gpu_helpers::gather_tensor_async(&handle)
254        .await
255        .map_err(|err| {
256            logical_error_with_message(
257                format!("{BUILTIN_NAME}: {err}"),
258                &LOGICAL_ERROR_GPU_GATHER_FAILED,
259            )
260        })?;
261    let buffer = LogicalBuffer::from_real_tensor(&tensor);
262    logical_buffer_to_gpu(buffer, provider)
263}
264
265fn logical_buffer_to_host(buffer: LogicalBuffer) -> BuiltinResult<Value> {
266    let LogicalBuffer { bits, shape } = buffer;
267    if tensor::element_count(&shape) == 1 && bits.len() == 1 {
268        Ok(Value::Bool(bits[0] != 0))
269    } else {
270        LogicalArray::new(bits, shape)
271            .map(Value::LogicalArray)
272            .map_err(|e| {
273                logical_error_with_message(format!("logical: {e}"), &LOGICAL_ERROR_INTERNAL)
274            })
275    }
276}
277
278fn logical_buffer_to_gpu(
279    buffer: LogicalBuffer,
280    provider: Option<&'static dyn AccelProvider>,
281) -> BuiltinResult<Value> {
282    if let Some(p) = provider {
283        let floats: Vec<f64> = buffer
284            .bits
285            .iter()
286            .map(|&b| if b != 0 { 1.0 } else { 0.0 })
287            .collect();
288        let view = HostTensorView {
289            data: &floats,
290            shape: &buffer.shape,
291        };
292        match p.upload(&view) {
293            Ok(handle) => Ok(gpu_helpers::logical_gpu_value(handle)),
294            Err(err) => {
295                trace!("logical: upload failed during fallback path ({err})");
296                logical_buffer_to_host(buffer)
297            }
298        }
299    } else {
300        logical_buffer_to_host(buffer)
301    }
302}
303
304async fn try_gpu_cast(
305    provider: &'static dyn AccelProvider,
306    input: &GpuTensorHandle,
307) -> Option<GpuTensorHandle> {
308    let zeros = provider.zeros_like(input).ok()?;
309    let result = provider.elem_ne(input, &zeros).await.ok();
310    let _ = provider.free(&zeros);
311    result
312}
313
314fn complex_is_zero(re: f64, im: f64) -> bool {
315    re == 0.0 && im == 0.0
316}
317
318fn conversion_error(type_name: &str) -> RuntimeError {
319    logical_error_with_message(
320        format!(
321            "logical: conversion to logical from {} is not possible",
322            type_name
323        ),
324        &LOGICAL_ERROR_CONVERSION_NOT_POSSIBLE,
325    )
326}
327
328#[derive(Clone)]
329struct LogicalBuffer {
330    bits: Vec<u8>,
331    shape: Vec<usize>,
332}
333
334impl LogicalBuffer {
335    fn from_real_tensor(tensor: &Tensor) -> Self {
336        let bits: Vec<u8> = tensor
337            .data
338            .iter()
339            .map(|&v| if v != 0.0 { 1 } else { 0 })
340            .collect();
341        let shape = canonical_shape(&tensor.shape, bits.len());
342        Self { bits, shape }
343    }
344
345    fn from_complex_tensor(tensor: &ComplexTensor) -> Self {
346        let bits: Vec<u8> = tensor
347            .data
348            .iter()
349            .map(|&(re, im)| if !complex_is_zero(re, im) { 1 } else { 0 })
350            .collect();
351        let shape = canonical_shape(&tensor.shape, bits.len());
352        Self { bits, shape }
353    }
354
355    fn from_char_array(chars: &CharArray) -> Self {
356        let bits: Vec<u8> = chars
357            .data
358            .iter()
359            .map(|&ch| if (ch as u32) != 0 { 1 } else { 0 })
360            .collect();
361        let original_shape = vec![chars.rows, chars.cols];
362        let shape = canonical_shape(&original_shape, bits.len());
363        Self { bits, shape }
364    }
365}
366
367fn canonical_shape(shape: &[usize], len: usize) -> Vec<usize> {
368    if tensor::element_count(shape) == len {
369        return normalize_scalar_shape(shape);
370    }
371    if len == 0 {
372        if shape.len() > 1 {
373            return shape.to_vec();
374        }
375        return vec![0];
376    }
377    if len == 1 {
378        canonical_scalar_shape()
379    } else {
380        vec![len, 1]
381    }
382}
383
384#[cfg(test)]
385pub(crate) mod tests {
386    use super::*;
387    use crate::builtins::common::test_support;
388    use futures::executor::block_on;
389    use runmat_accelerate_api::HostTensorView;
390    use runmat_builtins::{
391        CellArray, IntValue, MException, ObjectInstance, SparseTensor, StructValue, SymbolicExpr,
392    };
393
394    fn logical_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
395        block_on(super::logical_builtin(value, rest))
396    }
397
398    fn assert_error_message(err: &crate::RuntimeError, expected: &str) {
399        assert_eq!(err.message(), expected);
400    }
401
402    fn assert_error_contains(err: &crate::RuntimeError, expected: &str) {
403        assert!(
404            err.message().contains(expected),
405            "unexpected error: {}",
406            err.message()
407        );
408    }
409
410    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
411    #[test]
412    fn logical_scalar_num() {
413        let result = logical_builtin(Value::Num(5.0), Vec::new()).expect("logical");
414        assert_eq!(result, Value::Bool(true));
415
416        let zero_result = logical_builtin(Value::Num(0.0), Vec::new()).expect("logical");
417        assert_eq!(zero_result, Value::Bool(false));
418    }
419
420    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
421    #[test]
422    fn logical_converts_symbolic_constants() {
423        let nonzero = logical_builtin(Value::Symbolic(SymbolicExpr::constant(2.0)), Vec::new())
424            .expect("logical");
425        assert_eq!(nonzero, Value::Bool(true));
426
427        let zero = logical_builtin(Value::Symbolic(SymbolicExpr::constant(0.0)), Vec::new())
428            .expect("logical");
429        assert_eq!(zero, Value::Bool(false));
430    }
431
432    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
433    #[test]
434    fn logical_rejects_symbolic_variables() {
435        let err = logical_builtin(Value::Symbolic(SymbolicExpr::variable("x")), Vec::new())
436            .expect_err("symbolic variable should not convert");
437
438        assert_eq!(
439            err.identifier(),
440            LOGICAL_ERROR_CONVERSION_NOT_POSSIBLE.identifier
441        );
442        assert!(err.message().contains("logical from sym"));
443    }
444
445    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
446    #[test]
447    fn logical_nan_is_true() {
448        let tensor = Tensor::new(vec![0.0, f64::NAN, -0.0], vec![1, 3]).unwrap();
449        let result = logical_builtin(Value::Tensor(tensor), Vec::new()).expect("logical");
450        match result {
451            Value::LogicalArray(array) => assert_eq!(array.data, vec![0, 1, 0]),
452            other => panic!("expected logical array, got {:?}", other),
453        }
454    }
455
456    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
457    #[test]
458    fn logical_tensor_matrix() {
459        let tensor = Tensor::new(vec![0.0, 2.0, -3.0, 0.0], vec![2, 2]).unwrap();
460        let result = logical_builtin(Value::Tensor(tensor), Vec::new()).expect("logical");
461        match result {
462            Value::LogicalArray(array) => {
463                assert_eq!(array.shape, vec![2, 2]);
464                assert_eq!(array.data, vec![0, 1, 1, 0]);
465            }
466            other => panic!("expected logical array, got {:?}", other),
467        }
468    }
469
470    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
471    #[test]
472    fn logical_sparse_tensor_densifies() {
473        let sparse = SparseTensor::new(3, 2, vec![0, 1, 2], vec![1, 2], vec![4.0, -1.0]).unwrap();
474        let result = logical_builtin(Value::SparseTensor(sparse), Vec::new()).expect("logical");
475        match result {
476            Value::LogicalArray(array) => {
477                assert_eq!(array.shape, vec![3, 2]);
478                assert_eq!(array.data, vec![0, 1, 0, 0, 0, 1]);
479            }
480            other => panic!("expected logical array, got {:?}", other),
481        }
482    }
483
484    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
485    #[test]
486    fn logical_complex_conversion() {
487        let complex =
488            ComplexTensor::new(vec![(0.0, 0.0), (1.0, 0.0), (0.0, 2.0)], vec![3, 1]).unwrap();
489        let result = logical_builtin(Value::ComplexTensor(complex), Vec::new()).expect("logical");
490        match result {
491            Value::LogicalArray(array) => {
492                assert_eq!(array.data, vec![0, 1, 1]);
493            }
494            other => panic!("expected logical array, got {:?}", other),
495        }
496    }
497
498    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
499    #[test]
500    fn logical_char_array_conversion() {
501        let chars = CharArray::new(vec!['A', '\0', 'C'], 1, 3).unwrap();
502        let result = logical_builtin(Value::CharArray(chars), Vec::new()).expect("logical");
503        match result {
504            Value::LogicalArray(array) => assert_eq!(array.data, vec![1, 0, 1]),
505            other => panic!("expected logical array, got {:?}", other),
506        }
507    }
508
509    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
510    #[test]
511    fn logical_string_error() {
512        let err = logical_builtin(Value::String("runmat".to_string()), Vec::new()).unwrap_err();
513        assert_error_message(
514            &err,
515            "logical: conversion to logical from string is not possible",
516        );
517        assert_eq!(
518            err.identifier(),
519            LOGICAL_ERROR_CONVERSION_NOT_POSSIBLE.identifier
520        );
521    }
522
523    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
524    #[test]
525    fn logical_struct_error() {
526        let mut st = StructValue::new();
527        st.insert("field", Value::Num(1.0));
528        let err = logical_builtin(Value::Struct(st), Vec::new()).unwrap_err();
529        assert_error_contains(&err, "struct");
530        assert_eq!(
531            err.identifier(),
532            LOGICAL_ERROR_CONVERSION_NOT_POSSIBLE.identifier
533        );
534    }
535
536    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
537    #[test]
538    fn logical_cell_error() {
539        let cell = CellArray::new(vec![Value::Num(1.0)], 1, 1).expect("cell creation");
540        let err = logical_builtin(Value::Cell(cell), Vec::new()).unwrap_err();
541        assert_error_message(
542            &err,
543            "logical: conversion to logical from cell is not possible",
544        );
545        assert_eq!(
546            err.identifier(),
547            LOGICAL_ERROR_CONVERSION_NOT_POSSIBLE.identifier
548        );
549    }
550
551    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
552    #[test]
553    fn logical_function_handle_error() {
554        let err = logical_builtin(Value::FunctionHandle("foo".into()), Vec::new()).unwrap_err();
555        assert_error_message(
556            &err,
557            "logical: conversion to logical from function_handle is not possible",
558        );
559        assert_eq!(
560            err.identifier(),
561            LOGICAL_ERROR_CONVERSION_NOT_POSSIBLE.identifier
562        );
563    }
564
565    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
566    #[test]
567    fn logical_object_error() {
568        let obj = ObjectInstance::new("DemoClass".to_string());
569        let err = logical_builtin(Value::Object(obj), Vec::new()).unwrap_err();
570        assert_error_contains(&err, "DemoClass");
571        assert_eq!(
572            err.identifier(),
573            LOGICAL_ERROR_CONVERSION_NOT_POSSIBLE.identifier
574        );
575    }
576
577    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
578    #[test]
579    fn logical_mexception_error() {
580        let mex = MException::new("id:logical".into(), "message".into());
581        let err = logical_builtin(Value::MException(mex), Vec::new()).unwrap_err();
582        assert_error_message(
583            &err,
584            "logical: conversion to logical from MException is not possible",
585        );
586        assert_eq!(
587            err.identifier(),
588            LOGICAL_ERROR_CONVERSION_NOT_POSSIBLE.identifier
589        );
590    }
591
592    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
593    #[test]
594    fn logical_too_many_inputs_error() {
595        let err = logical_builtin(Value::Bool(true), vec![Value::Bool(false)]).unwrap_err();
596        assert_error_message(&err, LOGICAL_ERROR_TOO_MANY_INPUTS.message);
597        assert_eq!(err.identifier(), LOGICAL_ERROR_TOO_MANY_INPUTS.identifier);
598    }
599
600    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
601    #[test]
602    fn logical_gpu_roundtrip() {
603        test_support::with_test_provider(|provider| {
604            let tensor = Tensor::new(vec![0.0, 1.0, -2.0], vec![3, 1]).unwrap();
605            let view = HostTensorView {
606                data: &tensor.data,
607                shape: &tensor.shape,
608            };
609            let handle = provider.upload(&view).expect("upload");
610            let result =
611                logical_builtin(Value::GpuTensor(handle.clone()), Vec::new()).expect("logical");
612            let gathered = test_support::gather(result.clone()).expect("gather");
613            assert_eq!(gathered.data, vec![0.0, 1.0, 1.0]);
614            if let Value::GpuTensor(out) = result {
615                assert!(runmat_accelerate_api::handle_is_logical(&out));
616            } else {
617                panic!("expected gpu tensor output");
618            }
619        });
620    }
621
622    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
623    #[test]
624    fn logical_gpu_passthrough_for_logical_handle() {
625        test_support::with_test_provider(|provider| {
626            let tensor = Tensor::new(vec![0.0, 1.0], vec![2, 1]).unwrap();
627            let view = HostTensorView {
628                data: &tensor.data,
629                shape: &tensor.shape,
630            };
631            let handle = provider.upload(&view).expect("upload");
632            runmat_accelerate_api::set_handle_logical(&handle, true);
633            let result =
634                logical_builtin(Value::GpuTensor(handle.clone()), Vec::new()).expect("logical");
635            match result {
636                Value::GpuTensor(out) => assert_eq!(out, handle),
637                other => panic!("expected gpu tensor, got {:?}", other),
638            }
639        });
640    }
641
642    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
643    #[test]
644    fn logical_bool_and_logical_inputs_passthrough() {
645        let res_bool = logical_builtin(Value::Bool(true), Vec::new()).expect("logical");
646        assert_eq!(res_bool, Value::Bool(true));
647
648        let logical = LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap();
649        let res_array =
650            logical_builtin(Value::LogicalArray(logical.clone()), Vec::new()).expect("logical");
651        assert_eq!(res_array, Value::LogicalArray(logical));
652    }
653
654    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
655    #[test]
656    fn logical_empty_tensor_preserves_shape() {
657        let tensor = Tensor::new(Vec::new(), vec![0, 3]).unwrap();
658        let result = logical_builtin(Value::Tensor(tensor), Vec::new()).expect("logical");
659        match result {
660            Value::LogicalArray(array) => {
661                assert!(array.data.is_empty());
662                assert_eq!(array.shape, vec![0, 3]);
663            }
664            other => panic!("expected logical array, got {:?}", other),
665        }
666    }
667
668    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
669    #[test]
670    fn logical_integer_scalar() {
671        let res = logical_builtin(Value::Int(IntValue::I32(0)), Vec::new()).expect("logical");
672        assert_eq!(res, Value::Bool(false));
673
674        let res_nonzero =
675            logical_builtin(Value::Int(IntValue::I32(-5)), Vec::new()).expect("logical");
676        assert_eq!(res_nonzero, Value::Bool(true));
677    }
678
679    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
680    #[test]
681    #[cfg(feature = "wgpu")]
682    fn logical_wgpu_matches_cpu_conversion() {
683        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
684            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
685        );
686
687        let tensor = Tensor::new(vec![0.0, 2.0, -3.0, f64::NAN], vec![2, 2]).unwrap();
688        let cpu = logical_builtin(Value::Tensor(tensor.clone()), Vec::new()).unwrap();
689
690        let view = runmat_accelerate_api::HostTensorView {
691            data: &tensor.data,
692            shape: &tensor.shape,
693        };
694        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
695        let handle = provider.upload(&view).expect("upload");
696
697        let gpu_value = logical_builtin(Value::GpuTensor(handle), Vec::new()).unwrap();
698        let out_handle = match gpu_value {
699            Value::GpuTensor(ref h) => {
700                assert!(runmat_accelerate_api::handle_is_logical(h));
701                h.clone()
702            }
703            other => panic!("expected gpu tensor, got {other:?}"),
704        };
705
706        let gathered = test_support::gather(Value::GpuTensor(out_handle)).expect("gather");
707
708        let (expected, expected_shape): (Vec<f64>, Vec<usize>) = match cpu {
709            Value::LogicalArray(arr) => (
710                arr.data
711                    .iter()
712                    .map(|&b| if b != 0 { 1.0 } else { 0.0 })
713                    .collect(),
714                arr.shape.clone(),
715            ),
716            Value::Bool(flag) => (vec![if flag { 1.0 } else { 0.0 }], vec![1, 1]),
717            other => panic!("unexpected cpu result {other:?}"),
718        };
719
720        assert_eq!(gathered.shape, expected_shape);
721        assert_eq!(gathered.data, expected);
722    }
723}