1use runmat_accelerate_api::{HostTensorView, ProviderFindResult};
4use runmat_builtins::{
5 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
6 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
7 ComplexTensor, IntValue, IntegerStorage, ResolveContext, Tensor, Type, Value,
8};
9use runmat_macros::runtime_builtin;
10
11use crate::builtins::array::type_resolvers::column_vector_type;
12use crate::builtins::common::arg_tokens::ArgToken;
13use crate::builtins::common::random_args::complex_tensor_into_value;
14use crate::builtins::common::spec::{
15 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
16 ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
17};
18use crate::builtins::common::{gpu_helpers, tensor};
19use crate::{build_runtime_error, RuntimeError};
20
21#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::array::indexing::find")]
22pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
23 name: "find",
24 op_kind: GpuOpKind::Custom("find"),
25 supported_precisions: &[ScalarType::F32, ScalarType::F64],
26 broadcast: BroadcastSemantics::None,
27 provider_hooks: &[ProviderHook::Custom("find")],
28 constant_strategy: ConstantStrategy::InlineLiteral,
29 residency: ResidencyPolicy::NewHandle,
30 nan_mode: ReductionNaN::Include,
31 two_pass_threshold: None,
32 workgroup_size: None,
33 accepts_nan_mode: false,
34 notes: "WGPU provider executes find directly on device; other providers fall back to host and re-upload results to preserve residency.",
35};
36
37#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::array::indexing::find")]
38pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
39 name: "find",
40 shape: ShapeRequirements::Any,
41 constant_strategy: ConstantStrategy::InlineLiteral,
42 elementwise: None,
43 reduction: None,
44 emits_nan: false,
45 notes: "Find drives control flow and currently bypasses fusion; metadata is present for completeness only.",
46};
47
48fn find_type(_args: &[Type], _ctx: &ResolveContext) -> Type {
49 column_vector_type()
50}
51
52const BUILTIN_NAME: &str = "find";
53
54const FIND_OUTPUT_LINEAR: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
55 name: "idx",
56 ty: BuiltinParamType::NumericArray,
57 arity: BuiltinParamArity::Required,
58 default: None,
59 description: "Linear indices of non-zero elements.",
60}];
61
62const FIND_OUTPUT_ROW_COL: [BuiltinParamDescriptor; 2] = [
63 BuiltinParamDescriptor {
64 name: "row",
65 ty: BuiltinParamType::NumericArray,
66 arity: BuiltinParamArity::Required,
67 default: None,
68 description: "Row subscripts of non-zero elements.",
69 },
70 BuiltinParamDescriptor {
71 name: "col",
72 ty: BuiltinParamType::NumericArray,
73 arity: BuiltinParamArity::Required,
74 default: None,
75 description: "Column subscripts of non-zero elements.",
76 },
77];
78
79const FIND_OUTPUT_ROW_COL_VAL: [BuiltinParamDescriptor; 3] = [
80 BuiltinParamDescriptor {
81 name: "row",
82 ty: BuiltinParamType::NumericArray,
83 arity: BuiltinParamArity::Required,
84 default: None,
85 description: "Row subscripts of non-zero elements.",
86 },
87 BuiltinParamDescriptor {
88 name: "col",
89 ty: BuiltinParamType::NumericArray,
90 arity: BuiltinParamArity::Required,
91 default: None,
92 description: "Column subscripts of non-zero elements.",
93 },
94 BuiltinParamDescriptor {
95 name: "v",
96 ty: BuiltinParamType::Any,
97 arity: BuiltinParamArity::Required,
98 default: None,
99 description: "Values at the reported row/column locations.",
100 },
101];
102
103const FIND_INPUTS_BASE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
104 name: "X",
105 ty: BuiltinParamType::Any,
106 arity: BuiltinParamArity::Required,
107 default: None,
108 description: "Input array to search.",
109}];
110
111const FIND_INPUTS_LIMIT: [BuiltinParamDescriptor; 2] = [
112 BuiltinParamDescriptor {
113 name: "X",
114 ty: BuiltinParamType::Any,
115 arity: BuiltinParamArity::Required,
116 default: None,
117 description: "Input array to search.",
118 },
119 BuiltinParamDescriptor {
120 name: "K",
121 ty: BuiltinParamType::NumericScalar,
122 arity: BuiltinParamArity::Required,
123 default: None,
124 description: "Maximum number of indices to return.",
125 },
126];
127
128const FIND_INPUTS_LIMIT_DIR: [BuiltinParamDescriptor; 3] = [
129 BuiltinParamDescriptor {
130 name: "X",
131 ty: BuiltinParamType::Any,
132 arity: BuiltinParamArity::Required,
133 default: None,
134 description: "Input array to search.",
135 },
136 BuiltinParamDescriptor {
137 name: "K",
138 ty: BuiltinParamType::NumericScalar,
139 arity: BuiltinParamArity::Required,
140 default: None,
141 description: "Maximum number of indices to return.",
142 },
143 BuiltinParamDescriptor {
144 name: "direction",
145 ty: BuiltinParamType::StringScalar,
146 arity: BuiltinParamArity::Required,
147 default: Some("\"first\""),
148 description: "Direction selector: `\"first\"` or `\"last\"`.",
149 },
150];
151
152const FIND_SIGNATURES: [BuiltinSignatureDescriptor; 7] = [
153 BuiltinSignatureDescriptor {
154 label: "idx = find(X)",
155 inputs: &FIND_INPUTS_BASE,
156 outputs: &FIND_OUTPUT_LINEAR,
157 },
158 BuiltinSignatureDescriptor {
159 label: "idx = find(X, K)",
160 inputs: &FIND_INPUTS_LIMIT,
161 outputs: &FIND_OUTPUT_LINEAR,
162 },
163 BuiltinSignatureDescriptor {
164 label: "idx = find(X, K, direction)",
165 inputs: &FIND_INPUTS_LIMIT_DIR,
166 outputs: &FIND_OUTPUT_LINEAR,
167 },
168 BuiltinSignatureDescriptor {
169 label: "[row, col] = find(X)",
170 inputs: &FIND_INPUTS_BASE,
171 outputs: &FIND_OUTPUT_ROW_COL,
172 },
173 BuiltinSignatureDescriptor {
174 label: "[row, col] = find(X, K, direction)",
175 inputs: &FIND_INPUTS_LIMIT_DIR,
176 outputs: &FIND_OUTPUT_ROW_COL,
177 },
178 BuiltinSignatureDescriptor {
179 label: "[row, col, v] = find(X)",
180 inputs: &FIND_INPUTS_BASE,
181 outputs: &FIND_OUTPUT_ROW_COL_VAL,
182 },
183 BuiltinSignatureDescriptor {
184 label: "[row, col, v] = find(X, K, direction)",
185 inputs: &FIND_INPUTS_LIMIT_DIR,
186 outputs: &FIND_OUTPUT_ROW_COL_VAL,
187 },
188];
189
190const FIND_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
191 code: "RM.FIND.INVALID_INPUT",
192 identifier: Some("RunMat:find:InvalidInput"),
193 when: "Input type or option arguments are not valid for find.",
194 message: "find: invalid input arguments",
195};
196
197const FIND_ERROR_PROVIDER_OUTPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
198 code: "RM.FIND.PROVIDER_OUTPUT",
199 identifier: Some("RunMat:find:ProviderOutput"),
200 when: "GPU provider does not return expected output buffers for requested nargout.",
201 message: "find: provider output buffer mismatch",
202};
203
204const FIND_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
205 code: "RM.FIND.INTERNAL",
206 identifier: Some("RunMat:find:InternalError"),
207 when: "Internal tensor conversion/materialization fails while building outputs.",
208 message: "find: internal error",
209};
210
211const FIND_ERRORS: [BuiltinErrorDescriptor; 3] = [
212 FIND_ERROR_INVALID_INPUT,
213 FIND_ERROR_PROVIDER_OUTPUT,
214 FIND_ERROR_INTERNAL,
215];
216
217pub const FIND_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
218 signatures: &FIND_SIGNATURES,
219 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
220 completion_policy: BuiltinCompletionPolicy::Public,
221 errors: &FIND_ERRORS,
222};
223
224fn find_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
225 find_error_with_message(error.message, error)
226}
227
228fn find_error_with_message(
229 message: impl Into<String>,
230 error: &'static BuiltinErrorDescriptor,
231) -> RuntimeError {
232 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
233 if let Some(identifier) = error.identifier {
234 builder = builder.with_identifier(identifier);
235 }
236 builder.build()
237}
238
239fn parse_find_tokens(tokens: &[ArgToken]) -> crate::BuiltinResult<FindOptions> {
240 match tokens.len() {
241 0 => Ok(FindOptions::default()),
242 1 => {
243 if let Some(direction) = token_to_direction(&tokens[0])? {
244 let limit = if matches!(direction, FindDirection::Last) {
245 Some(1)
246 } else {
247 None
248 };
249 Ok(FindOptions { limit, direction })
250 } else {
251 let limit = token_to_limit(&tokens[0])?;
252 Ok(FindOptions {
253 limit: Some(limit),
254 direction: FindDirection::First,
255 })
256 }
257 }
258 2 => {
259 let limit = token_to_limit(&tokens[0])?;
260 let direction = token_to_direction(&tokens[1])?.ok_or_else(|| {
261 find_error_with_message(
262 "find: third argument must be 'first' or 'last'",
263 &FIND_ERROR_INVALID_INPUT,
264 )
265 })?;
266 Ok(FindOptions {
267 limit: Some(limit),
268 direction,
269 })
270 }
271 _ => Err(find_error_with_message(
272 "find: too many input arguments",
273 &FIND_ERROR_INVALID_INPUT,
274 )),
275 }
276}
277
278fn token_to_direction(token: &ArgToken) -> crate::BuiltinResult<Option<FindDirection>> {
279 match token {
280 ArgToken::String(text) => match text.as_str() {
281 "first" => Ok(Some(FindDirection::First)),
282 "last" => Ok(Some(FindDirection::Last)),
283 _ => Err(find_error_with_message(
284 "find: direction must be 'first' or 'last'",
285 &FIND_ERROR_INVALID_INPUT,
286 )),
287 },
288 _ => Ok(None),
289 }
290}
291
292fn token_to_limit(token: &ArgToken) -> crate::BuiltinResult<usize> {
293 match token {
294 ArgToken::Number(value) => parse_limit_scalar(*value),
295 _ => Err(find_error_with_message(
296 "find: second argument must be a scalar",
297 &FIND_ERROR_INVALID_INPUT,
298 )),
299 }
300}
301
302#[runtime_builtin(
303 name = "find",
304 category = "array/indexing",
305 summary = "Locate nonzero indices and values.",
306 keywords = "find,nonzero,indices,row,column,gpu",
307 accel = "custom",
308 type_resolver(find_type),
309 descriptor(crate::builtins::array::indexing::find::FIND_DESCRIPTOR),
310 builtin_path = "crate::builtins::array::indexing::find"
311)]
312async fn find_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
313 let eval = evaluate(value, &rest).await?;
314 if let Some(out_count) = crate::output_count::current_output_count() {
315 if out_count == 0 {
316 return Ok(Value::OutputList(Vec::new()));
317 }
318 if out_count <= 1 {
319 let linear = eval.linear_value()?;
320 return Ok(crate::output_count::output_list_with_padding(
321 out_count,
322 vec![linear],
323 ));
324 }
325 let rows = eval.row_value()?;
326 let cols = eval.column_value()?;
327 let mut outputs = vec![rows, cols];
328 if out_count >= 3 {
329 outputs.push(eval.values_value()?);
330 }
331 return Ok(crate::output_count::output_list_with_padding(
332 out_count, outputs,
333 ));
334 }
335 eval.linear_value()
336}
337
338pub async fn evaluate(value: Value, args: &[Value]) -> crate::BuiltinResult<FindEval> {
340 let options = parse_options(args).await?;
341 match value {
342 Value::GpuTensor(handle) => {
343 if let Some(result) = try_provider_find(&handle, &options) {
344 return Ok(FindEval::from_gpu(result));
345 }
346 let (storage, _) = materialize_input(Value::GpuTensor(handle)).await?;
347 let result = compute_find(&storage, &options);
348 Ok(FindEval::from_host(result, true))
349 }
350 Value::SparseTensor(sparse) => {
351 let result = compute_find_sparse(&sparse, &options);
352 Ok(FindEval::from_host(result, false))
353 }
354 other => {
355 let (storage, input_was_gpu) = materialize_input(other).await?;
356 let result = compute_find(&storage, &options);
357 Ok(FindEval::from_host(result, input_was_gpu))
358 }
359 }
360}
361
362fn try_provider_find(
363 handle: &runmat_accelerate_api::GpuTensorHandle,
364 options: &FindOptions,
365) -> Option<ProviderFindResult> {
366 let provider = runmat_accelerate_api::provider()?;
367 let direction = match options.direction {
368 FindDirection::First => runmat_accelerate_api::FindDirection::First,
369 FindDirection::Last => runmat_accelerate_api::FindDirection::Last,
370 };
371 let limit = options.effective_limit();
372 provider.find(handle, limit, direction).ok()
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376enum FindDirection {
377 First,
378 Last,
379}
380
381#[derive(Debug, Clone)]
382struct FindOptions {
383 limit: Option<usize>,
384 direction: FindDirection,
385}
386
387impl Default for FindOptions {
388 fn default() -> Self {
389 Self {
390 limit: None,
391 direction: FindDirection::First,
392 }
393 }
394}
395
396impl FindOptions {
397 fn effective_limit(&self) -> Option<usize> {
398 match self.direction {
399 FindDirection::Last => self.limit.or(Some(1)),
400 FindDirection::First => self.limit,
401 }
402 }
403}
404
405#[derive(Clone)]
406enum DataStorage {
407 Real(Tensor),
408 Complex(ComplexTensor),
409}
410
411impl DataStorage {
412 fn shape(&self) -> &[usize] {
413 match self {
414 DataStorage::Real(t) => &t.shape,
415 DataStorage::Complex(t) => &t.shape,
416 }
417 }
418}
419
420#[derive(Clone)]
421struct FindResult {
422 shape: Vec<usize>,
423 indices: Vec<usize>,
424 values: FindValues,
425}
426
427#[derive(Clone)]
428enum FindValues {
429 Real(Vec<f64>),
430 Integer(IntegerStorage),
431 Complex(Vec<(f64, f64)>),
432}
433
434pub struct FindEval {
435 inner: FindEvalInner,
436}
437
438enum FindEvalInner {
439 Host {
440 result: FindResult,
441 prefer_gpu: bool,
442 },
443 Gpu {
444 result: ProviderFindResult,
445 },
446}
447
448impl FindEval {
449 fn from_host(result: FindResult, prefer_gpu: bool) -> Self {
450 Self {
451 inner: FindEvalInner::Host { result, prefer_gpu },
452 }
453 }
454
455 fn from_gpu(result: ProviderFindResult) -> Self {
456 Self {
457 inner: FindEvalInner::Gpu { result },
458 }
459 }
460
461 pub fn linear_value(&self) -> crate::BuiltinResult<Value> {
462 match &self.inner {
463 FindEvalInner::Host { result, prefer_gpu } => {
464 let tensor = result.linear_tensor()?;
465 Ok(tensor_to_value(tensor, *prefer_gpu))
466 }
467 FindEvalInner::Gpu { result } => Ok(Value::GpuTensor(result.linear.clone())),
468 }
469 }
470
471 pub fn row_value(&self) -> crate::BuiltinResult<Value> {
472 match &self.inner {
473 FindEvalInner::Host { result, prefer_gpu } => {
474 let tensor = result.row_tensor()?;
475 Ok(tensor_to_value(tensor, *prefer_gpu))
476 }
477 FindEvalInner::Gpu { result } => Ok(Value::GpuTensor(result.rows.clone())),
478 }
479 }
480
481 pub fn column_value(&self) -> crate::BuiltinResult<Value> {
482 match &self.inner {
483 FindEvalInner::Host { result, prefer_gpu } => {
484 let tensor = result.column_tensor()?;
485 Ok(tensor_to_value(tensor, *prefer_gpu))
486 }
487 FindEvalInner::Gpu { result } => Ok(Value::GpuTensor(result.cols.clone())),
488 }
489 }
490
491 pub fn values_value(&self) -> crate::BuiltinResult<Value> {
492 match &self.inner {
493 FindEvalInner::Host { result, prefer_gpu } => result.values_value(*prefer_gpu),
494 FindEvalInner::Gpu { result } => result
495 .values
496 .as_ref()
497 .map(|handle| Value::GpuTensor(handle.clone()))
498 .ok_or_else(|| find_error(&FIND_ERROR_PROVIDER_OUTPUT)),
499 }
500 }
501}
502
503async fn parse_options(args: &[Value]) -> crate::BuiltinResult<FindOptions> {
504 parse_find_tokens(&crate::builtins::common::arg_tokens::tokens_from_values(
505 args,
506 ))
507}
508
509fn parse_limit_scalar(value: f64) -> crate::BuiltinResult<usize> {
510 if !value.is_finite() {
511 return Err(find_error_with_message(
512 "find: K must be a finite, non-negative integer",
513 &FIND_ERROR_INVALID_INPUT,
514 ));
515 }
516 let rounded = value.round();
517 if (rounded - value).abs() > f64::EPSILON {
518 return Err(find_error_with_message(
519 "find: K must be a finite, non-negative integer",
520 &FIND_ERROR_INVALID_INPUT,
521 ));
522 }
523 if rounded < 0.0 {
524 return Err(find_error_with_message(
525 "find: K must be >= 0",
526 &FIND_ERROR_INVALID_INPUT,
527 ));
528 }
529 Ok(rounded as usize)
530}
531
532async fn materialize_input(value: Value) -> crate::BuiltinResult<(DataStorage, bool)> {
533 match value {
534 Value::GpuTensor(handle) => {
535 let tensor = gpu_helpers::gather_tensor_async(&handle).await?;
536 Ok((DataStorage::Real(tensor), true))
537 }
538 Value::Tensor(tensor) => Ok((DataStorage::Real(tensor), false)),
539 Value::SparseTensor(sparse) => Ok((
540 DataStorage::Real(sparse.to_dense().map_err(|e| {
541 find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL)
542 })?),
543 false,
544 )),
545 Value::LogicalArray(logical) => {
546 let tensor = tensor::logical_to_tensor(&logical)
547 .map_err(|message| find_error_with_message(message, &FIND_ERROR_INTERNAL))?;
548 Ok((DataStorage::Real(tensor), false))
549 }
550 Value::Num(n) => {
551 let tensor = Tensor::new(vec![n], vec![1, 1])
552 .map_err(|e| find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL))?;
553 Ok((DataStorage::Real(tensor), false))
554 }
555 Value::Int(i) => {
556 let tensor = Tensor::new_integer(integer_storage_from_scalar(&i), vec![1, 1])
557 .map_err(|e| find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL))?;
558 Ok((DataStorage::Real(tensor), false))
559 }
560 Value::Bool(b) => {
561 let tensor = Tensor::new(vec![if b { 1.0 } else { 0.0 }], vec![1, 1])
562 .map_err(|e| find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL))?;
563 Ok((DataStorage::Real(tensor), false))
564 }
565 Value::Complex(re, im) => {
566 let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1])
567 .map_err(|e| find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL))?;
568 Ok((DataStorage::Complex(tensor), false))
569 }
570 Value::ComplexTensor(tensor) => Ok((DataStorage::Complex(tensor), false)),
571 Value::CharArray(chars) => {
572 let mut data = Vec::with_capacity(chars.data.len());
573 for c in 0..chars.cols {
574 for r in 0..chars.rows {
575 let ch = chars.data[r * chars.cols + c] as u32;
576 data.push(ch as f64);
577 }
578 }
579 let tensor = Tensor::new(data, vec![chars.rows, chars.cols])
580 .map_err(|e| find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL))?;
581 Ok((DataStorage::Real(tensor), false))
582 }
583 other => Err(find_error_with_message(
584 format!(
585 "find: unsupported input type {:?}; expected numeric, logical, or char data",
586 other
587 ),
588 &FIND_ERROR_INVALID_INPUT,
589 )),
590 }
591}
592
593fn compute_find(storage: &DataStorage, options: &FindOptions) -> FindResult {
594 let shape = storage.shape().to_vec();
595 let limit = options.effective_limit();
596
597 match storage {
598 DataStorage::Real(tensor) => {
599 let mut indices = Vec::new();
600
601 if matches!(limit, Some(0)) {
602 return FindResult::new(shape, indices, find_values_for_tensor(tensor, &[]));
603 }
604
605 let len = tensor.data.len();
606 match options.direction {
607 FindDirection::First => {
608 for idx in 0..len {
609 let value = tensor.data[idx];
610 if value != 0.0 {
611 indices.push(idx + 1);
612 if limit.is_some_and(|k| indices.len() >= k) {
613 break;
614 }
615 }
616 }
617 }
618 FindDirection::Last => {
619 for idx in (0..len).rev() {
620 let value = tensor.data[idx];
621 if value != 0.0 {
622 indices.push(idx + 1);
623 if limit.is_some_and(|k| indices.len() >= k) {
624 break;
625 }
626 }
627 }
628 }
629 }
630
631 let values = find_values_for_tensor(tensor, &indices);
632 FindResult::new(shape, indices, values)
633 }
634 DataStorage::Complex(tensor) => {
635 let mut indices = Vec::new();
636 let mut values = Vec::new();
637
638 if matches!(limit, Some(0)) {
639 return FindResult::new(shape, indices, FindValues::Complex(values));
640 }
641
642 let len = tensor.data.len();
643 match options.direction {
644 FindDirection::First => {
645 for idx in 0..len {
646 let (re, im) = tensor.data[idx];
647 if re != 0.0 || im != 0.0 {
648 indices.push(idx + 1);
649 values.push((re, im));
650 if limit.is_some_and(|k| indices.len() >= k) {
651 break;
652 }
653 }
654 }
655 }
656 FindDirection::Last => {
657 for idx in (0..len).rev() {
658 let (re, im) = tensor.data[idx];
659 if re != 0.0 || im != 0.0 {
660 indices.push(idx + 1);
661 values.push((re, im));
662 if limit.is_some_and(|k| indices.len() >= k) {
663 break;
664 }
665 }
666 }
667 }
668 }
669
670 FindResult::new(shape, indices, FindValues::Complex(values))
671 }
672 }
673}
674
675fn compute_find_sparse(
676 sparse: &runmat_builtins::SparseTensor,
677 options: &FindOptions,
678) -> FindResult {
679 let shape = vec![sparse.rows, sparse.cols];
680 let limit = options.effective_limit();
681
682 let mut indices = Vec::new();
683 let mut values = Vec::new();
684
685 if matches!(limit, Some(0)) {
686 return FindResult::new(shape, indices, FindValues::Real(values));
687 }
688
689 match options.direction {
690 FindDirection::First => {
691 for col in 0..sparse.cols {
692 let col_start = sparse.col_ptrs[col];
693 let col_end = sparse.col_ptrs[col + 1];
694 for idx in col_start..col_end {
695 let row = sparse.row_indices[idx];
696 let value = sparse.values[idx];
697 if value != 0.0 {
698 let linear_idx = row + col * sparse.rows;
699 indices.push(linear_idx + 1);
700 values.push(value);
701 if limit.is_some_and(|k| indices.len() >= k) {
702 return FindResult::new(shape, indices, FindValues::Real(values));
703 }
704 }
705 }
706 }
707 }
708 FindDirection::Last => {
709 for col in (0..sparse.cols).rev() {
710 let col_start = sparse.col_ptrs[col];
711 let col_end = sparse.col_ptrs[col + 1];
712 for idx in (col_start..col_end).rev() {
713 let row = sparse.row_indices[idx];
714 let value = sparse.values[idx];
715 if value != 0.0 {
716 let linear_idx = row + col * sparse.rows;
717 indices.push(linear_idx + 1);
718 values.push(value);
719 if limit.is_some_and(|k| indices.len() >= k) {
720 return FindResult::new(shape, indices, FindValues::Real(values));
721 }
722 }
723 }
724 }
725 }
726 }
727
728 FindResult::new(shape, indices, FindValues::Real(values))
729}
730
731impl FindResult {
732 fn new(shape: Vec<usize>, indices: Vec<usize>, values: FindValues) -> Self {
733 Self {
734 shape,
735 indices,
736 values,
737 }
738 }
739
740 fn linear_tensor(&self) -> crate::BuiltinResult<Tensor> {
741 let data: Vec<f64> = self.indices.iter().map(|&idx| idx as f64).collect();
742 let rows = data.len();
743 Tensor::new(data, vec![rows, 1])
744 .map_err(|e| find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL))
745 }
746
747 fn row_tensor(&self) -> crate::BuiltinResult<Tensor> {
748 let mut data = Vec::with_capacity(self.indices.len());
749 let rows = self.shape.first().copied().unwrap_or(1).max(1);
750 for &idx in &self.indices {
751 let zero_based = idx - 1;
752 let row = (zero_based % rows) + 1;
753 data.push(row as f64);
754 }
755 Tensor::new(data, vec![self.indices.len(), 1])
756 .map_err(|e| find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL))
757 }
758
759 fn column_tensor(&self) -> crate::BuiltinResult<Tensor> {
760 let mut data = Vec::with_capacity(self.indices.len());
761 let rows = self.shape.first().copied().unwrap_or(1).max(1);
762 for &idx in &self.indices {
763 let zero_based = idx - 1;
764 let col = (zero_based / rows) + 1;
765 data.push(col as f64);
766 }
767 Tensor::new(data, vec![self.indices.len(), 1])
768 .map_err(|e| find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL))
769 }
770
771 fn values_value(&self, prefer_gpu: bool) -> crate::BuiltinResult<Value> {
772 match &self.values {
773 FindValues::Real(values) => {
774 let tensor = Tensor::new(values.clone(), vec![values.len(), 1]).map_err(|e| {
775 find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL)
776 })?;
777 Ok(tensor_to_value(tensor, prefer_gpu))
778 }
779 FindValues::Integer(values) => integer_values_to_value(values.clone()),
780 FindValues::Complex(values) => {
781 let tensor =
782 ComplexTensor::new(values.clone(), vec![values.len(), 1]).map_err(|e| {
783 find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL)
784 })?;
785 Ok(complex_tensor_into_value(tensor))
786 }
787 }
788 }
789}
790
791fn find_values_for_tensor(tensor: &Tensor, indices: &[usize]) -> FindValues {
792 let Some(storage) = tensor.integer_storage() else {
793 return FindValues::Real(indices.iter().map(|index| tensor.data[index - 1]).collect());
794 };
795 let selected: Vec<usize> = indices.iter().map(|index| index - 1).collect();
796 FindValues::Integer(select_integer_values(storage, &selected))
797}
798
799fn select_integer_values(storage: &IntegerStorage, indices: &[usize]) -> IntegerStorage {
800 macro_rules! select {
801 ($values:expr, $variant:ident) => {
802 IntegerStorage::$variant(indices.iter().map(|&index| $values[index]).collect())
803 };
804 }
805 match storage {
806 IntegerStorage::I8(values) => select!(values, I8),
807 IntegerStorage::I16(values) => select!(values, I16),
808 IntegerStorage::I32(values) => select!(values, I32),
809 IntegerStorage::I64(values) => select!(values, I64),
810 IntegerStorage::U8(values) => select!(values, U8),
811 IntegerStorage::U16(values) => select!(values, U16),
812 IntegerStorage::U32(values) => select!(values, U32),
813 IntegerStorage::U64(values) => select!(values, U64),
814 }
815}
816
817fn integer_values_to_value(storage: IntegerStorage) -> crate::BuiltinResult<Value> {
818 if storage.len() == 1 {
819 return Ok(Value::Int(integer_storage_value(&storage, 0)));
820 }
821 let shape = vec![storage.len(), 1];
822 let tensor = Tensor::new_integer(storage, shape)
823 .map_err(|e| find_error_with_message(format!("find: {e}"), &FIND_ERROR_INTERNAL))?;
824 Ok(Value::Tensor(tensor))
825}
826
827fn integer_storage_value(storage: &IntegerStorage, index: usize) -> IntValue {
828 match storage {
829 IntegerStorage::I8(values) => IntValue::I8(values[index]),
830 IntegerStorage::I16(values) => IntValue::I16(values[index]),
831 IntegerStorage::I32(values) => IntValue::I32(values[index]),
832 IntegerStorage::I64(values) => IntValue::I64(values[index]),
833 IntegerStorage::U8(values) => IntValue::U8(values[index]),
834 IntegerStorage::U16(values) => IntValue::U16(values[index]),
835 IntegerStorage::U32(values) => IntValue::U32(values[index]),
836 IntegerStorage::U64(values) => IntValue::U64(values[index]),
837 }
838}
839
840fn integer_storage_from_scalar(value: &IntValue) -> IntegerStorage {
841 match value {
842 IntValue::I8(value) => IntegerStorage::I8(vec![*value]),
843 IntValue::I16(value) => IntegerStorage::I16(vec![*value]),
844 IntValue::I32(value) => IntegerStorage::I32(vec![*value]),
845 IntValue::I64(value) => IntegerStorage::I64(vec![*value]),
846 IntValue::U8(value) => IntegerStorage::U8(vec![*value]),
847 IntValue::U16(value) => IntegerStorage::U16(vec![*value]),
848 IntValue::U32(value) => IntegerStorage::U32(vec![*value]),
849 IntValue::U64(value) => IntegerStorage::U64(vec![*value]),
850 }
851}
852
853fn tensor_to_value(tensor: Tensor, prefer_gpu: bool) -> Value {
854 if prefer_gpu {
855 if let Some(provider) = runmat_accelerate_api::provider() {
856 let view = HostTensorView {
857 data: &tensor.data,
858 shape: &tensor.shape,
859 };
860 if let Ok(handle) = provider.upload(&view) {
861 return Value::GpuTensor(handle);
862 }
863 }
864 }
865 tensor::tensor_into_value(tensor)
866}
867
868#[cfg(test)]
869pub(crate) mod tests {
870 use super::*;
871 use crate::builtins::common::test_support;
872 use futures::executor::block_on;
873 use runmat_builtins::{CharArray, IntValue, Type};
874
875 fn find_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
876 block_on(super::find_builtin(value, rest))
877 }
878
879 fn evaluate(value: Value, rest: &[Value]) -> crate::BuiltinResult<FindEval> {
880 block_on(super::evaluate(value, rest))
881 }
882
883 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
884 #[test]
885 fn find_linear_indices_basic() {
886 let tensor = Tensor::new(vec![0.0, 4.0, 0.0, 7.0, 0.0, 9.0], vec![2, 3]).unwrap();
887 let value = find_builtin(Value::Tensor(tensor), Vec::new()).expect("find");
888 match value {
889 Value::Tensor(t) => {
890 assert_eq!(t.shape, vec![3, 1]);
891 assert_eq!(t.data, vec![2.0, 4.0, 6.0]);
892 }
893 other => panic!("expected tensor, got {other:?}"),
894 }
895 }
896
897 #[test]
898 fn find_type_is_column_vector() {
899 assert_eq!(
900 find_type(
901 &[Type::Tensor { shape: None }],
902 &ResolveContext::new(Vec::new()),
903 ),
904 Type::Tensor {
905 shape: Some(vec![None, Some(1)])
906 }
907 );
908 }
909
910 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
911 #[test]
912 fn find_limited_first() {
913 let tensor = Tensor::new(vec![0.0, 3.0, 5.0, 0.0, 8.0], vec![1, 5]).unwrap();
914 let result =
915 find_builtin(Value::Tensor(tensor), vec![Value::Int(IntValue::I32(2))]).expect("find");
916 match result {
917 Value::Tensor(t) => {
918 assert_eq!(t.data, vec![2.0, 3.0]);
919 }
920 other => panic!("expected tensor, got {other:?}"),
921 }
922 }
923
924 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
925 #[test]
926 fn find_last_single() {
927 let tensor = Tensor::new(vec![1.0, 0.0, 0.0, 6.0, 0.0, 2.0], vec![1, 6]).unwrap();
928 let result = find_builtin(Value::Tensor(tensor), vec![Value::from("last")]).expect("find");
929 match result {
930 Value::Num(n) => assert_eq!(n, 6.0),
931 Value::Tensor(t) => {
932 assert_eq!(t.data, vec![6.0]);
933 }
934 other => panic!("unexpected result {other:?}"),
935 }
936 }
937
938 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
939 #[test]
940 fn find_complex_values() {
941 let tensor =
942 ComplexTensor::new(vec![(0.0, 0.0), (1.0, 2.0), (0.0, 0.0)], vec![3, 1]).unwrap();
943 let eval = evaluate(Value::ComplexTensor(tensor), &[]).expect("find compute");
944 let values = eval.values_value().expect("values");
945 match values {
946 Value::Complex(re, im) => {
947 assert_eq!(re, 1.0);
948 assert_eq!(im, 2.0);
949 }
950 Value::ComplexTensor(ct) => {
951 assert_eq!(ct.shape, vec![1, 1]);
952 assert_eq!(ct.data, vec![(1.0, 2.0)]);
953 }
954 other => panic!("expected complex result, got {other:?}"),
955 }
956 }
957
958 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
959 #[test]
960 fn find_gpu_roundtrip() {
961 test_support::with_test_provider(|provider| {
962 let tensor = Tensor::new(vec![0.0, 4.0, 0.0, 7.0], vec![2, 2]).unwrap();
963 let view = HostTensorView {
964 data: &tensor.data,
965 shape: &tensor.shape,
966 };
967 let handle = provider.upload(&view).expect("upload");
968 let result = find_builtin(Value::GpuTensor(handle), Vec::new()).expect("find");
969 let gathered = test_support::gather(result).expect("gather");
970 assert_eq!(gathered.shape, vec![2, 1]);
971 assert_eq!(gathered.data, vec![2.0, 4.0]);
972 });
973 }
974
975 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
976 #[test]
977 fn find_direction_error() {
978 let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
979 let err = find_builtin(
980 Value::Tensor(tensor),
981 vec![Value::Int(IntValue::I32(1)), Value::from("invalid")],
982 )
983 .expect_err("expected error");
984 assert!(err.to_string().contains("direction"));
985 assert_eq!(err.identifier(), super::FIND_ERROR_INVALID_INPUT.identifier);
986 }
987
988 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
989 #[test]
990 fn find_multi_output_rows_cols_values() {
991 let tensor = Tensor::new(vec![0.0, 2.0, 3.0, 0.0, 0.0, 6.0], vec![2, 3]).unwrap();
992 let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
993
994 let rows = test_support::gather(eval.row_value().expect("rows")).expect("gather rows");
995 assert_eq!(rows.shape, vec![3, 1]);
996 assert_eq!(rows.data, vec![2.0, 1.0, 2.0]);
997
998 let cols = test_support::gather(eval.column_value().expect("cols")).expect("gather cols");
999 assert_eq!(cols.shape, vec![3, 1]);
1000 assert_eq!(cols.data, vec![1.0, 2.0, 3.0]);
1001
1002 let vals = test_support::gather(eval.values_value().expect("vals")).expect("gather vals");
1003 assert_eq!(vals.shape, vec![3, 1]);
1004 assert_eq!(vals.data, vec![2.0, 3.0, 6.0]);
1005 }
1006
1007 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1008 #[test]
1009 fn find_values_preserve_exact_uint64_storage() {
1010 let input = Tensor::new_integer(
1011 IntegerStorage::U64(vec![0, u64::MAX, 1_u64 << 63, 0]),
1012 vec![2, 2],
1013 )
1014 .expect("integer tensor");
1015 let eval = evaluate(Value::Tensor(input), &[]).expect("evaluate");
1016 let values = eval.values_value().expect("values");
1017 let Value::Tensor(values) = values else {
1018 panic!("expected typed tensor values");
1019 };
1020 assert_eq!(values.shape, vec![2, 1]);
1021 assert_eq!(
1022 values.integer_storage(),
1023 Some(&IntegerStorage::U64(vec![u64::MAX, 1_u64 << 63]))
1024 );
1025 }
1026
1027 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1028 #[test]
1029 fn find_single_integer_value_preserves_scalar_class() {
1030 let input = Tensor::new_integer(IntegerStorage::I64(vec![0, i64::MIN]), vec![2, 1])
1031 .expect("integer tensor");
1032 let eval = evaluate(Value::Tensor(input), &[]).expect("evaluate");
1033 assert_eq!(
1034 eval.values_value().expect("values"),
1035 Value::Int(IntValue::I64(i64::MIN))
1036 );
1037 }
1038
1039 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1040 #[test]
1041 fn find_integer_scalar_preserves_exact_value_output() {
1042 let eval = evaluate(Value::Int(IntValue::U64(u64::MAX)), &[]).expect("evaluate");
1043 assert_eq!(
1044 eval.values_value().expect("values"),
1045 Value::Int(IntValue::U64(u64::MAX))
1046 );
1047 }
1048
1049 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1050 #[test]
1051 fn find_last_order_descending() {
1052 let tensor = Tensor::new(vec![1.0, 0.0, 2.0, 3.0, 0.0], vec![1, 5]).unwrap();
1053 let result = find_builtin(
1054 Value::Tensor(tensor),
1055 vec![Value::Int(IntValue::I32(2)), Value::from("last")],
1056 )
1057 .expect("find");
1058 match result {
1059 Value::Tensor(t) => {
1060 assert_eq!(t.shape, vec![2, 1]);
1061 assert_eq!(t.data, vec![4.0, 3.0]);
1062 }
1063 Value::Num(_) => panic!("expected column vector"),
1064 other => panic!("unexpected result {other:?}"),
1065 }
1066 }
1067
1068 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1069 #[test]
1070 fn find_limit_zero_returns_empty() {
1071 let tensor = Tensor::new(vec![1.0, 0.0, 3.0], vec![3, 1]).unwrap();
1072 let result = find_builtin(Value::Tensor(tensor), vec![Value::Num(0.0)]).expect("find");
1073 match result {
1074 Value::Tensor(t) => {
1075 assert_eq!(t.shape, vec![0, 1]);
1076 assert!(t.data.is_empty());
1077 }
1078 other => panic!("expected empty tensor, got {other:?}"),
1079 }
1080 }
1081
1082 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1083 #[test]
1084 fn find_char_array_supports_nonzero_codes() {
1085 let chars = CharArray::new(vec!['\0', 'A', '\0'], 1, 3).unwrap();
1086 let result = find_builtin(Value::CharArray(chars), Vec::new()).expect("find");
1087 match result {
1088 Value::Num(n) => assert_eq!(n, 2.0),
1089 Value::Tensor(t) => assert_eq!(t.data, vec![2.0]),
1090 other => panic!("unexpected result {other:?}"),
1091 }
1092 }
1093
1094 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1095 #[test]
1096 fn find_gpu_multi_outputs_return_gpu_handles() {
1097 test_support::with_test_provider(|provider| {
1098 let tensor = Tensor::new(vec![0.0, 4.0, 5.0, 0.0], vec![2, 2]).unwrap();
1099 let view = HostTensorView {
1100 data: &tensor.data,
1101 shape: &tensor.shape,
1102 };
1103 let handle = provider.upload(&view).expect("upload");
1104 let eval = evaluate(Value::GpuTensor(handle), &[]).expect("evaluate");
1105
1106 let rows = eval.row_value().expect("rows");
1107 assert!(matches!(rows, Value::GpuTensor(_)));
1108 let rows_host = test_support::gather(rows).expect("gather rows");
1109 assert_eq!(rows_host.data, vec![2.0, 1.0]);
1110
1111 let cols = eval.column_value().expect("cols");
1112 assert!(matches!(cols, Value::GpuTensor(_)));
1113 let cols_host = test_support::gather(cols).expect("gather cols");
1114 assert_eq!(cols_host.data, vec![1.0, 2.0]);
1115
1116 let vals = eval.values_value().expect("vals");
1117 assert!(matches!(vals, Value::GpuTensor(_)));
1118 let vals_host = test_support::gather(vals).expect("gather vals");
1119 assert_eq!(vals_host.data, vec![4.0, 5.0]);
1120 });
1121 }
1122
1123 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1124 #[test]
1125 #[cfg(feature = "wgpu")]
1126 fn find_wgpu_matches_cpu() {
1127 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1128 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1129 );
1130 let tensor = Tensor::new(vec![0.0, 2.0, 0.0, 3.0, 4.0, 0.0], vec![3, 2]).unwrap();
1131 let cpu_eval = evaluate(Value::Tensor(tensor.clone()), &[]).expect("cpu evaluate");
1132 let cpu_linear =
1133 test_support::gather(cpu_eval.linear_value().expect("cpu linear")).expect("cpu gather");
1134 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1135 let view = HostTensorView {
1136 data: &tensor.data,
1137 shape: &tensor.shape,
1138 };
1139 let handle = provider.upload(&view).expect("upload");
1140 let gpu_eval = evaluate(Value::GpuTensor(handle), &[]).expect("gpu evaluate");
1141 let gpu_linear =
1142 test_support::gather(gpu_eval.linear_value().expect("gpu linear")).expect("gpu gather");
1143 assert_eq!(gpu_linear.data, cpu_linear.data);
1144 }
1145}