Skip to main content

runmat_runtime/builtins/math/reduction/
max.rs

1//! MATLAB-compatible `max` builtin with GPU-aware semantics for RunMat.
2
3use std::cmp::Ordering;
4use std::collections::BTreeSet;
5
6use runmat_accelerate_api::{AccelProvider, GpuTensorHandle, ReduceDimResult};
7use runmat_builtins::{
8    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
9    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
10    ComplexTensor, ResolveContext, Tensor, Type, Value,
11};
12use runmat_macros::runtime_builtin;
13
14use crate::{build_runtime_error, BuiltinResult, RuntimeError};
15
16const NAME: &str = "max";
17
18fn max_type(args: &[Type], ctx: &ResolveContext) -> Type {
19    min_max_type(args, ctx)
20}
21
22fn nanmax_type(args: &[Type], ctx: &ResolveContext) -> Type {
23    min_max_type(args, ctx)
24}
25
26const MAX_OUTPUT_M: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
27    name: "M",
28    ty: BuiltinParamType::NumericArray,
29    arity: BuiltinParamArity::Required,
30    default: None,
31    description: "Maximum values.",
32}];
33
34const MAX_OUTPUT_MI: [BuiltinParamDescriptor; 2] = [
35    BuiltinParamDescriptor {
36        name: "M",
37        ty: BuiltinParamType::NumericArray,
38        arity: BuiltinParamArity::Required,
39        default: None,
40        description: "Maximum values.",
41    },
42    BuiltinParamDescriptor {
43        name: "I",
44        ty: BuiltinParamType::NumericArray,
45        arity: BuiltinParamArity::Required,
46        default: None,
47        description: "One-based maximum indices/origins.",
48    },
49];
50
51const MAX_PARAM_A: BuiltinParamDescriptor = BuiltinParamDescriptor {
52    name: "A",
53    ty: BuiltinParamType::Any,
54    arity: BuiltinParamArity::Required,
55    default: None,
56    description: "Input scalar or array.",
57};
58
59const MAX_PARAM_B: BuiltinParamDescriptor = BuiltinParamDescriptor {
60    name: "B",
61    ty: BuiltinParamType::Any,
62    arity: BuiltinParamArity::Required,
63    default: None,
64    description: "Second operand for element-wise maximum.",
65};
66
67const MAX_PARAM_EMPTY: BuiltinParamDescriptor = BuiltinParamDescriptor {
68    name: "placeholder",
69    ty: BuiltinParamType::Any,
70    arity: BuiltinParamArity::Optional,
71    default: Some("[]"),
72    description: "Empty placeholder selecting reduction-argument grammar.",
73};
74
75const MAX_PARAM_DIM: BuiltinParamDescriptor = BuiltinParamDescriptor {
76    name: "dim",
77    ty: BuiltinParamType::Any,
78    arity: BuiltinParamArity::Optional,
79    default: Some("[]"),
80    description: "Reduction dimension selector (scalar or dimension vector).",
81};
82
83const MAX_PARAM_REDUCTION_FLAG: BuiltinParamDescriptor = BuiltinParamDescriptor {
84    name: "flag",
85    ty: BuiltinParamType::StringScalar,
86    arity: BuiltinParamArity::Optional,
87    default: Some("\"all\""),
88    description: "Reduction mode flag: \"all\" or \"linear\".",
89};
90
91const MAX_PARAM_NANFLAG: BuiltinParamDescriptor = BuiltinParamDescriptor {
92    name: "nanflag",
93    ty: BuiltinParamType::StringScalar,
94    arity: BuiltinParamArity::Optional,
95    default: Some("\"includenan\""),
96    description: "Missing-value mode: \"includenan\" or \"omitnan\".",
97};
98
99const MAX_PARAM_COMPARISON_NAME: BuiltinParamDescriptor = BuiltinParamDescriptor {
100    name: "optionName",
101    ty: BuiltinParamType::StringScalar,
102    arity: BuiltinParamArity::Optional,
103    default: Some("\"ComparisonMethod\""),
104    description: "Option name (currently \"ComparisonMethod\").",
105};
106
107const MAX_PARAM_COMPARISON_VALUE: BuiltinParamDescriptor = BuiltinParamDescriptor {
108    name: "method",
109    ty: BuiltinParamType::StringScalar,
110    arity: BuiltinParamArity::Optional,
111    default: Some("\"auto\""),
112    description: "Comparison method: \"auto\", \"abs\"/\"magnitude\", or \"real\".",
113};
114
115const MAX_PARAM_OPTION_NAME: BuiltinParamDescriptor = BuiltinParamDescriptor {
116    name: "optionName",
117    ty: BuiltinParamType::StringScalar,
118    arity: BuiltinParamArity::Variadic,
119    default: None,
120    description: "Name-value option name.",
121};
122
123const MAX_PARAM_OPTION_VALUE: BuiltinParamDescriptor = BuiltinParamDescriptor {
124    name: "optionValue",
125    ty: BuiltinParamType::Any,
126    arity: BuiltinParamArity::Variadic,
127    default: None,
128    description: "Name-value option value.",
129};
130
131const MAX_INPUTS_A: [BuiltinParamDescriptor; 1] = [MAX_PARAM_A];
132const MAX_INPUTS_A_B: [BuiltinParamDescriptor; 2] = [MAX_PARAM_A, MAX_PARAM_B];
133const MAX_INPUTS_A_EMPTY_DIM: [BuiltinParamDescriptor; 3] =
134    [MAX_PARAM_A, MAX_PARAM_EMPTY, MAX_PARAM_DIM];
135const MAX_INPUTS_A_EMPTY_FLAG: [BuiltinParamDescriptor; 3] =
136    [MAX_PARAM_A, MAX_PARAM_EMPTY, MAX_PARAM_REDUCTION_FLAG];
137const MAX_INPUTS_A_EMPTY_NANFLAG: [BuiltinParamDescriptor; 3] =
138    [MAX_PARAM_A, MAX_PARAM_EMPTY, MAX_PARAM_NANFLAG];
139const MAX_INPUTS_A_EMPTY_COMPARISON: [BuiltinParamDescriptor; 4] = [
140    MAX_PARAM_A,
141    MAX_PARAM_EMPTY,
142    MAX_PARAM_COMPARISON_NAME,
143    MAX_PARAM_COMPARISON_VALUE,
144];
145const MAX_INPUTS_A_B_COMPARISON: [BuiltinParamDescriptor; 4] = [
146    MAX_PARAM_A,
147    MAX_PARAM_B,
148    MAX_PARAM_COMPARISON_NAME,
149    MAX_PARAM_COMPARISON_VALUE,
150];
151const MAX_INPUTS_A_EMPTY_OPTIONS: [BuiltinParamDescriptor; 4] = [
152    MAX_PARAM_A,
153    MAX_PARAM_EMPTY,
154    MAX_PARAM_OPTION_NAME,
155    MAX_PARAM_OPTION_VALUE,
156];
157const MAX_INPUTS_A_B_OPTIONS: [BuiltinParamDescriptor; 4] = [
158    MAX_PARAM_A,
159    MAX_PARAM_B,
160    MAX_PARAM_OPTION_NAME,
161    MAX_PARAM_OPTION_VALUE,
162];
163
164const NANMAX_INPUTS_A: [BuiltinParamDescriptor; 1] = [MAX_PARAM_A];
165const NANMAX_INPUTS_A_B: [BuiltinParamDescriptor; 2] = [MAX_PARAM_A, MAX_PARAM_B];
166const NANMAX_INPUTS_A_EMPTY_DIM: [BuiltinParamDescriptor; 3] =
167    [MAX_PARAM_A, MAX_PARAM_EMPTY, MAX_PARAM_DIM];
168
169const NANMAX_SIGNATURES: [BuiltinSignatureDescriptor; 16] = [
170    BuiltinSignatureDescriptor {
171        label: "M = nanmax(A)",
172        inputs: &NANMAX_INPUTS_A,
173        outputs: &MAX_OUTPUT_M,
174    },
175    BuiltinSignatureDescriptor {
176        label: "[M, I] = nanmax(A)",
177        inputs: &NANMAX_INPUTS_A,
178        outputs: &MAX_OUTPUT_MI,
179    },
180    BuiltinSignatureDescriptor {
181        label: "M = nanmax(A, B)",
182        inputs: &NANMAX_INPUTS_A_B,
183        outputs: &MAX_OUTPUT_M,
184    },
185    BuiltinSignatureDescriptor {
186        label: "[M, I] = nanmax(A, B)",
187        inputs: &NANMAX_INPUTS_A_B,
188        outputs: &MAX_OUTPUT_MI,
189    },
190    BuiltinSignatureDescriptor {
191        label: "M = nanmax(A, [], dim)",
192        inputs: &NANMAX_INPUTS_A_EMPTY_DIM,
193        outputs: &MAX_OUTPUT_M,
194    },
195    BuiltinSignatureDescriptor {
196        label: "[M, I] = nanmax(A, [], dim)",
197        inputs: &NANMAX_INPUTS_A_EMPTY_DIM,
198        outputs: &MAX_OUTPUT_MI,
199    },
200    BuiltinSignatureDescriptor {
201        label: "M = nanmax(A, [], vecdim)",
202        inputs: &NANMAX_INPUTS_A_EMPTY_DIM,
203        outputs: &MAX_OUTPUT_M,
204    },
205    BuiltinSignatureDescriptor {
206        label: "[M, I] = nanmax(A, [], vecdim)",
207        inputs: &NANMAX_INPUTS_A_EMPTY_DIM,
208        outputs: &MAX_OUTPUT_MI,
209    },
210    BuiltinSignatureDescriptor {
211        label: "M = nanmax(A, [], \"all\")",
212        inputs: &MAX_INPUTS_A_EMPTY_FLAG,
213        outputs: &MAX_OUTPUT_M,
214    },
215    BuiltinSignatureDescriptor {
216        label: "[M, I] = nanmax(A, [], \"all\")",
217        inputs: &MAX_INPUTS_A_EMPTY_FLAG,
218        outputs: &MAX_OUTPUT_MI,
219    },
220    BuiltinSignatureDescriptor {
221        label: "M = nanmax(A, [], \"linear\")",
222        inputs: &MAX_INPUTS_A_EMPTY_FLAG,
223        outputs: &MAX_OUTPUT_M,
224    },
225    BuiltinSignatureDescriptor {
226        label: "[M, I] = nanmax(A, [], \"linear\")",
227        inputs: &MAX_INPUTS_A_EMPTY_FLAG,
228        outputs: &MAX_OUTPUT_MI,
229    },
230    BuiltinSignatureDescriptor {
231        label: "M = nanmax(A, [], \"ComparisonMethod\", method)",
232        inputs: &MAX_INPUTS_A_EMPTY_COMPARISON,
233        outputs: &MAX_OUTPUT_M,
234    },
235    BuiltinSignatureDescriptor {
236        label: "[M, I] = nanmax(A, [], \"ComparisonMethod\", method)",
237        inputs: &MAX_INPUTS_A_EMPTY_COMPARISON,
238        outputs: &MAX_OUTPUT_MI,
239    },
240    BuiltinSignatureDescriptor {
241        label: "M = nanmax(A, B, \"ComparisonMethod\", method)",
242        inputs: &MAX_INPUTS_A_B_COMPARISON,
243        outputs: &MAX_OUTPUT_M,
244    },
245    BuiltinSignatureDescriptor {
246        label: "[M, I] = nanmax(A, B, \"ComparisonMethod\", method)",
247        inputs: &MAX_INPUTS_A_B_COMPARISON,
248        outputs: &MAX_OUTPUT_MI,
249    },
250];
251
252const MAX_SIGNATURES: [BuiltinSignatureDescriptor; 22] = [
253    BuiltinSignatureDescriptor {
254        label: "M = max(A)",
255        inputs: &MAX_INPUTS_A,
256        outputs: &MAX_OUTPUT_M,
257    },
258    BuiltinSignatureDescriptor {
259        label: "[M, I] = max(A)",
260        inputs: &MAX_INPUTS_A,
261        outputs: &MAX_OUTPUT_MI,
262    },
263    BuiltinSignatureDescriptor {
264        label: "M = max(A, B)",
265        inputs: &MAX_INPUTS_A_B,
266        outputs: &MAX_OUTPUT_M,
267    },
268    BuiltinSignatureDescriptor {
269        label: "[M, I] = max(A, B)",
270        inputs: &MAX_INPUTS_A_B,
271        outputs: &MAX_OUTPUT_MI,
272    },
273    BuiltinSignatureDescriptor {
274        label: "M = max(A, [], dim)",
275        inputs: &MAX_INPUTS_A_EMPTY_DIM,
276        outputs: &MAX_OUTPUT_M,
277    },
278    BuiltinSignatureDescriptor {
279        label: "[M, I] = max(A, [], dim)",
280        inputs: &MAX_INPUTS_A_EMPTY_DIM,
281        outputs: &MAX_OUTPUT_MI,
282    },
283    BuiltinSignatureDescriptor {
284        label: "M = max(A, [], vecdim)",
285        inputs: &MAX_INPUTS_A_EMPTY_DIM,
286        outputs: &MAX_OUTPUT_M,
287    },
288    BuiltinSignatureDescriptor {
289        label: "[M, I] = max(A, [], vecdim)",
290        inputs: &MAX_INPUTS_A_EMPTY_DIM,
291        outputs: &MAX_OUTPUT_MI,
292    },
293    BuiltinSignatureDescriptor {
294        label: "M = max(A, [], \"all\")",
295        inputs: &MAX_INPUTS_A_EMPTY_FLAG,
296        outputs: &MAX_OUTPUT_M,
297    },
298    BuiltinSignatureDescriptor {
299        label: "[M, I] = max(A, [], \"all\")",
300        inputs: &MAX_INPUTS_A_EMPTY_FLAG,
301        outputs: &MAX_OUTPUT_MI,
302    },
303    BuiltinSignatureDescriptor {
304        label: "M = max(A, [], \"linear\")",
305        inputs: &MAX_INPUTS_A_EMPTY_FLAG,
306        outputs: &MAX_OUTPUT_M,
307    },
308    BuiltinSignatureDescriptor {
309        label: "[M, I] = max(A, [], \"linear\")",
310        inputs: &MAX_INPUTS_A_EMPTY_FLAG,
311        outputs: &MAX_OUTPUT_MI,
312    },
313    BuiltinSignatureDescriptor {
314        label: "M = max(A, [], nanflag)",
315        inputs: &MAX_INPUTS_A_EMPTY_NANFLAG,
316        outputs: &MAX_OUTPUT_M,
317    },
318    BuiltinSignatureDescriptor {
319        label: "[M, I] = max(A, [], nanflag)",
320        inputs: &MAX_INPUTS_A_EMPTY_NANFLAG,
321        outputs: &MAX_OUTPUT_MI,
322    },
323    BuiltinSignatureDescriptor {
324        label: "M = max(A, [], \"ComparisonMethod\", method)",
325        inputs: &MAX_INPUTS_A_EMPTY_COMPARISON,
326        outputs: &MAX_OUTPUT_M,
327    },
328    BuiltinSignatureDescriptor {
329        label: "[M, I] = max(A, [], \"ComparisonMethod\", method)",
330        inputs: &MAX_INPUTS_A_EMPTY_COMPARISON,
331        outputs: &MAX_OUTPUT_MI,
332    },
333    BuiltinSignatureDescriptor {
334        label: "M = max(A, B, \"ComparisonMethod\", method)",
335        inputs: &MAX_INPUTS_A_B_COMPARISON,
336        outputs: &MAX_OUTPUT_M,
337    },
338    BuiltinSignatureDescriptor {
339        label: "[M, I] = max(A, B, \"ComparisonMethod\", method)",
340        inputs: &MAX_INPUTS_A_B_COMPARISON,
341        outputs: &MAX_OUTPUT_MI,
342    },
343    BuiltinSignatureDescriptor {
344        label: "M = max(A, [], optionName, optionValue, ...)",
345        inputs: &MAX_INPUTS_A_EMPTY_OPTIONS,
346        outputs: &MAX_OUTPUT_M,
347    },
348    BuiltinSignatureDescriptor {
349        label: "[M, I] = max(A, [], optionName, optionValue, ...)",
350        inputs: &MAX_INPUTS_A_EMPTY_OPTIONS,
351        outputs: &MAX_OUTPUT_MI,
352    },
353    BuiltinSignatureDescriptor {
354        label: "M = max(A, B, optionName, optionValue, ...)",
355        inputs: &MAX_INPUTS_A_B_OPTIONS,
356        outputs: &MAX_OUTPUT_M,
357    },
358    BuiltinSignatureDescriptor {
359        label: "[M, I] = max(A, B, optionName, optionValue, ...)",
360        inputs: &MAX_INPUTS_A_B_OPTIONS,
361        outputs: &MAX_OUTPUT_MI,
362    },
363];
364
365const MAX_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
366    code: "RM.MAX.INVALID_ARGUMENT",
367    identifier: Some("RunMat:max:InvalidArgument"),
368    when: "Argument grammar, dimensions, or option names/values are invalid.",
369    message: "max: invalid argument",
370};
371
372const MAX_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
373    code: "RM.MAX.INVALID_INPUT",
374    identifier: Some("RunMat:max:InvalidInput"),
375    when: "Input values cannot be converted to supported max domains.",
376    message: "max: invalid input",
377};
378
379const MAX_ERROR_SIZE_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
380    code: "RM.MAX.SIZE_MISMATCH",
381    identifier: Some("RunMat:max:SizeMismatch"),
382    when: "Element-wise operands are not broadcast-compatible.",
383    message: "max: size mismatch",
384};
385
386const MAX_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
387    code: "RM.MAX.INTERNAL",
388    identifier: Some("RunMat:max:Internal"),
389    when: "Execution fails due to gather, provider, allocation, or conversion internals.",
390    message: "max: internal failure",
391};
392
393const MAX_ERRORS: [BuiltinErrorDescriptor; 4] = [
394    MAX_ERROR_INVALID_ARGUMENT,
395    MAX_ERROR_INVALID_INPUT,
396    MAX_ERROR_SIZE_MISMATCH,
397    MAX_ERROR_INTERNAL,
398];
399
400pub const MAX_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
401    signatures: &MAX_SIGNATURES,
402    output_mode: BuiltinOutputMode::ByRequestedOutputCount,
403    completion_policy: BuiltinCompletionPolicy::Public,
404    errors: &MAX_ERRORS,
405};
406
407pub const NANMAX_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
408    signatures: &NANMAX_SIGNATURES,
409    output_mode: BuiltinOutputMode::ByRequestedOutputCount,
410    completion_policy: BuiltinCompletionPolicy::Public,
411    errors: &MAX_ERRORS,
412};
413
414fn max_descriptor_error_with_message(
415    message: impl Into<String>,
416    error: &'static BuiltinErrorDescriptor,
417) -> RuntimeError {
418    let mut builder = build_runtime_error(message).with_builtin(NAME);
419    if let Some(identifier) = error.identifier {
420        builder = builder.with_identifier(identifier);
421    }
422    builder.build()
423}
424
425fn max_descriptor_error_with_detail(
426    error: &'static BuiltinErrorDescriptor,
427    detail: impl AsRef<str>,
428) -> RuntimeError {
429    max_descriptor_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
430}
431
432fn max_invalid_argument(detail: impl AsRef<str>) -> RuntimeError {
433    max_descriptor_error_with_detail(&MAX_ERROR_INVALID_ARGUMENT, detail)
434}
435
436fn max_invalid_input(detail: impl AsRef<str>) -> RuntimeError {
437    max_descriptor_error_with_detail(&MAX_ERROR_INVALID_INPUT, detail)
438}
439
440fn max_size_mismatch(detail: impl AsRef<str>) -> RuntimeError {
441    max_descriptor_error_with_detail(&MAX_ERROR_SIZE_MISMATCH, detail)
442}
443
444fn max_internal_error(detail: impl AsRef<str>) -> RuntimeError {
445    max_descriptor_error_with_detail(&MAX_ERROR_INTERNAL, detail)
446}
447
448use crate::builtins::common::arg_tokens::tokens_from_values;
449use crate::builtins::common::broadcast::BroadcastPlan;
450use crate::builtins::common::random_args::{complex_tensor_into_value, keyword_of};
451use crate::builtins::common::spec::{
452    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, FusionError,
453    FusionExprContext, FusionKernelTemplate, GpuOpKind, ProviderHook, ReductionNaN,
454    ResidencyPolicy, ScalarType, ShapeRequirements,
455};
456use crate::builtins::common::{
457    gpu_helpers,
458    shape::{is_scalar_shape, normalize_scalar_shape},
459    tensor,
460};
461use crate::builtins::math::reduction::type_resolvers::min_max_type;
462
463#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::reduction::max")]
464pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
465    name: "max",
466    op_kind: GpuOpKind::Reduction,
467    supported_precisions: &[ScalarType::F32, ScalarType::F64],
468    broadcast: BroadcastSemantics::Matlab,
469    provider_hooks: &[
470        ProviderHook::Reduction {
471            name: "reduce_max_dim",
472        },
473        ProviderHook::Reduction {
474            name: "reduce_max",
475        },
476    ],
477    constant_strategy: ConstantStrategy::InlineLiteral,
478    residency: ResidencyPolicy::NewHandle,
479    nan_mode: ReductionNaN::Include,
480    two_pass_threshold: Some(256),
481    workgroup_size: Some(256),
482    accepts_nan_mode: false,
483    notes:
484        "Providers should implement reduce_max_dim / reduce_max. Requests that require omitnan, comparisonmethod overrides, or complex inputs fall back to the host implementation.",
485};
486
487#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::reduction::max")]
488pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
489    name: "max",
490    shape: ShapeRequirements::BroadcastCompatible,
491    constant_strategy: ConstantStrategy::InlineLiteral,
492    elementwise: None,
493    reduction: Some(FusionKernelTemplate {
494        scalar_precisions: &[ScalarType::F32, ScalarType::F64],
495        wgsl_body: |ctx: &FusionExprContext| {
496            let input = ctx.inputs.first().ok_or(FusionError::MissingInput(0))?;
497            Ok(format!("accumulator = max(accumulator, {input});"))
498        },
499    }),
500    emits_nan: true,
501    notes: "Fusion planner emits canonical reduction kernels; providers may substitute custom WGSL via reduce_max_dim hooks.",
502};
503
504/// Evaluation artifact returned by `max` that carries both values and indices.
505#[derive(Debug, Clone)]
506pub struct MaxEvaluation {
507    values: Value,
508    indices: Value,
509}
510
511impl MaxEvaluation {
512    /// Consume the evaluation and return only the maximum values (single-output call).
513    pub fn into_value(self) -> Value {
514        self.values
515    }
516
517    /// Consume the evaluation and return both maxima and indices.
518    pub fn into_pair(self) -> (Value, Value) {
519        (self.values, self.indices)
520    }
521
522    /// Peek at the indices without consuming.
523    pub fn indices_value(&self) -> Value {
524        self.indices.clone()
525    }
526}
527
528#[runtime_builtin(
529    name = "max",
530    category = "math/reduction",
531    summary = "Return maximum elements along dimensions or pairwise comparisons.",
532    keywords = "max,maximum,reduction,gpu,comparisonmethod,omitnan",
533    accel = "reduction",
534    type_resolver(max_type),
535    descriptor(crate::builtins::math::reduction::max::MAX_DESCRIPTOR),
536    builtin_path = "crate::builtins::math::reduction::max"
537)]
538async fn max_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
539    if let Some(eval) = crate::builtins::table::categorical_max_evaluate(&value, &rest).await {
540        return crate::builtins::table::categorical_extrema_to_value(eval?);
541    }
542    let eval = evaluate(value, &rest).await?;
543    evaluation_to_value(eval)
544}
545
546#[runtime_builtin(
547    name = "nanmax",
548    category = "stats/summary",
549    summary = "Return maximum values while omitting NaNs.",
550    keywords = "nanmax,max,maximum,omitnan,statistics",
551    type_resolver(nanmax_type),
552    descriptor(crate::builtins::math::reduction::max::NANMAX_DESCRIPTOR),
553    builtin_path = "crate::builtins::math::reduction::max"
554)]
555async fn nanmax_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
556    let adjusted = nanmax_rest(rest);
557    let eval = evaluate(value, &adjusted).await?;
558    evaluation_to_value(eval)
559}
560
561fn evaluation_to_value(eval: MaxEvaluation) -> BuiltinResult<Value> {
562    if let Some(out_count) = crate::output_count::current_output_count() {
563        if out_count == 0 {
564            return Ok(Value::OutputList(Vec::new()));
565        }
566        if out_count == 1 {
567            return Ok(Value::OutputList(vec![eval.into_value()]));
568        }
569        let (values, indices) = eval.into_pair();
570        return Ok(crate::output_count::output_list_with_padding(
571            out_count,
572            vec![values, indices],
573        ));
574    }
575    Ok(eval.into_value())
576}
577
578fn nanmax_rest(rest: Vec<Value>) -> Vec<Value> {
579    let mut args = Vec::with_capacity(rest.len() + 2);
580    if rest.is_empty() {
581        args.push(empty_placeholder());
582    }
583    args.extend(rest);
584    args.push(Value::from("omitnan"));
585    args
586}
587
588fn empty_placeholder() -> Value {
589    Value::Tensor(Tensor::new(Vec::<f64>::new(), vec![0, 0]).expect("empty placeholder shape"))
590}
591
592/// Evaluate the builtin once and expose both outputs (value + indices).
593pub async fn evaluate(value: Value, rest: &[Value]) -> BuiltinResult<MaxEvaluation> {
594    let parsed = parse_call(rest).await?;
595    if std::env::var("RUNMAT_DEBUG_MAX").is_ok() {
596        let call_label = match &parsed {
597            ParsedCall::Reduction(_) => "reduction",
598            ParsedCall::Elementwise(_) => "elementwise",
599        };
600        let first_arg = rest.first().map(debug_value_kind).unwrap_or("None");
601        tracing::debug!(
602            call_type = call_label,
603            rest_len = rest.len(),
604            first_arg = first_arg,
605            "[runmat-debug-max]"
606        );
607    }
608    match parsed {
609        ParsedCall::Elementwise(args) => elementwise_max(value, args).await,
610        ParsedCall::Reduction(args) => reduction_max(value, args).await,
611    }
612}
613
614#[derive(Debug, Clone)]
615enum ParsedCall {
616    Reduction(ReductionArgs),
617    Elementwise(ElementwiseArgs),
618}
619
620#[derive(Debug, Clone)]
621struct ReductionArgs {
622    selection: DimSelection,
623    nan_mode: ReductionNaN,
624    comparison: ComparisonMethod,
625    linear_index: bool,
626}
627
628impl Default for ReductionArgs {
629    fn default() -> Self {
630        Self {
631            selection: DimSelection::Auto,
632            nan_mode: ReductionNaN::Include,
633            comparison: ComparisonMethod::Auto,
634            linear_index: false,
635        }
636    }
637}
638
639#[derive(Debug, Clone)]
640enum DimSelection {
641    Auto,
642    Dim(usize),
643    Vec(Vec<usize>),
644    All,
645}
646
647#[derive(Debug, Clone, Copy, PartialEq, Eq)]
648enum ComparisonMethod {
649    Auto,
650    Real,
651    Abs,
652}
653
654#[derive(Debug, Clone)]
655struct ElementwiseArgs {
656    other: Value,
657    nan_mode: ReductionNaN,
658    comparison: ComparisonMethod,
659}
660
661async fn parse_call(rest: &[Value]) -> BuiltinResult<ParsedCall> {
662    if rest.is_empty() {
663        return Ok(ParsedCall::Reduction(ReductionArgs::default()));
664    }
665
666    let first = &rest[0];
667    if !is_empty_placeholder(first) {
668        let (nan_mode, comparison) = parse_elementwise_options(&rest[1..])?;
669        return Ok(ParsedCall::Elementwise(ElementwiseArgs {
670            other: first.clone(),
671            nan_mode,
672            comparison,
673        }));
674    }
675
676    let mut args = ReductionArgs::default();
677    parse_reduction_options(&mut args, &rest[1..]).await?;
678    Ok(ParsedCall::Reduction(args))
679}
680
681fn debug_value_kind(value: &Value) -> &'static str {
682    match value {
683        Value::Num(_) => "Num",
684        Value::Int(_) => "Int",
685        Value::Bool(_) => "Bool",
686        Value::Tensor(t) => {
687            if t.data.is_empty() {
688                "Tensor(empty)"
689            } else {
690                "Tensor"
691            }
692        }
693        Value::GpuTensor(_) => "GpuTensor",
694        Value::String(_) => "String",
695        Value::CharArray(_) => "CharArray",
696        Value::StringArray(sa) => {
697            if sa.data.is_empty() {
698                "StringArray(empty)"
699            } else {
700                "StringArray"
701            }
702        }
703        Value::LogicalArray(l) => {
704            if l.data.is_empty() {
705                "LogicalArray(empty)"
706            } else {
707                "LogicalArray"
708            }
709        }
710        Value::Cell(c) => {
711            if c.data.is_empty() {
712                "Cell(empty)"
713            } else {
714                "Cell"
715            }
716        }
717        _ => "Other",
718    }
719}
720
721fn is_empty_placeholder(value: &Value) -> bool {
722    match value {
723        Value::Tensor(t) => t.data.is_empty(),
724        Value::LogicalArray(l) => l.data.is_empty(),
725        Value::StringArray(sa) => sa.data.is_empty(),
726        Value::CharArray(ca) => ca.data.is_empty(),
727        Value::Cell(cell) => cell.data.is_empty(),
728        Value::String(s) => s.is_empty(),
729        _ => false,
730    }
731}
732
733async fn parse_reduction_options(args: &mut ReductionArgs, rest: &[Value]) -> BuiltinResult<()> {
734    let mut idx = 0usize;
735    let mut selection_set = !matches!(args.selection, DimSelection::Auto);
736    let mut comparison_set = matches!(args.comparison, ComparisonMethod::Auto);
737    let tokens = tokens_from_values(rest);
738    while idx < rest.len() {
739        if let Some(crate::builtins::common::arg_tokens::ArgToken::String(text)) = tokens.get(idx) {
740            match text.as_str() {
741                "omitnan" => {
742                    args.nan_mode = ReductionNaN::Omit;
743                    idx += 1;
744                    continue;
745                }
746                "includenan" => {
747                    args.nan_mode = ReductionNaN::Include;
748                    idx += 1;
749                    continue;
750                }
751                "all" => {
752                    if selection_set {
753                        return Err(max_invalid_argument(
754                            "max: 'all' cannot be combined with an explicit dimension",
755                        ));
756                    }
757                    args.selection = DimSelection::All;
758                    selection_set = true;
759                    idx += 1;
760                    continue;
761                }
762                _ => {}
763            }
764        }
765        if let Some(keyword) = keyword_of(&rest[idx]) {
766            match keyword.as_str() {
767                "omitnan" => {
768                    args.nan_mode = ReductionNaN::Omit;
769                    idx += 1;
770                    continue;
771                }
772                "includenan" => {
773                    args.nan_mode = ReductionNaN::Include;
774                    idx += 1;
775                    continue;
776                }
777                "all" => {
778                    if selection_set {
779                        return Err(max_invalid_argument(
780                            "max: 'all' cannot be combined with an explicit dimension",
781                        ));
782                    }
783                    args.selection = DimSelection::All;
784                    selection_set = true;
785                    idx += 1;
786                    continue;
787                }
788                "linear" => {
789                    if selection_set {
790                        return Err(max_invalid_argument(
791                            "max: 'linear' cannot be combined with an explicit dimension",
792                        ));
793                    }
794                    args.selection = DimSelection::All;
795                    args.linear_index = true;
796                    selection_set = true;
797                    idx += 1;
798                    continue;
799                }
800                "comparisonmethod" => {
801                    let Some(value) = rest.get(idx + 1) else {
802                        return Err(max_invalid_argument(
803                            "max: expected a value after 'ComparisonMethod'",
804                        ));
805                    };
806                    args.comparison = parse_comparison_method(value)?;
807                    comparison_set = true;
808                    idx += 2;
809                    continue;
810                }
811                _ => {}
812            }
813        }
814
815        if !selection_set {
816            if let Some(selection) = parse_dimension_value(&rest[idx]).await? {
817                args.selection = selection;
818                selection_set = true;
819                idx += 1;
820                continue;
821            }
822        }
823
824        return Err(max_invalid_argument(format!(
825            "max: unrecognised argument {:?}",
826            rest[idx]
827        )));
828    }
829
830    if !comparison_set {
831        args.comparison = ComparisonMethod::Auto;
832    }
833
834    Ok(())
835}
836
837fn parse_elementwise_options(rest: &[Value]) -> BuiltinResult<(ReductionNaN, ComparisonMethod)> {
838    let mut nan_mode = ReductionNaN::Include;
839    let mut comparison = ComparisonMethod::Auto;
840    let mut comparison_set = false;
841    let mut idx = 0usize;
842    while idx < rest.len() {
843        if let Some(keyword) = keyword_of(&rest[idx]) {
844            match keyword.as_str() {
845                "omitnan" => {
846                    nan_mode = ReductionNaN::Omit;
847                    idx += 1;
848                    continue;
849                }
850                "includenan" => {
851                    nan_mode = ReductionNaN::Include;
852                    idx += 1;
853                    continue;
854                }
855                "comparisonmethod" => {
856                    let Some(value) = rest.get(idx + 1) else {
857                        return Err(max_invalid_argument(
858                            "max: expected a value after 'ComparisonMethod'",
859                        ));
860                    };
861                    comparison = parse_comparison_method(value)?;
862                    comparison_set = true;
863                    idx += 2;
864                    continue;
865                }
866                "all" | "linear" => {
867                    return Err(max_invalid_argument(format!(
868                        "max: '{}' is only supported for reduction calls",
869                        keyword
870                    )));
871                }
872                _ => {}
873            }
874        }
875        return Err(max_invalid_argument(format!(
876            "max: unrecognised argument {:?}",
877            rest[idx]
878        )));
879    }
880    if !comparison_set {
881        comparison = ComparisonMethod::Auto;
882    }
883    Ok((nan_mode, comparison))
884}
885
886fn parse_comparison_method(value: &Value) -> BuiltinResult<ComparisonMethod> {
887    let Some(keyword) = keyword_of(value) else {
888        return Err(max_invalid_argument(
889            "max: 'ComparisonMethod' expects a string value",
890        ));
891    };
892    match keyword.as_str() {
893        "auto" => Ok(ComparisonMethod::Auto),
894        "abs" | "magnitude" => Ok(ComparisonMethod::Abs),
895        "real" => Ok(ComparisonMethod::Real),
896        other => Err(max_invalid_argument(format!(
897            "max: unsupported ComparisonMethod '{other}'"
898        ))),
899    }
900}
901
902async fn parse_dimension_value(value: &Value) -> BuiltinResult<Option<DimSelection>> {
903    match value {
904        Value::Int(_) | Value::Num(_) => tensor::dimension_from_value_async(value, "max", false)
905            .await
906            .map_err(map_scalar_dim_error)
907            .map(|dim| dim.map(DimSelection::Dim)),
908        Value::Tensor(t) => parse_dimension_tensor(value, &t.shape).await,
909        Value::LogicalArray(logical) => parse_dimension_tensor(value, &logical.shape).await,
910        Value::GpuTensor(_) => Err(max_invalid_argument(
911            "max: dimension arguments must reside on the host",
912        )),
913        _ => Ok(None),
914    }
915}
916
917async fn parse_dimension_tensor(
918    value: &Value,
919    shape: &[usize],
920) -> BuiltinResult<Option<DimSelection>> {
921    if tensor::element_count(shape) == 0 {
922        return Ok(Some(DimSelection::Auto));
923    }
924    let is_vector = shape.len() == 1
925        || shape.get(0).copied().unwrap_or(1) == 1
926        || shape.get(1).copied().unwrap_or(1) == 1;
927    if !is_vector {
928        return Err(max_invalid_argument(
929            "max: dimension vector must be a row or column vector",
930        ));
931    }
932    let dims = tensor::dims_from_value_async(value)
933        .await
934        .map_err(map_vector_dim_error)?;
935    let Some(dims) = dims else {
936        return Ok(None);
937    };
938    if dims.is_empty() {
939        return Ok(Some(DimSelection::Auto));
940    }
941    let mut seen = BTreeSet::new();
942    let mut uniq = Vec::with_capacity(dims.len());
943    for dim in dims {
944        if dim < 1 {
945            return Err(max_invalid_argument("max: dimension indices must be >= 1"));
946        }
947        if seen.insert(dim) {
948            uniq.push(dim);
949        }
950    }
951    Ok(Some(DimSelection::Vec(uniq)))
952}
953
954fn map_scalar_dim_error(message: String) -> RuntimeError {
955    if message.contains("integer") {
956        return max_invalid_argument("max: dimension must be integral");
957    }
958    max_invalid_argument(message)
959}
960
961fn map_vector_dim_error(message: String) -> RuntimeError {
962    if message.contains("non-negative") {
963        return max_invalid_argument("max: dimension indices must be >= 1");
964    }
965    if message.contains("finite") {
966        return max_invalid_argument("max: dimension entries must be finite");
967    }
968    if message.contains("integer") {
969        return max_invalid_argument("max: dimension entries must be integers");
970    }
971    max_invalid_argument(message)
972}
973
974async fn reduction_max(value: Value, args: ReductionArgs) -> BuiltinResult<MaxEvaluation> {
975    match value {
976        Value::GpuTensor(handle) => {
977            if let Some(eval) = reduction_max_gpu(handle.clone(), &args).await? {
978                return Ok(eval);
979            }
980            // Fall back to host if GPU path is unavailable.
981            let tensor = gpu_helpers::gather_tensor_async(&handle)
982                .await
983                .map_err(|e| max_internal_error(format!("max: {e}")))?;
984            reduction_max_host(Value::Tensor(tensor), &args)
985        }
986        other => reduction_max_host(other, &args),
987    }
988}
989
990async fn reduction_max_gpu(
991    handle: GpuTensorHandle,
992    args: &ReductionArgs,
993) -> BuiltinResult<Option<MaxEvaluation>> {
994    if args.nan_mode == ReductionNaN::Omit {
995        log::trace!("max: gpu path disabled (nan_mode=omit)");
996        return Ok(None);
997    }
998    if args.comparison != ComparisonMethod::Auto {
999        log::trace!("max: gpu path disabled (comparison != auto)");
1000        return Ok(None);
1001    }
1002    if args.linear_index {
1003        log::trace!("max: gpu path disabled (linear_index=true)");
1004        return Ok(None);
1005    }
1006    let provider = match runmat_accelerate_api::provider() {
1007        Some(p) => p,
1008        None => {
1009            log::trace!(
1010                "max: gpu path unavailable (provider() is None) handle_shape={:?} device_id={}",
1011                handle.shape,
1012                handle.device_id
1013            );
1014            return Ok(None);
1015        }
1016    };
1017    let target_dim = match args.selection {
1018        DimSelection::Auto => default_dimension_from_shape(&handle.shape),
1019        DimSelection::Dim(dim) => dim,
1020        DimSelection::Vec(ref dims) if dims.len() == 1 => dims[0],
1021        DimSelection::All => {
1022            if handle.shape.len() <= 1 {
1023                1
1024            } else {
1025                return Ok(None);
1026            }
1027        }
1028        _ => return Ok(None),
1029    };
1030    if target_dim == 0 {
1031        return Ok(None);
1032    }
1033    // MATLAB dimensions are 1-based; `reduce_max_dim` expects zero-based.
1034    let zero_based = target_dim.saturating_sub(1);
1035    if zero_based >= handle.shape.len() {
1036        return Ok(None);
1037    }
1038    log::trace!(
1039        "max: attempting reduce_max_dim dim={} (zero_based={}) shape={:?} device_id={}",
1040        target_dim,
1041        zero_based,
1042        handle.shape,
1043        handle.device_id
1044    );
1045    match provider.reduce_max_dim(&handle, zero_based).await {
1046        Ok(ReduceDimResult { values, indices }) => Ok(Some(MaxEvaluation {
1047            values: Value::GpuTensor(values),
1048            indices: Value::GpuTensor(indices),
1049        })),
1050        Err(err) => {
1051            log::trace!("max: reduce_max_dim failed: {err}");
1052            Ok(None)
1053        }
1054    }
1055}
1056
1057fn reduction_max_host(value: Value, args: &ReductionArgs) -> BuiltinResult<MaxEvaluation> {
1058    if let Value::Int(value) = &value {
1059        let storage = crate::builtins::math::reduction::integer_native::storage_from_scalar(value);
1060        return reduce_integer_max(&storage, vec![1, 1], args);
1061    }
1062    if let Some((storage, shape)) = native_integer_input(&value) {
1063        return reduce_integer_max(storage, shape, args);
1064    }
1065    match materialize_for_max("max", value)? {
1066        InputData::Real(tensor) => reduce_real_tensor(tensor, args),
1067        InputData::Complex(tensor) => reduce_complex_tensor(tensor, args),
1068    }
1069}
1070
1071fn native_integer_input(value: &Value) -> Option<(&runmat_builtins::IntegerStorage, Vec<usize>)> {
1072    match value {
1073        Value::Tensor(tensor) => tensor
1074            .integer_storage()
1075            .map(|storage| (storage, tensor.shape.clone())),
1076        _ => None,
1077    }
1078}
1079
1080fn reduce_integer_max(
1081    storage: &runmat_builtins::IntegerStorage,
1082    shape: Vec<usize>,
1083    args: &ReductionArgs,
1084) -> BuiltinResult<MaxEvaluation> {
1085    if storage.is_empty() {
1086        // Integer extrema preserve the empty input shape for every reduction
1087        // selector. A non-empty reduced shape cannot represent an empty typed
1088        // payload, and MATLAB extrema of an empty array stay empty.
1089        let output_shape = shape;
1090        let values = crate::builtins::math::reduction::integer_native::empty_like(
1091            storage,
1092            output_shape.clone(),
1093        )
1094        .map_err(|error| max_internal_error(format!("max: {error}")))?;
1095        let indices = Tensor::new(Vec::new(), output_shape)
1096            .map_err(|error| max_internal_error(format!("max: {error}")))?;
1097        return Ok(MaxEvaluation {
1098            values,
1099            indices: tensor::tensor_into_value(indices),
1100        });
1101    }
1102
1103    let resolved = resolve_reduction_dims(&shape, &args.selection)?;
1104    let comparison = match args.comparison {
1105        ComparisonMethod::Auto | ComparisonMethod::Real => {
1106            crate::builtins::math::reduction::integer_native::ExtremaComparison::Natural
1107        }
1108        ComparisonMethod::Abs => {
1109            crate::builtins::math::reduction::integer_native::ExtremaComparison::Absolute
1110        }
1111    };
1112    let extrema = crate::builtins::math::reduction::integer_native::extrema(
1113        storage,
1114        &shape,
1115        resolved.output_shape,
1116        &resolved.reduced_dims,
1117        &resolved.dims_mask,
1118        &resolved.reduce_strides,
1119        resolved.reduce_all,
1120        args.linear_index,
1121        crate::builtins::math::reduction::integer_native::ExtremaDirection::Max,
1122        comparison,
1123    )
1124    .map_err(|error| max_internal_error(format!("max: {error}")))?;
1125    Ok(MaxEvaluation {
1126        values: extrema.values,
1127        indices: extrema.indices,
1128    })
1129}
1130
1131enum InputData {
1132    Real(Tensor),
1133    Complex(ComplexTensor),
1134}
1135
1136fn materialize_for_max(name: &str, value: Value) -> BuiltinResult<InputData> {
1137    match value {
1138        Value::Tensor(t) => Ok(InputData::Real(t)),
1139        Value::LogicalArray(logical) => {
1140            let tensor = tensor::logical_to_tensor(&logical).map_err(max_invalid_input)?;
1141            Ok(InputData::Real(tensor))
1142        }
1143        Value::Num(n) => {
1144            let tensor = Tensor::new(vec![n], vec![1, 1])
1145                .map_err(|e| max_internal_error(format!("{name}: {e}")))?;
1146            Ok(InputData::Real(tensor))
1147        }
1148        Value::Int(i) => {
1149            let tensor = Tensor::new(vec![i.to_f64()], vec![1, 1])
1150                .map_err(|e| max_internal_error(format!("{name}: {e}")))?;
1151            Ok(InputData::Real(tensor))
1152        }
1153        Value::Bool(b) => {
1154            let tensor = Tensor::new(vec![if b { 1.0 } else { 0.0 }], vec![1, 1])
1155                .map_err(|e| max_internal_error(format!("{name}: {e}")))?;
1156            Ok(InputData::Real(tensor))
1157        }
1158        Value::Complex(re, im) => {
1159            let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1])
1160                .map_err(|e| max_internal_error(format!("{name}: {e}")))?;
1161            Ok(InputData::Complex(tensor))
1162        }
1163        Value::ComplexTensor(ct) => Ok(InputData::Complex(ct)),
1164        Value::String(_)
1165        | Value::StringArray(_)
1166        | Value::CharArray(_)
1167        | Value::SparseTensor(_)
1168        | Value::Symbolic(_)
1169        | Value::SymbolicArray(_)
1170        | Value::Cell(_) => Err(max_invalid_input(format!(
1171            "{name}: expected numeric or logical input, received non-numeric value"
1172        ))),
1173        Value::GpuTensor(_) => Err(max_internal_error(format!(
1174            "{name}: internal error – GPU tensors must be gathered before host execution"
1175        ))),
1176        Value::Object(_) | Value::HandleObject(_) | Value::Struct(_) | Value::Listener(_) => {
1177            Err(max_invalid_input(format!("{name}: unsupported input type")))
1178        }
1179        Value::FunctionHandle(_)
1180        | Value::ExternalFunctionHandle(_)
1181        | Value::MethodFunctionHandle(_)
1182        | Value::BoundFunctionHandle { .. }
1183        | Value::Closure(_)
1184        | Value::ClassRef(_)
1185        | Value::MException(_)
1186        | Value::OutputList(_) => Err(max_invalid_input(format!("{name}: unsupported input type"))),
1187    }
1188}
1189
1190fn reduce_real_tensor(tensor: Tensor, args: &ReductionArgs) -> BuiltinResult<MaxEvaluation> {
1191    let shape = tensor.shape.clone();
1192    if tensor.data.is_empty() {
1193        let output_shape = resolve_output_shape(&shape, &args.selection, &[])?;
1194        let values = Tensor::new(Vec::new(), output_shape.clone())
1195            .map_err(|e| max_internal_error(format!("max: {e}")))?;
1196        let indices = Tensor::new(Vec::new(), output_shape)
1197            .map_err(|e| max_internal_error(format!("max: {e}")))?;
1198        return Ok(MaxEvaluation {
1199            values: tensor::tensor_into_value(values),
1200            indices: tensor::tensor_into_value(indices),
1201        });
1202    }
1203    let resolved = resolve_reduction_dims(&shape, &args.selection)?;
1204    let output_shape = resolved.output_shape.clone();
1205    let output_len = tensor::element_count(&output_shape);
1206
1207    if output_len == 0 {
1208        let values = Tensor::new(Vec::new(), output_shape.clone())
1209            .map_err(|e| max_internal_error(format!("max: {e}")))?;
1210        let indices = Tensor::new(Vec::new(), output_shape)
1211            .map_err(|e| max_internal_error(format!("max: {e}")))?;
1212        return Ok(MaxEvaluation {
1213            values: tensor::tensor_into_value(values),
1214            indices: tensor::tensor_into_value(indices),
1215        });
1216    }
1217
1218    let strides = compute_strides(&shape);
1219    let output_strides = compute_strides(&output_shape);
1220    let dims_mask = resolved.dims_mask.clone();
1221    let reduce_strides = resolved.reduce_strides.clone();
1222
1223    let mut best = vec![BestReal::new(); output_len];
1224    let mut coords = vec![0usize; shape.len()];
1225    for &value in &tensor.data {
1226        let out_idx = map_output_index(&coords, &output_strides, &dims_mask);
1227        let reduce_idx = map_reduce_index(
1228            &coords,
1229            &resolved.reduced_dims,
1230            &reduce_strides,
1231            resolved.reduce_all,
1232        );
1233        let full_idx = map_linear_index(&coords, &strides);
1234
1235        update_best_real(
1236            &mut best[out_idx],
1237            value,
1238            reduce_idx,
1239            full_idx,
1240            args.nan_mode,
1241            args.comparison,
1242        );
1243        increment_coords(&mut coords, &shape);
1244    }
1245
1246    let mut values = vec![0.0f64; output_len];
1247    let mut indices = vec![0.0f64; output_len];
1248
1249    for (i, entry) in best.iter().enumerate() {
1250        if entry.nan_fixed {
1251            values[i] = f64::NAN;
1252            indices[i] = if args.linear_index || resolved.reduce_all {
1253                (entry.full_index + 1) as f64
1254            } else if resolved.reduced_dims.is_empty() {
1255                1.0
1256            } else {
1257                (entry.reduce_index + 1) as f64
1258            };
1259            continue;
1260        }
1261        if !entry.has_value {
1262            values[i] = f64::NAN;
1263            indices[i] = f64::NAN;
1264            continue;
1265        }
1266        values[i] = entry.value;
1267        indices[i] = if args.linear_index || resolved.reduce_all {
1268            (entry.full_index + 1) as f64
1269        } else if resolved.reduced_dims.is_empty() {
1270            1.0
1271        } else {
1272            (entry.reduce_index + 1) as f64
1273        };
1274    }
1275
1276    let value_tensor = Tensor::new(values, output_shape.clone())
1277        .map_err(|e| max_internal_error(format!("max: {e}")))?;
1278    let index_tensor =
1279        Tensor::new(indices, output_shape).map_err(|e| max_internal_error(format!("max: {e}")))?;
1280
1281    Ok(MaxEvaluation {
1282        values: tensor::tensor_into_value(value_tensor),
1283        indices: tensor::tensor_into_value(index_tensor),
1284    })
1285}
1286
1287fn reduce_complex_tensor(
1288    tensor: ComplexTensor,
1289    args: &ReductionArgs,
1290) -> BuiltinResult<MaxEvaluation> {
1291    let shape = tensor.shape.clone();
1292    if tensor.data.is_empty() {
1293        let output_shape = resolve_output_shape(&shape, &args.selection, &[])?;
1294        let values = ComplexTensor::new(Vec::new(), output_shape.clone())
1295            .map_err(|e| max_internal_error(format!("max: {e}")))?;
1296        let indices = Tensor::new(Vec::new(), output_shape)
1297            .map_err(|e| max_internal_error(format!("max: {e}")))?;
1298        return Ok(MaxEvaluation {
1299            values: complex_tensor_into_value(values),
1300            indices: tensor::tensor_into_value(indices),
1301        });
1302    }
1303
1304    let resolved = resolve_reduction_dims(&shape, &args.selection)?;
1305    let output_shape = resolved.output_shape.clone();
1306    let output_len = tensor::element_count(&output_shape);
1307
1308    if output_len == 0 {
1309        let values = ComplexTensor::new(Vec::new(), output_shape.clone())
1310            .map_err(|e| max_internal_error(format!("max: {e}")))?;
1311        let indices = Tensor::new(Vec::new(), output_shape)
1312            .map_err(|e| max_internal_error(format!("max: {e}")))?;
1313        return Ok(MaxEvaluation {
1314            values: complex_tensor_into_value(values),
1315            indices: tensor::tensor_into_value(indices),
1316        });
1317    }
1318
1319    let strides = compute_strides(&shape);
1320    let output_strides = compute_strides(&output_shape);
1321    let dims_mask = resolved.dims_mask.clone();
1322    let reduce_strides = resolved.reduce_strides.clone();
1323
1324    let mut best = vec![BestComplex::new(); output_len];
1325    let mut coords = vec![0usize; shape.len()];
1326
1327    for &(re, im) in &tensor.data {
1328        let out_idx = map_output_index(&coords, &output_strides, &dims_mask);
1329        let reduce_idx = map_reduce_index(
1330            &coords,
1331            &resolved.reduced_dims,
1332            &reduce_strides,
1333            resolved.reduce_all,
1334        );
1335        let full_idx = map_linear_index(&coords, &strides);
1336        update_best_complex(
1337            &mut best[out_idx],
1338            (re, im),
1339            reduce_idx,
1340            full_idx,
1341            args.nan_mode,
1342            args.comparison,
1343        );
1344        increment_coords(&mut coords, &shape);
1345    }
1346
1347    let mut values = vec![(0.0f64, 0.0f64); output_len];
1348    let mut indices = vec![0.0f64; output_len];
1349
1350    for (i, entry) in best.iter().enumerate() {
1351        if entry.nan_fixed {
1352            values[i] = (f64::NAN, f64::NAN);
1353            indices[i] = if args.linear_index || resolved.reduce_all {
1354                (entry.full_index + 1) as f64
1355            } else if resolved.reduced_dims.is_empty() {
1356                1.0
1357            } else {
1358                (entry.reduce_index + 1) as f64
1359            };
1360            continue;
1361        }
1362        if !entry.has_value {
1363            values[i] = (f64::NAN, f64::NAN);
1364            indices[i] = f64::NAN;
1365            continue;
1366        }
1367        values[i] = entry.value;
1368        indices[i] = if args.linear_index || resolved.reduce_all {
1369            (entry.full_index + 1) as f64
1370        } else if resolved.reduced_dims.is_empty() {
1371            1.0
1372        } else {
1373            (entry.reduce_index + 1) as f64
1374        };
1375    }
1376
1377    let value_tensor = ComplexTensor::new(values, output_shape.clone())
1378        .map_err(|e| max_internal_error(format!("max: {e}")))?;
1379    let index_tensor =
1380        Tensor::new(indices, output_shape).map_err(|e| max_internal_error(format!("max: {e}")))?;
1381    Ok(MaxEvaluation {
1382        values: complex_tensor_into_value(value_tensor),
1383        indices: tensor::tensor_into_value(index_tensor),
1384    })
1385}
1386
1387#[derive(Debug, Clone)]
1388struct BestReal {
1389    value: f64,
1390    reduce_index: usize,
1391    full_index: usize,
1392    has_value: bool,
1393    nan_fixed: bool,
1394}
1395
1396impl BestReal {
1397    fn new() -> Self {
1398        Self {
1399            value: 0.0,
1400            reduce_index: 0,
1401            full_index: 0,
1402            has_value: false,
1403            nan_fixed: false,
1404        }
1405    }
1406}
1407
1408#[derive(Debug, Clone)]
1409struct BestComplex {
1410    value: (f64, f64),
1411    reduce_index: usize,
1412    full_index: usize,
1413    has_value: bool,
1414    nan_fixed: bool,
1415}
1416
1417impl BestComplex {
1418    fn new() -> Self {
1419        Self {
1420            value: (0.0, 0.0),
1421            reduce_index: 0,
1422            full_index: 0,
1423            has_value: false,
1424            nan_fixed: false,
1425        }
1426    }
1427}
1428
1429fn resolve_output_shape(
1430    shape: &[usize],
1431    selection: &DimSelection,
1432    reduced_dims: &[usize],
1433) -> BuiltinResult<Vec<usize>> {
1434    if is_scalar_shape(shape) {
1435        return Ok(normalize_scalar_shape(shape));
1436    }
1437    let mut output = shape.to_vec();
1438    match selection {
1439        DimSelection::All => {
1440            output.fill(1);
1441        }
1442        _ => {
1443            for &dim in reduced_dims {
1444                if dim < output.len() {
1445                    output[dim] = 1;
1446                }
1447            }
1448        }
1449    }
1450    Ok(output)
1451}
1452
1453struct ResolvedDims {
1454    output_shape: Vec<usize>,
1455    reduced_dims: Vec<usize>,
1456    reduce_all: bool,
1457    dims_mask: Vec<bool>,
1458    reduce_strides: Vec<usize>,
1459}
1460
1461fn resolve_reduction_dims(
1462    shape: &[usize],
1463    selection: &DimSelection,
1464) -> BuiltinResult<ResolvedDims> {
1465    if is_scalar_shape(shape) {
1466        return Ok(ResolvedDims {
1467            output_shape: normalize_scalar_shape(shape),
1468            reduced_dims: Vec::new(),
1469            reduce_all: true,
1470            dims_mask: Vec::new(),
1471            reduce_strides: Vec::new(),
1472        });
1473    }
1474
1475    let mut reduced_dims = match selection {
1476        DimSelection::Auto => {
1477            let mut dim = None;
1478            for (index, &len) in shape.iter().enumerate() {
1479                if len > 1 {
1480                    dim = Some(index);
1481                    break;
1482                }
1483            }
1484            vec![dim.unwrap_or(0)]
1485        }
1486        DimSelection::Dim(dim) => {
1487            if *dim == 0 {
1488                return Err(max_invalid_argument("max: dimension must be >= 1"));
1489            }
1490            let index = dim.saturating_sub(1);
1491            if index >= shape.len() {
1492                Vec::new()
1493            } else {
1494                vec![index]
1495            }
1496        }
1497        DimSelection::Vec(dims) => {
1498            if dims.is_empty() {
1499                Vec::new()
1500            } else {
1501                dims.iter()
1502                    .filter_map(|dim| {
1503                        if *dim == 0 {
1504                            None
1505                        } else {
1506                            let idx = dim - 1;
1507                            if idx < shape.len() {
1508                                Some(idx)
1509                            } else {
1510                                None
1511                            }
1512                        }
1513                    })
1514                    .collect()
1515            }
1516        }
1517        DimSelection::All => (0..shape.len()).collect(),
1518    };
1519
1520    reduced_dims.sort_unstable();
1521    reduced_dims.dedup();
1522
1523    let reduce_all = !reduced_dims.is_empty()
1524        && reduced_dims.len() == shape.len()
1525        && reduced_dims.iter().enumerate().all(|(i, &d)| i == d);
1526
1527    let output_shape = resolve_output_shape(shape, selection, &reduced_dims)?;
1528    let mut dims_mask = vec![false; shape.len()];
1529    for &dim in &reduced_dims {
1530        if dim < dims_mask.len() {
1531            dims_mask[dim] = true;
1532        }
1533    }
1534    let reduce_strides = compute_subspace_strides(shape, &reduced_dims);
1535
1536    Ok(ResolvedDims {
1537        output_shape,
1538        reduced_dims,
1539        reduce_all,
1540        dims_mask,
1541        reduce_strides,
1542    })
1543}
1544
1545fn compute_strides(shape: &[usize]) -> Vec<usize> {
1546    let mut strides = Vec::with_capacity(shape.len());
1547    let mut stride = 1usize;
1548    for &len in shape {
1549        strides.push(stride);
1550        stride = stride.saturating_mul(len.max(1));
1551    }
1552    strides
1553}
1554
1555fn compute_subspace_strides(shape: &[usize], dims: &[usize]) -> Vec<usize> {
1556    if dims.is_empty() {
1557        return Vec::new();
1558    }
1559    let mut strides = Vec::with_capacity(dims.len());
1560    let mut accum = 1usize;
1561    for &dim in dims {
1562        let len = shape.get(dim).copied().unwrap_or(1).max(1);
1563        strides.push(accum);
1564        accum = accum.saturating_mul(len);
1565    }
1566    strides
1567}
1568
1569fn map_output_index(coords: &[usize], output_strides: &[usize], dims_mask: &[bool]) -> usize {
1570    if coords.is_empty() {
1571        return 0;
1572    }
1573    let mut index = 0usize;
1574    for (dim, stride) in output_strides.iter().enumerate() {
1575        let coord = if *dims_mask.get(dim).unwrap_or(&false) {
1576            0
1577        } else {
1578            coords[dim]
1579        };
1580        index = index.saturating_add(coord.saturating_mul(*stride));
1581    }
1582    index
1583}
1584
1585fn map_reduce_index(
1586    coords: &[usize],
1587    reduced_dims: &[usize],
1588    reduce_strides: &[usize],
1589    reduce_all: bool,
1590) -> usize {
1591    if reduced_dims.is_empty() {
1592        return 0;
1593    }
1594    if reduce_all {
1595        // When all dimensions are reduced, the full index is used separately.
1596        return 0;
1597    }
1598    let mut index = 0usize;
1599    for (pos, &dim) in reduced_dims.iter().enumerate() {
1600        if let Some(coord) = coords.get(dim) {
1601            if let Some(stride) = reduce_strides.get(pos) {
1602                index = index.saturating_add(coord.saturating_mul(*stride));
1603            }
1604        }
1605    }
1606    index
1607}
1608
1609fn map_linear_index(coords: &[usize], strides: &[usize]) -> usize {
1610    coords
1611        .iter()
1612        .zip(strides.iter())
1613        .fold(0usize, |acc, (&coord, &stride)| {
1614            acc.saturating_add(coord.saturating_mul(stride))
1615        })
1616}
1617
1618fn increment_coords(coords: &mut [usize], shape: &[usize]) {
1619    for dim in 0..coords.len() {
1620        if shape[dim] == 0 {
1621            continue;
1622        }
1623        coords[dim] += 1;
1624        if coords[dim] < shape[dim] {
1625            break;
1626        }
1627        coords[dim] = 0;
1628    }
1629}
1630
1631fn update_best_real(
1632    best: &mut BestReal,
1633    value: f64,
1634    reduce_index: usize,
1635    full_index: usize,
1636    nan_mode: ReductionNaN,
1637    comparison: ComparisonMethod,
1638) {
1639    if value.is_nan() {
1640        match nan_mode {
1641            ReductionNaN::Include => {
1642                if !best.nan_fixed {
1643                    best.value = f64::NAN;
1644                    best.reduce_index = reduce_index;
1645                    best.full_index = full_index;
1646                    best.has_value = true;
1647                    best.nan_fixed = true;
1648                }
1649            }
1650            ReductionNaN::Omit => {}
1651        }
1652        return;
1653    }
1654    if best.nan_fixed {
1655        return;
1656    }
1657
1658    if !best.has_value {
1659        best.value = value;
1660        best.reduce_index = reduce_index;
1661        best.full_index = full_index;
1662        best.has_value = true;
1663        return;
1664    }
1665
1666    if should_replace_real(best.value, value, comparison) {
1667        best.value = value;
1668        best.reduce_index = reduce_index;
1669        best.full_index = full_index;
1670    }
1671}
1672
1673fn update_best_complex(
1674    best: &mut BestComplex,
1675    value: (f64, f64),
1676    reduce_index: usize,
1677    full_index: usize,
1678    nan_mode: ReductionNaN,
1679    comparison: ComparisonMethod,
1680) {
1681    if value.0.is_nan() || value.1.is_nan() {
1682        match nan_mode {
1683            ReductionNaN::Include => {
1684                if !best.nan_fixed {
1685                    best.value = (f64::NAN, f64::NAN);
1686                    best.reduce_index = reduce_index;
1687                    best.full_index = full_index;
1688                    best.has_value = true;
1689                    best.nan_fixed = true;
1690                }
1691            }
1692            ReductionNaN::Omit => {}
1693        }
1694        return;
1695    }
1696    if best.nan_fixed {
1697        return;
1698    }
1699
1700    if !best.has_value {
1701        best.value = value;
1702        best.reduce_index = reduce_index;
1703        best.full_index = full_index;
1704        best.has_value = true;
1705        return;
1706    }
1707
1708    if should_replace_complex(best.value, value, comparison) {
1709        best.value = value;
1710        best.reduce_index = reduce_index;
1711        best.full_index = full_index;
1712    }
1713}
1714
1715fn should_replace_real(current: f64, candidate: f64, comparison: ComparisonMethod) -> bool {
1716    match comparison {
1717        ComparisonMethod::Auto | ComparisonMethod::Real => {
1718            if candidate > current {
1719                return true;
1720            }
1721            if candidate < current {
1722                return false;
1723            }
1724            if candidate == 0.0 && current == 0.0 {
1725                return candidate.is_sign_positive() && !current.is_sign_positive();
1726            }
1727            false
1728        }
1729        ComparisonMethod::Abs => {
1730            let curr_abs = current.abs();
1731            let cand_abs = candidate.abs();
1732            if cand_abs > curr_abs {
1733                return true;
1734            }
1735            if cand_abs < curr_abs {
1736                return false;
1737            }
1738            if candidate > current {
1739                return true;
1740            }
1741            if candidate < current {
1742                return false;
1743            }
1744            if candidate == 0.0 && current == 0.0 {
1745                return candidate.is_sign_positive() && !current.is_sign_positive();
1746            }
1747            false
1748        }
1749    }
1750}
1751
1752fn should_replace_complex(
1753    current: (f64, f64),
1754    candidate: (f64, f64),
1755    comparison: ComparisonMethod,
1756) -> bool {
1757    match comparison {
1758        ComparisonMethod::Auto | ComparisonMethod::Abs => {
1759            compare_complex_auto(current, candidate) == Ordering::Less
1760        }
1761        ComparisonMethod::Real => compare_complex_real(current, candidate) == Ordering::Less,
1762    }
1763}
1764
1765fn compare_complex_auto(a: (f64, f64), b: (f64, f64)) -> Ordering {
1766    let a_mag = magnitude_squared(a);
1767    let b_mag = magnitude_squared(b);
1768    if a_mag < b_mag {
1769        return Ordering::Less;
1770    }
1771    if a_mag > b_mag {
1772        return Ordering::Greater;
1773    }
1774    // Equal magnitude: tie-break using phase angle.
1775    let a_angle = a.1.atan2(a.0);
1776    let b_angle = b.1.atan2(b.0);
1777    if a_angle < b_angle {
1778        Ordering::Less
1779    } else if a_angle > b_angle {
1780        Ordering::Greater
1781    } else {
1782        Ordering::Equal
1783    }
1784}
1785
1786fn compare_complex_real(a: (f64, f64), b: (f64, f64)) -> Ordering {
1787    if a.0 < b.0 {
1788        return Ordering::Less;
1789    }
1790    if a.0 > b.0 {
1791        return Ordering::Greater;
1792    }
1793    // Equal real parts: use magnitude and phase tie-breakers.
1794    compare_complex_auto(a, b)
1795}
1796
1797fn magnitude_squared(z: (f64, f64)) -> f64 {
1798    z.0.mul_add(z.0, z.1 * z.1)
1799}
1800
1801fn default_dimension_from_shape(shape: &[usize]) -> usize {
1802    if is_scalar_shape(shape) {
1803        return 1;
1804    }
1805    for (i, &len) in shape.iter().enumerate() {
1806        if len > 1 {
1807            return i + 1;
1808        }
1809    }
1810    1
1811}
1812
1813async fn elementwise_max(value: Value, args: ElementwiseArgs) -> BuiltinResult<MaxEvaluation> {
1814    let ElementwiseArgs {
1815        other,
1816        nan_mode,
1817        comparison,
1818    } = args;
1819    match (value, other) {
1820        (Value::GpuTensor(handle_a), Value::GpuTensor(handle_b)) => {
1821            if gpu_tensor_is_scalar(&handle_b) {
1822                if let Some(num) = gpu_tensor_scalar_value(&handle_b).await {
1823                    let scalar = Value::Num(num);
1824                    if nan_mode == ReductionNaN::Include {
1825                        if let Some(eval) =
1826                            elementwise_max_gpu_scalar_left(&handle_a, &scalar, comparison).await
1827                        {
1828                            return Ok(eval);
1829                        }
1830                    }
1831                    if let Ok(ta) = gpu_helpers::gather_tensor_async(&handle_a).await {
1832                        if let Ok(eval) = elementwise_real_or_complex(
1833                            Value::Tensor(ta),
1834                            scalar.clone(),
1835                            nan_mode,
1836                            comparison,
1837                        ) {
1838                            return Ok(eval);
1839                        }
1840                    }
1841                    return Err(max_internal_error(
1842                        "max: elementwise GPU scalar path failed",
1843                    ));
1844                }
1845            }
1846            if gpu_tensor_is_scalar(&handle_a) {
1847                if let Some(num) = gpu_tensor_scalar_value(&handle_a).await {
1848                    let scalar = Value::Num(num);
1849                    if nan_mode == ReductionNaN::Include {
1850                        if let Some(eval) =
1851                            elementwise_max_gpu_scalar_right(&scalar, &handle_b, comparison).await
1852                        {
1853                            return Ok(eval);
1854                        }
1855                    }
1856                    if let Ok(tb) = gpu_helpers::gather_tensor_async(&handle_b).await {
1857                        if let Ok(eval) = elementwise_real_or_complex(
1858                            scalar.clone(),
1859                            Value::Tensor(tb),
1860                            nan_mode,
1861                            comparison,
1862                        ) {
1863                            return Ok(eval);
1864                        }
1865                    }
1866                    return Err(max_internal_error(
1867                        "max: elementwise GPU scalar path failed",
1868                    ));
1869                }
1870            }
1871            if nan_mode == ReductionNaN::Include {
1872                if let Some(eval) = elementwise_max_gpu_pair(&handle_a, &handle_b, comparison).await
1873                {
1874                    return Ok(eval);
1875                }
1876            }
1877            if let (Ok(ta), Ok(tb)) = (
1878                gpu_helpers::gather_tensor_async(&handle_a).await,
1879                gpu_helpers::gather_tensor_async(&handle_b).await,
1880            ) {
1881                if let Ok(eval) = elementwise_real_or_complex(
1882                    Value::Tensor(ta),
1883                    Value::Tensor(tb),
1884                    nan_mode,
1885                    comparison,
1886                ) {
1887                    return Ok(eval);
1888                }
1889            }
1890            Err(max_internal_error("max: elementwise GPU path failed"))
1891        }
1892        (Value::GpuTensor(handle), other) => {
1893            if nan_mode == ReductionNaN::Include {
1894                if let Some(eval) =
1895                    elementwise_max_gpu_scalar_left(&handle, &other, comparison).await
1896                {
1897                    return Ok(eval);
1898                }
1899            }
1900            let t = gpu_helpers::gather_tensor_async(&handle)
1901                .await
1902                .map_err(|_| max_internal_error("max: elementwise GPU scalar path failed"))?;
1903            elementwise_real_or_complex(Value::Tensor(t), other, nan_mode, comparison)
1904        }
1905        (other, Value::GpuTensor(handle)) => {
1906            if nan_mode == ReductionNaN::Include {
1907                if let Some(eval) =
1908                    elementwise_max_gpu_scalar_right(&other, &handle, comparison).await
1909                {
1910                    return Ok(eval);
1911                }
1912            }
1913            let t = gpu_helpers::gather_tensor_async(&handle)
1914                .await
1915                .map_err(|_| max_internal_error("max: elementwise GPU scalar path failed"))?;
1916            elementwise_real_or_complex(other, Value::Tensor(t), nan_mode, comparison)
1917        }
1918        (lhs, rhs) => elementwise_real_or_complex(lhs, rhs, nan_mode, comparison),
1919    }
1920}
1921
1922async fn elementwise_max_gpu_pair(
1923    a: &GpuTensorHandle,
1924    b: &GpuTensorHandle,
1925    comparison: ComparisonMethod,
1926) -> Option<MaxEvaluation> {
1927    if comparison != ComparisonMethod::Auto {
1928        return None;
1929    }
1930    let provider = runmat_accelerate_api::provider()?;
1931    // Equal-shape fast path
1932    if a.shape == b.shape {
1933        let values = provider.elem_max(a, b).await.ok()?;
1934        // Try device mask first; if unavailable, compute indices on host while keeping values on device
1935        if let Ok(mask) = provider.elem_ge(a, b).await {
1936            let indices = gpu_mask_indices(provider, &mask)?;
1937            let _ = provider.free(&mask);
1938            return Some(MaxEvaluation {
1939                values: Value::GpuTensor(values),
1940                indices: Value::GpuTensor(indices),
1941            });
1942        } else {
1943            // Host path for indices only
1944            let ta = gpu_helpers::gather_tensor_async(a).await.ok()?;
1945            let tb = gpu_helpers::gather_tensor_async(b).await.ok()?;
1946            let mut indices = Vec::with_capacity(ta.data.len());
1947            for i in 0..ta.data.len() {
1948                indices.push(if ta.data[i] >= tb.data[i] { 1.0 } else { 2.0 });
1949            }
1950            let index_tensor = Tensor::new(indices, ta.shape.clone()).ok()?;
1951            return Some(MaxEvaluation {
1952                values: Value::GpuTensor(values),
1953                indices: tensor::tensor_into_value(index_tensor),
1954            });
1955        }
1956    }
1957    // Broadcast-compatible path via repmat, then device compare
1958    let (out_shape, reps_a, reps_b) = broadcast_reps(&a.shape, &b.shape)?;
1959    let a_exp = if reps_a.iter().any(|&r| r != 1) {
1960        provider.repmat(a, &reps_a).ok()?
1961    } else {
1962        a.clone()
1963    };
1964    let b_exp = if reps_b.iter().any(|&r| r != 1) {
1965        provider.repmat(b, &reps_b).ok()?
1966    } else {
1967        b.clone()
1968    };
1969    let values = provider.elem_max(&a_exp, &b_exp).await.ok();
1970    let mask = provider.elem_ge(&a_exp, &b_exp).await.ok();
1971    if !std::ptr::eq(&a_exp, a) {
1972        let _ = provider.free(&a_exp);
1973    }
1974    if !std::ptr::eq(&b_exp, b) {
1975        let _ = provider.free(&b_exp);
1976    }
1977    let values = values?;
1978    if values.shape != out_shape {
1979        let _ = provider.free(&values);
1980        return None;
1981    }
1982    let index_tensor = if let Some(mask) = mask {
1983        let mask_host = gpu_helpers::gather_tensor_async(&mask).await.ok()?;
1984        let _ = provider.free(&mask);
1985        let mut indices = Vec::with_capacity(mask_host.data.len());
1986        for &m in &mask_host.data {
1987            indices.push(if m != 0.0 { 1.0 } else { 2.0 });
1988        }
1989        Tensor::new(indices, out_shape).ok()?
1990    } else {
1991        // Host indices fallback
1992        let ta = gpu_helpers::gather_tensor_async(&a_exp).await.ok()?;
1993        let tb = gpu_helpers::gather_tensor_async(&b_exp).await.ok()?;
1994        let mut indices = Vec::with_capacity(ta.data.len());
1995        for i in 0..ta.data.len() {
1996            indices.push(if ta.data[i] >= tb.data[i] { 1.0 } else { 2.0 });
1997        }
1998        Tensor::new(indices, out_shape).ok()?
1999    };
2000    Some(MaxEvaluation {
2001        values: Value::GpuTensor(values),
2002        indices: tensor::tensor_into_value(index_tensor),
2003    })
2004}
2005
2006fn broadcast_reps(a: &[usize], b: &[usize]) -> Option<(Vec<usize>, Vec<usize>, Vec<usize>)> {
2007    let rank = a.len().max(b.len()).max(1);
2008    let mut out = vec![1usize; rank];
2009    let mut aa = vec![1usize; rank];
2010    let mut bb = vec![1usize; rank];
2011    for i in 0..rank {
2012        aa[i] = *a.get(i).unwrap_or(&1);
2013        bb[i] = *b.get(i).unwrap_or(&1);
2014    }
2015    for i in 0..rank {
2016        let (ad, bd) = (aa[i], bb[i]);
2017        if ad == bd {
2018            out[i] = ad;
2019        } else if ad == 1 {
2020            out[i] = bd;
2021        } else if bd == 1 {
2022            out[i] = ad;
2023        } else {
2024            return None;
2025        }
2026    }
2027    let reps_a: Vec<usize> = (0..rank)
2028        .map(|i| if aa[i] == out[i] { 1 } else { out[i] })
2029        .collect();
2030    let reps_b: Vec<usize> = (0..rank)
2031        .map(|i| if bb[i] == out[i] { 1 } else { out[i] })
2032        .collect();
2033    Some((out, reps_a, reps_b))
2034}
2035
2036async fn elementwise_max_gpu_scalar_left(
2037    a: &GpuTensorHandle,
2038    other: &Value,
2039    comparison: ComparisonMethod,
2040) -> Option<MaxEvaluation> {
2041    if comparison != ComparisonMethod::Auto {
2042        return None;
2043    }
2044    let provider = runmat_accelerate_api::provider()?;
2045    let scalar = extract_scalar(other)?;
2046    // Prefer tensorize + elem_max for broader provider compatibility
2047    let values = if let Ok(fill) = provider.fill_like(a, scalar) {
2048        let vals = provider.elem_max(a, &fill).await.ok();
2049        let _ = provider.free(&fill);
2050        vals?
2051    } else {
2052        provider.scalar_max(a, scalar).ok()?
2053    };
2054    // Try device mask; if unavailable, compute on host
2055    let index_tensor = if let Ok(fill) = provider.fill_like(a, scalar) {
2056        if let Ok(mask) = provider.elem_ge(a, &fill).await {
2057            let _ = provider.free(&fill);
2058            let indices = gpu_mask_indices(provider, &mask)?;
2059            let _ = provider.free(&mask);
2060            return Some(MaxEvaluation {
2061                values: Value::GpuTensor(values),
2062                indices: Value::GpuTensor(indices),
2063            });
2064        } else {
2065            let _ = provider.free(&fill);
2066            let ta = gpu_helpers::gather_tensor_async(a).await.ok()?;
2067            let mut indices = Vec::with_capacity(ta.data.len());
2068            for &v in &ta.data {
2069                indices.push(if v >= scalar { 1.0 } else { 2.0 });
2070            }
2071            Tensor::new(indices, ta.shape.clone()).ok()?
2072        }
2073    } else {
2074        let ta = gpu_helpers::gather_tensor_async(a).await.ok()?;
2075        let mut indices = Vec::with_capacity(ta.data.len());
2076        for &v in &ta.data {
2077            indices.push(if v >= scalar { 1.0 } else { 2.0 });
2078        }
2079        Tensor::new(indices, ta.shape.clone()).ok()?
2080    };
2081    Some(MaxEvaluation {
2082        values: Value::GpuTensor(values),
2083        indices: tensor::tensor_into_value(index_tensor),
2084    })
2085}
2086
2087async fn elementwise_max_gpu_scalar_right(
2088    other: &Value,
2089    b: &GpuTensorHandle,
2090    comparison: ComparisonMethod,
2091) -> Option<MaxEvaluation> {
2092    if comparison != ComparisonMethod::Auto {
2093        return None;
2094    }
2095    let provider = runmat_accelerate_api::provider()?;
2096    let scalar = extract_scalar(other)?;
2097    let values = if let Ok(fill) = provider.fill_like(b, scalar) {
2098        let vals = provider.elem_max(&fill, b).await.ok();
2099        let _ = provider.free(&fill);
2100        vals?
2101    } else {
2102        provider.scalar_max(b, scalar).ok()?
2103    };
2104    // Try device mask; if unavailable, compute on host (origin 1 if scalar >= b)
2105    let index_tensor = if let Ok(fill) = provider.fill_like(b, scalar) {
2106        if let Ok(mask) = provider.elem_ge(&fill, b).await {
2107            let _ = provider.free(&fill);
2108            let indices = gpu_mask_indices(provider, &mask)?;
2109            let _ = provider.free(&mask);
2110            return Some(MaxEvaluation {
2111                values: Value::GpuTensor(values),
2112                indices: Value::GpuTensor(indices),
2113            });
2114        } else {
2115            let _ = provider.free(&fill);
2116            let tb = gpu_helpers::gather_tensor_async(b).await.ok()?;
2117            let mut indices = Vec::with_capacity(tb.data.len());
2118            for &v in &tb.data {
2119                indices.push(if scalar >= v { 1.0 } else { 2.0 });
2120            }
2121            Tensor::new(indices, tb.shape.clone()).ok()?
2122        }
2123    } else {
2124        let tb = gpu_helpers::gather_tensor_async(b).await.ok()?;
2125        let mut indices = Vec::with_capacity(tb.data.len());
2126        for &v in &tb.data {
2127            indices.push(if scalar >= v { 1.0 } else { 2.0 });
2128        }
2129        Tensor::new(indices, tb.shape.clone()).ok()?
2130    };
2131    Some(MaxEvaluation {
2132        values: Value::GpuTensor(values),
2133        indices: tensor::tensor_into_value(index_tensor),
2134    })
2135}
2136
2137fn extract_scalar(v: &Value) -> Option<f64> {
2138    match v {
2139        Value::Num(n) => Some(*n),
2140        Value::Int(i) => Some(i.to_f64()),
2141        Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
2142        Value::Tensor(t) if t.data.len() == 1 => t.data.first().copied(),
2143        Value::LogicalArray(l) if l.data.len() == 1 => Some(if l.data[0] != 0 { 1.0 } else { 0.0 }),
2144        _ => None,
2145    }
2146}
2147
2148fn gpu_tensor_is_scalar(handle: &GpuTensorHandle) -> bool {
2149    handle.shape.iter().copied().product::<usize>().max(1) == 1
2150}
2151
2152async fn gpu_tensor_scalar_value(handle: &GpuTensorHandle) -> Option<f64> {
2153    let tensor = gpu_helpers::gather_tensor_async(handle).await.ok()?;
2154    tensor.data.first().copied()
2155}
2156
2157fn gpu_mask_indices(
2158    provider: &dyn AccelProvider,
2159    mask: &GpuTensorHandle,
2160) -> Option<GpuTensorHandle> {
2161    let scaled = provider.scalar_mul(mask, -1.0).ok()?;
2162    let shifted = provider.scalar_add(&scaled, 2.0).ok()?;
2163    let _ = provider.free(&scaled);
2164    Some(shifted)
2165}
2166
2167fn elementwise_real_or_complex(
2168    lhs: Value,
2169    rhs: Value,
2170    nan_mode: ReductionNaN,
2171    comparison: ComparisonMethod,
2172) -> BuiltinResult<MaxEvaluation> {
2173    if let Some(eval) = scalar_elementwise_max(&lhs, &rhs, nan_mode, comparison) {
2174        return Ok(eval);
2175    }
2176    match (
2177        materialize_for_max("max", lhs)?,
2178        materialize_for_max("max", rhs)?,
2179    ) {
2180        (InputData::Complex(a), InputData::Complex(b)) => {
2181            elementwise_complex_max(a, b, nan_mode, comparison)
2182        }
2183        (InputData::Complex(a), InputData::Real(b)) => {
2184            let converted = promote_real_tensor_to_complex(b);
2185            elementwise_complex_max(a, converted, nan_mode, comparison)
2186        }
2187        (InputData::Real(a), InputData::Complex(b)) => {
2188            let converted = promote_real_tensor_to_complex(a);
2189            elementwise_complex_max(converted, b, nan_mode, comparison)
2190        }
2191        (InputData::Real(a), InputData::Real(b)) => {
2192            elementwise_real_max(a, b, nan_mode, comparison)
2193        }
2194    }
2195}
2196
2197fn scalar_real_value(value: &Value) -> Option<f64> {
2198    match value {
2199        Value::Num(n) => Some(*n),
2200        Value::Int(i) => Some(i.to_f64()),
2201        Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
2202        Value::Tensor(t) if t.data.len() == 1 => t.data.first().copied(),
2203        Value::LogicalArray(l) if l.data.len() == 1 => Some(if l.data[0] != 0 { 1.0 } else { 0.0 }),
2204        _ => None,
2205    }
2206}
2207
2208fn scalar_complex_value(value: &Value) -> Option<(f64, f64)> {
2209    match value {
2210        Value::Complex(re, im) => Some((*re, *im)),
2211        Value::ComplexTensor(ct) if ct.data.len() == 1 => ct.data.first().copied(),
2212        _ => None,
2213    }
2214}
2215
2216fn scalar_elementwise_max(
2217    lhs: &Value,
2218    rhs: &Value,
2219    nan_mode: ReductionNaN,
2220    comparison: ComparisonMethod,
2221) -> Option<MaxEvaluation> {
2222    let left = scalar_complex_value(lhs).or_else(|| scalar_real_value(lhs).map(|v| (v, 0.0)))?;
2223    let right = scalar_complex_value(rhs).or_else(|| scalar_real_value(rhs).map(|v| (v, 0.0)))?;
2224    let (ar, ai) = left;
2225    let (br, bi) = right;
2226    if ai != 0.0 || bi != 0.0 {
2227        let (value, origin) = choose_complex_elementwise((ar, ai), (br, bi), nan_mode, comparison);
2228        return Some(MaxEvaluation {
2229            values: Value::Complex(value.0, value.1),
2230            indices: Value::Num(origin),
2231        });
2232    }
2233    let (value, origin) = choose_real_elementwise(ar, br, nan_mode, comparison);
2234    Some(MaxEvaluation {
2235        values: Value::Num(value),
2236        indices: Value::Num(origin),
2237    })
2238}
2239
2240fn elementwise_real_max(
2241    lhs: Tensor,
2242    rhs: Tensor,
2243    nan_mode: ReductionNaN,
2244    comparison: ComparisonMethod,
2245) -> BuiltinResult<MaxEvaluation> {
2246    let plan = BroadcastPlan::new(&lhs.shape, &rhs.shape)
2247        .map_err(|err| max_size_mismatch(format!("max: {err}")))?;
2248    let mut values = vec![0.0f64; plan.len()];
2249    let mut indices = vec![0.0f64; plan.len()];
2250
2251    for (offset, index_a, index_b) in plan.iter() {
2252        let a = lhs.data.get(index_a).copied().unwrap_or(f64::NAN);
2253        let b = rhs.data.get(index_b).copied().unwrap_or(f64::NAN);
2254        let (value, origin) = choose_real_elementwise(a, b, nan_mode, comparison);
2255        values[offset] = value;
2256        indices[offset] = origin;
2257    }
2258
2259    let value_tensor = Tensor::new(values, plan.output_shape().to_vec())
2260        .map_err(|e| max_internal_error(format!("max: {e}")))?;
2261    let index_tensor = Tensor::new(indices, plan.output_shape().to_vec())
2262        .map_err(|e| max_internal_error(format!("max: {e}")))?;
2263
2264    Ok(MaxEvaluation {
2265        values: tensor::tensor_into_value(value_tensor),
2266        indices: tensor::tensor_into_value(index_tensor),
2267    })
2268}
2269
2270fn elementwise_complex_max(
2271    lhs: ComplexTensor,
2272    rhs: ComplexTensor,
2273    nan_mode: ReductionNaN,
2274    comparison: ComparisonMethod,
2275) -> BuiltinResult<MaxEvaluation> {
2276    let plan = BroadcastPlan::new(&lhs.shape, &rhs.shape)
2277        .map_err(|err| max_size_mismatch(format!("max: {err}")))?;
2278    let mut values = vec![(0.0f64, 0.0f64); plan.len()];
2279    let mut indices = vec![0.0f64; plan.len()];
2280
2281    for (offset, index_a, index_b) in plan.iter() {
2282        let a = lhs
2283            .data
2284            .get(index_a)
2285            .copied()
2286            .unwrap_or((f64::NAN, f64::NAN));
2287        let b = rhs
2288            .data
2289            .get(index_b)
2290            .copied()
2291            .unwrap_or((f64::NAN, f64::NAN));
2292        let (value, origin) = choose_complex_elementwise(a, b, nan_mode, comparison);
2293        values[offset] = value;
2294        indices[offset] = origin;
2295    }
2296
2297    let value_tensor = ComplexTensor::new(values, plan.output_shape().to_vec())
2298        .map_err(|e| max_internal_error(format!("max: {e}")))?;
2299    let index_tensor = Tensor::new(indices, plan.output_shape().to_vec())
2300        .map_err(|e| max_internal_error(format!("max: {e}")))?;
2301
2302    Ok(MaxEvaluation {
2303        values: complex_tensor_into_value(value_tensor),
2304        indices: tensor::tensor_into_value(index_tensor),
2305    })
2306}
2307
2308fn promote_real_tensor_to_complex(tensor: Tensor) -> ComplexTensor {
2309    let data = tensor
2310        .data
2311        .iter()
2312        .copied()
2313        .map(|re| (re, 0.0))
2314        .collect::<Vec<_>>();
2315    ComplexTensor {
2316        data,
2317        shape: tensor.shape.clone(),
2318        rows: tensor.rows,
2319        cols: tensor.cols,
2320    }
2321}
2322
2323fn choose_real_elementwise(
2324    a: f64,
2325    b: f64,
2326    nan_mode: ReductionNaN,
2327    comparison: ComparisonMethod,
2328) -> (f64, f64) {
2329    match (a.is_nan(), b.is_nan()) {
2330        (true, true) => (f64::NAN, 1.0),
2331        (true, false) if nan_mode == ReductionNaN::Omit => (b, 2.0),
2332        (false, true) if nan_mode == ReductionNaN::Omit => (a, 1.0),
2333        (true, false) => (f64::NAN, 1.0),
2334        (false, true) => (f64::NAN, 2.0),
2335        (false, false) => {
2336            if should_replace_real(a, b, comparison) {
2337                (b, 2.0)
2338            } else {
2339                (a, 1.0)
2340            }
2341        }
2342    }
2343}
2344
2345fn choose_complex_elementwise(
2346    a: (f64, f64),
2347    b: (f64, f64),
2348    nan_mode: ReductionNaN,
2349    comparison: ComparisonMethod,
2350) -> ((f64, f64), f64) {
2351    let a_nan = a.0.is_nan() || a.1.is_nan();
2352    let b_nan = b.0.is_nan() || b.1.is_nan();
2353    match (a_nan, b_nan) {
2354        (true, true) => ((f64::NAN, f64::NAN), 1.0),
2355        (true, false) if nan_mode == ReductionNaN::Omit => (b, 2.0),
2356        (false, true) if nan_mode == ReductionNaN::Omit => (a, 1.0),
2357        (true, false) => ((f64::NAN, f64::NAN), 1.0),
2358        (false, true) => ((f64::NAN, f64::NAN), 2.0),
2359        (false, false) => {
2360            if should_replace_complex(a, b, comparison) {
2361                (b, 2.0)
2362            } else {
2363                (a, 1.0)
2364            }
2365        }
2366    }
2367}
2368
2369#[cfg(test)]
2370pub(crate) mod tests {
2371    use super::*;
2372    #[cfg(feature = "wgpu")]
2373    use crate::builtins::common::test_support;
2374    use futures::executor::block_on;
2375    #[cfg(feature = "wgpu")]
2376    use runmat_accelerate_api::HostTensorView;
2377    use runmat_builtins::{IntValue, Tensor, Value};
2378
2379    fn max_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
2380        block_on(super::max_builtin(value, rest))
2381    }
2382
2383    fn nanmax_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
2384        block_on(super::nanmax_builtin(value, rest))
2385    }
2386
2387    #[test]
2388    fn max_type_with_two_args_returns_tensor() {
2389        let out = max_type(
2390            &[Type::Tensor { shape: None }, Type::Tensor { shape: None }],
2391            &ResolveContext::new(Vec::new()),
2392        );
2393        assert_eq!(out, Type::tensor());
2394    }
2395
2396    #[test]
2397    fn max_descriptor_signatures_cover_core_forms() {
2398        let labels: Vec<&str> = MAX_DESCRIPTOR
2399            .signatures
2400            .iter()
2401            .map(|sig| sig.label)
2402            .collect();
2403        assert!(labels.contains(&"M = max(A)"));
2404        assert!(labels.contains(&"[M, I] = max(A)"));
2405        assert!(labels.contains(&"M = max(A, B)"));
2406        assert!(labels.contains(&"[M, I] = max(A, B)"));
2407        assert!(labels.contains(&"M = max(A, [], dim)"));
2408        assert!(labels.contains(&"M = max(A, [], \"all\")"));
2409        assert!(labels.contains(&"M = max(A, [], \"ComparisonMethod\", method)"));
2410        assert!(labels.contains(&"M = max(A, B, \"ComparisonMethod\", method)"));
2411    }
2412
2413    #[test]
2414    fn nanmax_descriptor_signatures_cover_core_forms() {
2415        let labels: Vec<&str> = NANMAX_DESCRIPTOR
2416            .signatures
2417            .iter()
2418            .map(|sig| sig.label)
2419            .collect();
2420        assert!(labels.contains(&"M = nanmax(A)"));
2421        assert!(labels.contains(&"[M, I] = nanmax(A)"));
2422        assert!(labels.contains(&"M = nanmax(A, B)"));
2423        assert!(labels.contains(&"[M, I] = nanmax(A, B)"));
2424        assert!(labels.contains(&"M = nanmax(A, [], dim)"));
2425        assert!(labels.contains(&"[M, I] = nanmax(A, [], dim)"));
2426        assert!(labels.contains(&"M = nanmax(A, [], vecdim)"));
2427        assert!(labels.contains(&"M = nanmax(A, [], \"all\")"));
2428        assert!(labels.contains(&"M = nanmax(A, [], \"linear\")"));
2429        assert!(labels.contains(&"M = nanmax(A, [], \"ComparisonMethod\", method)"));
2430        assert!(labels.contains(&"M = nanmax(A, B, \"ComparisonMethod\", method)"));
2431    }
2432
2433    #[test]
2434    fn max_descriptor_errors_have_stable_codes() {
2435        assert!(MAX_DESCRIPTOR
2436            .errors
2437            .iter()
2438            .any(|error| error.code == MAX_ERROR_INVALID_ARGUMENT.code));
2439        assert!(MAX_DESCRIPTOR
2440            .errors
2441            .iter()
2442            .any(|error| error.code == MAX_ERROR_INVALID_INPUT.code));
2443        assert!(MAX_DESCRIPTOR
2444            .errors
2445            .iter()
2446            .any(|error| error.code == MAX_ERROR_SIZE_MISMATCH.code));
2447        assert!(MAX_DESCRIPTOR
2448            .errors
2449            .iter()
2450            .any(|error| error.code == MAX_ERROR_INTERNAL.code));
2451    }
2452
2453    fn evaluate(value: Value, rest: &[Value]) -> BuiltinResult<MaxEvaluation> {
2454        block_on(super::evaluate(value, rest))
2455    }
2456
2457    fn placeholder() -> Value {
2458        let tensor = Tensor::new(Vec::<f64>::new(), vec![0, 0]).unwrap();
2459        Value::Tensor(tensor)
2460    }
2461
2462    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2463    #[test]
2464    fn max_scalar_returns_input() {
2465        let result = max_builtin(Value::Num(5.0), Vec::new()).expect("max");
2466        assert_eq!(result, Value::Num(5.0));
2467    }
2468
2469    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2470    #[test]
2471    fn max_vector_with_indices() {
2472        let tensor = Tensor::new(vec![3.0, 1.0, 5.0], vec![3, 1]).unwrap();
2473        let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
2474        let (values, indices) = eval.into_pair();
2475        assert_eq!(values, Value::Num(5.0));
2476        assert_eq!(indices, Value::Num(3.0));
2477    }
2478
2479    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2480    #[test]
2481    fn max_native_integer_reduction_preserves_uint64_values_and_indices() {
2482        let input = Tensor::new_integer(
2483            runmat_builtins::IntegerStorage::U64(vec![u64::MAX - 1, u64::MAX, 3, 2]),
2484            vec![2, 2],
2485        )
2486        .expect("input");
2487        let (values, indices) = evaluate(Value::Tensor(input), &[])
2488            .expect("max")
2489            .into_pair();
2490        assert_eq!(
2491            values,
2492            Value::Tensor(
2493                Tensor::new_integer(
2494                    runmat_builtins::IntegerStorage::U64(vec![u64::MAX, 3]),
2495                    vec![1, 2],
2496                )
2497                .expect("values"),
2498            )
2499        );
2500        assert_eq!(
2501            indices,
2502            Value::Tensor(Tensor::new(vec![2.0, 1.0], vec![1, 2]).expect("indices")),
2503        );
2504    }
2505
2506    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2507    #[test]
2508    fn max_native_integer_abs_all_uses_exact_int64_minimum() {
2509        let input = Tensor::new_integer(
2510            runmat_builtins::IntegerStorage::I64(vec![i64::MIN, -3, 3]),
2511            vec![3, 1],
2512        )
2513        .expect("input");
2514        let args = vec![
2515            placeholder(),
2516            Value::from("all"),
2517            Value::from("ComparisonMethod"),
2518            Value::from("abs"),
2519        ];
2520        let (values, indices) = evaluate(Value::Tensor(input), &args)
2521            .expect("max")
2522            .into_pair();
2523        assert_eq!(values, Value::Int(IntValue::I64(i64::MIN)));
2524        assert_eq!(indices, Value::Num(1.0));
2525    }
2526
2527    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2528    #[test]
2529    fn max_native_integer_empty_array_retains_its_class() {
2530        let input =
2531            Tensor::new_integer(runmat_builtins::IntegerStorage::U32(Vec::new()), vec![0, 0])
2532                .expect("input");
2533        let (values, indices) = evaluate(Value::Tensor(input), &[])
2534            .expect("max")
2535            .into_pair();
2536        assert_eq!(
2537            values,
2538            Value::Tensor(
2539                Tensor::new_integer(runmat_builtins::IntegerStorage::U32(Vec::new()), vec![0, 0])
2540                    .expect("values"),
2541            )
2542        );
2543        assert_eq!(
2544            indices,
2545            Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).expect("indices")),
2546        );
2547    }
2548
2549    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2550    #[test]
2551    fn max_native_integer_empty_all_reduction_remains_empty() {
2552        let input =
2553            Tensor::new_integer(runmat_builtins::IntegerStorage::I16(Vec::new()), vec![0, 0])
2554                .expect("input");
2555        let values = evaluate(Value::Tensor(input), &[placeholder(), Value::from("all")])
2556            .expect("max")
2557            .into_value();
2558        assert_eq!(
2559            values,
2560            Value::Tensor(
2561                Tensor::new_integer(runmat_builtins::IntegerStorage::I16(Vec::new()), vec![0, 0])
2562                    .expect("values"),
2563            )
2564        );
2565    }
2566
2567    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2568    #[test]
2569    fn max_row_vector_reduces_across_columns() {
2570        let tensor = Tensor::new(vec![3.0, 1.0, 5.0], vec![1, 3]).unwrap();
2571        let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
2572        let (values, indices) = eval.into_pair();
2573        assert_eq!(values, Value::Num(5.0));
2574        assert_eq!(indices, Value::Num(3.0));
2575    }
2576
2577    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2578    #[test]
2579    fn max_single_row_vector_reduces_across_columns() {
2580        let tensor = Tensor::from_f32(vec![3.0, 1.0, 5.0], vec![1, 3]).unwrap();
2581        let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
2582        let (values, indices) = eval.into_pair();
2583        assert_eq!(values, Value::Num(5.0));
2584        assert_eq!(indices, Value::Num(3.0));
2585    }
2586
2587    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2588    #[test]
2589    fn max_registered_single_row_vector_reduces_across_columns() {
2590        let tensor = Tensor::from_f32(vec![3.0, 1.0, 5.0], vec![1, 3]).unwrap();
2591        let value = block_on(crate::call_builtin_async_with_outputs(
2592            "max",
2593            &[Value::Tensor(tensor)],
2594            1,
2595        ))
2596        .expect("dispatch max");
2597        match value {
2598            Value::OutputList(values) => {
2599                assert_eq!(values.len(), 1);
2600                assert_eq!(values[0], Value::Num(5.0));
2601            }
2602            other => assert_eq!(other, Value::Num(5.0)),
2603        }
2604    }
2605
2606    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2607    #[test]
2608    fn max_matrix_default_dimension() {
2609        let tensor = Tensor::new(vec![3.0, 4.0, 1.0, 2.0, 5.0, 6.0], vec![2, 3]).unwrap();
2610        let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
2611        let (values, indices) = eval.into_pair();
2612        match values {
2613            Value::Tensor(t) => {
2614                assert_eq!(t.shape, vec![1, 3]);
2615                assert_eq!(t.data, vec![4.0, 2.0, 6.0]);
2616            }
2617            other => panic!("expected tensor, got {other:?}"),
2618        }
2619        match indices {
2620            Value::Tensor(t) => {
2621                assert_eq!(t.data, vec![2.0, 2.0, 2.0]);
2622            }
2623            other => panic!("expected tensor, got {other:?}"),
2624        }
2625    }
2626
2627    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2628    #[test]
2629    fn max_all_linear_index() {
2630        let tensor =
2631            Tensor::new((1..=12).map(|v| v as f64).collect::<Vec<_>>(), vec![3, 4]).unwrap();
2632        let args = vec![placeholder(), Value::from("all")];
2633        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2634        let (values, indices) = eval.into_pair();
2635        assert_eq!(values, Value::Num(12.0));
2636        assert_eq!(indices, Value::Num(12.0));
2637
2638        let args_linear = vec![placeholder(), Value::from("linear")];
2639        let eval = evaluate(
2640            Value::Tensor(Tensor::new(vec![2.0, 3.0], vec![1, 2]).unwrap()),
2641            &args_linear,
2642        )
2643        .expect("evaluate");
2644        let (values, indices) = eval.into_pair();
2645        assert_eq!(values, Value::Num(3.0));
2646        assert_eq!(indices, Value::Num(2.0));
2647    }
2648
2649    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2650    #[test]
2651    fn max_with_omitnan() {
2652        let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0], vec![3, 1]).unwrap();
2653        let args = vec![placeholder(), Value::from("omitnan")];
2654        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2655        let (values, indices) = eval.into_pair();
2656        assert_eq!(values, Value::Num(4.0));
2657        assert_eq!(indices, Value::Num(2.0));
2658    }
2659
2660    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2661    #[test]
2662    fn max_omitnan_all_nan_slice() {
2663        let tensor = Tensor::new(vec![f64::NAN, f64::NAN], vec![2, 1]).unwrap();
2664        let args = vec![placeholder(), Value::from("omitnan")];
2665        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2666        let (values, indices) = eval.into_pair();
2667        match values {
2668            Value::Num(v) => assert!(v.is_nan()),
2669            other => panic!("expected scalar NaN, got {other:?}"),
2670        }
2671        match indices {
2672            Value::Num(v) => assert!(v.is_nan()),
2673            other => panic!("expected scalar NaN index, got {other:?}"),
2674        }
2675    }
2676
2677    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2678    #[test]
2679    fn nanmax_reduction_omits_nan_by_default() {
2680        let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0, f64::NAN], vec![2, 2]).unwrap();
2681        let result = nanmax_builtin(Value::Tensor(tensor), Vec::new()).expect("nanmax");
2682        match result {
2683            Value::Tensor(t) => {
2684                assert_eq!(t.shape, vec![1, 2]);
2685                assert_eq!(t.data[0], 4.0);
2686                assert_eq!(t.data[1], 2.0);
2687            }
2688            other => panic!("expected tensor, got {other:?}"),
2689        }
2690    }
2691
2692    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2693    #[test]
2694    fn nanmax_reduction_accepts_empty_placeholder_and_dim() {
2695        let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0, f64::NAN], vec![2, 2]).unwrap();
2696        let result = nanmax_builtin(Value::Tensor(tensor), vec![placeholder(), Value::Num(2.0)])
2697            .expect("nanmax dim");
2698        match result {
2699            Value::Tensor(t) => {
2700                assert_eq!(t.shape, vec![2, 1]);
2701                assert_eq!(t.data[0], 2.0);
2702                assert_eq!(t.data[1], 4.0);
2703            }
2704            other => panic!("expected tensor, got {other:?}"),
2705        }
2706    }
2707
2708    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2709    #[test]
2710    fn nanmax_output_count_returns_indices_and_zero_output_list() {
2711        let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0], vec![3, 1]).unwrap();
2712        let _guard = crate::output_count::push_output_count(Some(2));
2713        let result = nanmax_builtin(Value::Tensor(tensor), Vec::new()).expect("nanmax outputs");
2714        match result {
2715            Value::OutputList(values) => {
2716                assert_eq!(values.len(), 2);
2717                assert_eq!(values[0], Value::Num(4.0));
2718                assert_eq!(values[1], Value::Num(2.0));
2719            }
2720            other => panic!("expected output list, got {other:?}"),
2721        }
2722        drop(_guard);
2723
2724        let _guard = crate::output_count::push_output_count(Some(0));
2725        let result = nanmax_builtin(Value::Num(1.0), Vec::new()).expect("nanmax zero outputs");
2726        assert_eq!(result, Value::OutputList(Vec::new()));
2727    }
2728
2729    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2730    #[test]
2731    fn max_reduction_abs_comparison() {
2732        let tensor = Tensor::new(vec![1.0, -3.0, -2.0, 4.0], vec![2, 2]).unwrap();
2733        let args = vec![
2734            placeholder(),
2735            Value::from("ComparisonMethod"),
2736            Value::from("abs"),
2737        ];
2738        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2739        let (values, indices) = eval.into_pair();
2740        match values {
2741            Value::Tensor(t) => {
2742                assert_eq!(t.shape, vec![1, 2]);
2743                assert_eq!(t.data, vec![-3.0, 4.0]);
2744            }
2745            other => panic!("expected tensor result, got {other:?}"),
2746        }
2747        match indices {
2748            Value::Tensor(t) => {
2749                assert_eq!(t.data, vec![2.0, 2.0]);
2750            }
2751            other => panic!("expected tensor indices, got {other:?}"),
2752        }
2753    }
2754
2755    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2756    #[test]
2757    fn max_reduction_complex_real_comparison() {
2758        let tensor = ComplexTensor::new(vec![(1.0, 2.0), (0.5, 5.0)], vec![2, 1]).expect("tensor");
2759        let args = vec![
2760            placeholder(),
2761            Value::from("ComparisonMethod"),
2762            Value::from("real"),
2763        ];
2764        let eval = evaluate(Value::ComplexTensor(tensor), &args).expect("evaluate");
2765        let (values, indices) = eval.into_pair();
2766        match values {
2767            Value::Complex(re, im) => {
2768                assert!((re - 1.0).abs() < 1e-12);
2769                assert!((im - 2.0).abs() < 1e-12);
2770            }
2771            other => panic!("expected complex scalar, got {other:?}"),
2772        }
2773        assert_eq!(indices, Value::Num(1.0));
2774    }
2775
2776    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2777    #[test]
2778    fn max_elementwise_broadcast() {
2779        let lhs = Tensor::new(vec![1.0, 4.0, 7.0], vec![1, 3]).unwrap();
2780        let rhs = Tensor::new(vec![2.0, 3.0, 5.0], vec![3, 1]).unwrap();
2781        let eval = evaluate(Value::Tensor(lhs), &[Value::Tensor(rhs)]).expect("evaluate");
2782        let (values, indices) = eval.into_pair();
2783        match values {
2784            Value::Tensor(t) => {
2785                assert_eq!(t.shape, vec![3, 3]);
2786                assert_eq!([t.data[0], t.data[3], t.data[6]], [2.0, 4.0, 7.0]);
2787                assert_eq!([t.data[1], t.data[4], t.data[7]], [3.0, 4.0, 7.0]);
2788                assert_eq!([t.data[2], t.data[5], t.data[8]], [5.0, 5.0, 7.0]);
2789            }
2790            other => panic!("expected tensor, got {other:?}"),
2791        }
2792        match indices {
2793            Value::Tensor(t) => {
2794                assert_eq!(t.shape, vec![3, 3]);
2795                assert_eq!([t.data[0], t.data[3], t.data[6]], [2.0, 1.0, 1.0]);
2796                assert_eq!([t.data[1], t.data[4], t.data[7]], [2.0, 1.0, 1.0]);
2797                assert_eq!([t.data[2], t.data[5], t.data[8]], [2.0, 2.0, 1.0]);
2798            }
2799            other => panic!("expected tensor, got {other:?}"),
2800        }
2801    }
2802
2803    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2804    #[test]
2805    fn max_elementwise_abs_comparison() {
2806        let lhs = Tensor::new(vec![-2.0, 1.0], vec![2, 1]).unwrap();
2807        let rhs = Tensor::new(vec![1.5, -3.0], vec![2, 1]).unwrap();
2808        let args = vec![
2809            Value::Tensor(rhs),
2810            Value::from("ComparisonMethod"),
2811            Value::from("abs"),
2812        ];
2813        let eval = evaluate(Value::Tensor(lhs), &args).expect("evaluate");
2814        let (values, indices) = eval.into_pair();
2815        match values {
2816            Value::Tensor(t) => {
2817                assert_eq!(t.data, vec![-2.0, -3.0]);
2818            }
2819            other => panic!("expected tensor, got {other:?}"),
2820        }
2821        match indices {
2822            Value::Tensor(t) => {
2823                assert_eq!(t.data, vec![1.0, 2.0]);
2824            }
2825            other => panic!("expected tensor, got {other:?}"),
2826        }
2827    }
2828
2829    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2830    #[test]
2831    fn max_elementwise_omitnan_chooses_non_nan_side() {
2832        let lhs = Tensor::new(vec![f64::NAN, 2.0, f64::NAN], vec![3, 1]).unwrap();
2833        let rhs = Tensor::new(vec![3.0, f64::NAN, f64::NAN], vec![3, 1]).unwrap();
2834        let eval = evaluate(
2835            Value::Tensor(lhs),
2836            &[Value::Tensor(rhs), Value::from("omitnan")],
2837        )
2838        .expect("evaluate");
2839        let (values, indices) = eval.into_pair();
2840        match values {
2841            Value::Tensor(t) => {
2842                assert_eq!(t.shape, vec![3, 1]);
2843                assert_eq!(t.data[0], 3.0);
2844                assert_eq!(t.data[1], 2.0);
2845                assert!(t.data[2].is_nan());
2846            }
2847            other => panic!("expected tensor, got {other:?}"),
2848        }
2849        match indices {
2850            Value::Tensor(t) => {
2851                assert_eq!(t.data[0], 2.0);
2852                assert_eq!(t.data[1], 1.0);
2853                assert_eq!(t.data[2], 1.0);
2854            }
2855            other => panic!("expected tensor, got {other:?}"),
2856        }
2857    }
2858
2859    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2860    #[test]
2861    fn nanmax_elementwise_omits_nan_by_default() {
2862        let lhs = Tensor::new(vec![f64::NAN, 2.0], vec![2, 1]).unwrap();
2863        let rhs = Tensor::new(vec![3.0, f64::NAN], vec![2, 1]).unwrap();
2864        let result = nanmax_builtin(Value::Tensor(lhs), vec![Value::Tensor(rhs)]).expect("nanmax");
2865        match result {
2866            Value::Tensor(t) => assert_eq!(t.data, vec![3.0, 2.0]),
2867            other => panic!("expected tensor, got {other:?}"),
2868        }
2869    }
2870
2871    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2872    #[test]
2873    #[cfg(feature = "wgpu")]
2874    fn nanmax_gpu_omitnan_gathers_to_host_result() {
2875        let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0, f64::NAN], vec![2, 2]).unwrap();
2876        test_support::with_test_provider(|provider| {
2877            let view = HostTensorView {
2878                data: &tensor.data,
2879                shape: &tensor.shape,
2880            };
2881            let handle = provider.upload(&view).expect("upload");
2882            let result = nanmax_builtin(Value::GpuTensor(handle), Vec::new()).expect("nanmax gpu");
2883            match result {
2884                Value::Tensor(t) => {
2885                    assert_eq!(t.shape, vec![1, 2]);
2886                    assert_eq!(t.data, vec![4.0, 2.0]);
2887                }
2888                other => panic!("expected host tensor fallback, got {other:?}"),
2889            }
2890        });
2891    }
2892
2893    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2894    #[test]
2895    fn max_complex_real_comparison() {
2896        let lhs = ComplexTensor::new(vec![(1.0, 2.0)], vec![1, 1]).unwrap();
2897        let rhs = ComplexTensor::new(vec![(0.5, 5.0)], vec![1, 1]).unwrap();
2898        let args = vec![
2899            Value::ComplexTensor(rhs),
2900            Value::from("ComparisonMethod"),
2901            Value::from("real"),
2902        ];
2903        let eval = evaluate(Value::ComplexTensor(lhs), &args).expect("evaluate");
2904        let (values, indices) = eval.into_pair();
2905        assert_eq!(values, Value::Complex(1.0, 2.0));
2906        assert_eq!(indices, Value::Num(1.0));
2907    }
2908
2909    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2910    #[test]
2911    fn max_dimension_argument_parsing() {
2912        let tensor = Tensor::new(vec![3.0, 4.0, 1.0, 2.0], vec![2, 2]).unwrap();
2913        let dims = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
2914        let args = vec![placeholder(), Value::Tensor(dims)];
2915        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2916        let (values, indices) = eval.into_pair();
2917        assert_eq!(values, Value::Num(4.0));
2918        assert_eq!(indices, Value::Num(2.0));
2919    }
2920
2921    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2922    #[test]
2923    fn max_vecdim_duplicate_entries() {
2924        let tensor = Tensor::new(vec![5.0, 2.0, 7.0, 1.0], vec![2, 2]).unwrap();
2925        let dims = Tensor::new(vec![1.0, 1.0, 2.0], vec![3, 1]).unwrap();
2926        let args = vec![placeholder(), Value::Tensor(dims)];
2927        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2928        let (values, indices) = eval.into_pair();
2929        assert_eq!(values, Value::Num(7.0));
2930        assert_eq!(indices, Value::Num(3.0));
2931    }
2932
2933    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2934    #[test]
2935    fn max_dimension_gpu_argument_errors() {
2936        let tensor = Tensor::new(vec![3.0, 1.0], vec![2, 1]).unwrap();
2937        let dim_handle = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
2938            shape: vec![1, 1],
2939            device_id: 0,
2940            buffer_id: 42,
2941        });
2942        let err = evaluate(Value::Tensor(tensor), &[placeholder(), dim_handle])
2943            .expect_err("expected error");
2944        assert_eq!(err.identifier(), MAX_ERROR_INVALID_ARGUMENT.identifier);
2945        assert!(err
2946            .message()
2947            .contains("dimension arguments must reside on the host"));
2948    }
2949
2950    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2951    #[test]
2952    fn max_invalid_comparison_method_errors() {
2953        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
2954        let args = vec![
2955            placeholder(),
2956            Value::from("ComparisonMethod"),
2957            Value::from("chebyshev"),
2958        ];
2959        let err = evaluate(Value::Tensor(tensor), &args).expect_err("expected error");
2960        assert_eq!(err.identifier(), MAX_ERROR_INVALID_ARGUMENT.identifier);
2961        assert!(err.message().contains("unsupported ComparisonMethod"));
2962    }
2963
2964    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2965    #[test]
2966    #[cfg(feature = "wgpu")]
2967    fn max_gpu_dim1_matches_cpu() {
2968        let tensor = Tensor::new(vec![3.0, 1.0, 2.0, 4.0], vec![2, 2]).unwrap();
2969        let eval_cpu = evaluate(Value::Tensor(tensor.clone()), &[]).expect("cpu");
2970        let (values_cpu, indices_cpu) = eval_cpu.into_pair();
2971
2972        test_support::with_test_provider(|provider| {
2973            let view = HostTensorView {
2974                data: &tensor.data,
2975                shape: &tensor.shape,
2976            };
2977            let handle = provider.upload(&view).expect("upload");
2978            let eval_gpu = evaluate(Value::GpuTensor(handle), &[]).expect("gpu");
2979            let (values_gpu, indices_gpu) = eval_gpu.into_pair();
2980            match (&values_gpu, &indices_gpu) {
2981                (Value::GpuTensor(_), Value::GpuTensor(_)) => {}
2982                other => panic!("expected GPU tensors, got {other:?}"),
2983            }
2984            let gathered_vals = test_support::gather(values_gpu).expect("gather values");
2985            let gathered_idx = test_support::gather(indices_gpu).expect("gather indices");
2986            let expected_vals = match values_cpu {
2987                Value::Tensor(t) => t,
2988                other => panic!("expected tensor values from cpu eval, got {other:?}"),
2989            };
2990            let expected_idx = match indices_cpu {
2991                Value::Tensor(t) => t,
2992                other => panic!("expected tensor indices from cpu eval, got {other:?}"),
2993            };
2994            assert_eq!(gathered_vals.shape, expected_vals.shape);
2995            assert_eq!(gathered_vals.data, expected_vals.data);
2996            assert_eq!(gathered_idx.shape, expected_idx.shape);
2997            assert_eq!(gathered_idx.data, expected_idx.data);
2998        });
2999    }
3000
3001    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3002    #[test]
3003    fn max_dimension_numeric_argument() {
3004        let tensor = Tensor::new(vec![3.0, 4.0, 1.0, 2.0], vec![2, 2]).unwrap();
3005        let args = vec![placeholder(), Value::Num(2.0)];
3006        let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
3007        let (values, indices) = eval.into_pair();
3008        match values {
3009            Value::Tensor(t) => {
3010                assert_eq!(t.shape, vec![2, 1]);
3011                assert_eq!(t.data, vec![3.0, 4.0]);
3012            }
3013            other => panic!("expected tensor, got {other:?}"),
3014        }
3015        match indices {
3016            Value::Tensor(t) => {
3017                assert_eq!(t.data, vec![1.0, 1.0]);
3018            }
3019            other => panic!("expected tensor, got {other:?}"),
3020        }
3021    }
3022
3023    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3024    #[test]
3025    fn max_complex_auto_comparison() {
3026        let lhs = ComplexTensor::new(vec![(1.0, 2.0)], vec![1, 1]).unwrap();
3027        let rhs = ComplexTensor::new(vec![(2.0, 1.0)], vec![1, 1]).unwrap();
3028        let eval =
3029            evaluate(Value::ComplexTensor(lhs), &[Value::ComplexTensor(rhs)]).expect("evaluate");
3030        let (values, indices) = eval.into_pair();
3031        assert_eq!(values, Value::Complex(1.0, 2.0));
3032        assert_eq!(indices, Value::Num(1.0));
3033    }
3034
3035    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3036    #[test]
3037    fn max_scalar_pair_arguments() {
3038        let args = vec![Value::Num(2.0)];
3039        let result = max_builtin(Value::Num(3.0), args).expect("max");
3040        assert_eq!(result, Value::Num(3.0));
3041    }
3042
3043    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3044    #[test]
3045    fn max_rejects_invalid_dimension() {
3046        let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
3047        let args = vec![placeholder(), Value::Int(IntValue::I32(0))];
3048        let err = evaluate(Value::Tensor(tensor), &args).expect_err("expected error");
3049        assert_eq!(err.identifier(), MAX_ERROR_INVALID_ARGUMENT.identifier);
3050        assert!(err.message().contains("dimension must be >= 1"));
3051    }
3052}