1use 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}