Skip to main content

runmat_runtime/builtins/array/indexing/
find.rs

1//! MATLAB-compatible `find` builtin with GPU-aware semantics for RunMat.
2
3use 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
338/// Evaluate `find` and return an object that can materialise the various outputs.
339pub 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}