Skip to main content

runmat_runtime/builtins/array/creation/
meshgrid.rs

1//! MATLAB-compatible `meshgrid` builtin with GPU-aware semantics.
2
3use runmat_builtins::{
4    BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor, BuiltinIntegerComputationDomain,
5    BuiltinIntegerInputAvailability, BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule,
6    BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
7};
8use std::cmp::max;
9
10use runmat_accelerate_api::{GpuTensorHandle, GpuTensorStorage};
11use runmat_builtins::{
12    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
13    BuiltinExtensionMode, BuiltinOutputMode, BuiltinParamArity, BuiltinParamDescriptor,
14    BuiltinParamType, BuiltinSignatureDescriptor, ResolveContext, Type,
15};
16use runmat_value::{
17    ComplexStorage, ComplexTensor, IntegerComplexStorage, IntegerStorage, NumericDType,
18    NumericStorage, Tensor, Value,
19};
20
21use crate::builtins::array::type_resolvers::size_vector_len;
22use runmat_macros::runtime_builtin;
23
24use crate::build_runtime_error;
25use crate::builtins::common::gpu_helpers;
26use crate::builtins::common::random_args::{complex_tensor_into_value, keyword_of};
27use crate::builtins::common::residency::{sequence_gpu_preference, SequenceIntent};
28use crate::builtins::common::spec::{
29    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
30    ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
31};
32use crate::builtins::common::tensor;
33
34const MESHGRID_LIKE_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
35    id: "meshgrid-like",
36    mode: BuiltinExtensionMode::RunMatOnly,
37    description: "the meshgrid \"like\" prototype selector is a RunMat extension",
38    error_identifier: Some("RunMat:compatibility:MeshgridLikeExtension"),
39};
40
41const MESHGRID_COMPLEX_AXES_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
42    id: "meshgrid-complex-axes",
43    mode: BuiltinExtensionMode::RunMatOnly,
44    description: "complex meshgrid axes are a RunMat extension",
45    error_identifier: Some("RunMat:compatibility:MeshgridComplexAxesExtension"),
46};
47
48pub const MESHGRID_EXTENSIONS: [BuiltinExtensionDescriptor; 2] =
49    [MESHGRID_LIKE_EXTENSION, MESHGRID_COMPLEX_AXES_EXTENSION];
50
51#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::array::creation::meshgrid")]
52pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
53    name: "meshgrid",
54    op_kind: GpuOpKind::Custom("array_construct"),
55    supported_precisions: &[ScalarType::F32, ScalarType::F64],
56    broadcast: BroadcastSemantics::Matlab,
57    provider_hooks: &[ProviderHook::Custom("meshgrid")],
58    constant_strategy: ConstantStrategy::InlineLiteral,
59    residency: ResidencyPolicy::NewHandle,
60    nan_mode: ReductionNaN::Include,
61    two_pass_threshold: None,
62    workgroup_size: None,
63    accepts_nan_mode: false,
64    notes: "Providers may supply a dedicated meshgrid hook; until then the runtime builds grids on the host and uploads them when GPU residency is requested.",
65};
66
67fn builtin_error(message: impl Into<String>) -> crate::RuntimeError {
68    build_runtime_error(message)
69        .with_builtin("meshgrid")
70        .build()
71}
72
73#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::array::creation::meshgrid")]
74pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
75    name: "meshgrid",
76    shape: ShapeRequirements::Any,
77    constant_strategy: ConstantStrategy::InlineLiteral,
78    elementwise: None,
79    reduction: None,
80    emits_nan: false,
81    notes:
82        "Meshgrid explicitly materialises dense coordinate arrays and therefore bypasses fusion.",
83};
84
85fn meshgrid_type(args: &[Type], _context: &ResolveContext) -> Type {
86    if args.is_empty() {
87        return Type::Unknown;
88    }
89    let mut axis_count = args.len();
90    if axis_count >= 2 && matches!(args[axis_count - 2], Type::String) {
91        axis_count = axis_count.saturating_sub(2);
92    }
93    if axis_count == 0 {
94        return Type::Unknown;
95    }
96    let axis_args = &args[..axis_count];
97    let len_x = axis_args.get(0).and_then(size_vector_len);
98    let len_y = axis_args.get(1).and_then(size_vector_len).or(len_x);
99    let len_z = axis_args.get(2).and_then(size_vector_len);
100    let shape = if axis_count >= 3 {
101        vec![len_y, len_x, len_z]
102    } else {
103        vec![len_y, len_x]
104    };
105    Type::Tensor { shape: Some(shape) }
106}
107
108const MESHGRID_OUTPUT_XY: [BuiltinParamDescriptor; 2] = [
109    BuiltinParamDescriptor {
110        name: "X",
111        ty: BuiltinParamType::NumericArray,
112        arity: BuiltinParamArity::Required,
113        default: None,
114        description: "Grid coordinates along X-axis.",
115    },
116    BuiltinParamDescriptor {
117        name: "Y",
118        ty: BuiltinParamType::NumericArray,
119        arity: BuiltinParamArity::Required,
120        default: None,
121        description: "Grid coordinates along Y-axis.",
122    },
123];
124
125const MESHGRID_OUTPUT_XYZ: [BuiltinParamDescriptor; 3] = [
126    BuiltinParamDescriptor {
127        name: "X",
128        ty: BuiltinParamType::NumericArray,
129        arity: BuiltinParamArity::Required,
130        default: None,
131        description: "Grid coordinates along X-axis.",
132    },
133    BuiltinParamDescriptor {
134        name: "Y",
135        ty: BuiltinParamType::NumericArray,
136        arity: BuiltinParamArity::Required,
137        default: None,
138        description: "Grid coordinates along Y-axis.",
139    },
140    BuiltinParamDescriptor {
141        name: "Z",
142        ty: BuiltinParamType::NumericArray,
143        arity: BuiltinParamArity::Optional,
144        default: None,
145        description: "Grid coordinates along Z-axis.",
146    },
147];
148
149const MESHGRID_SIG_X_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
150    name: "x",
151    ty: BuiltinParamType::NumericArray,
152    arity: BuiltinParamArity::Required,
153    default: None,
154    description: "X-axis vector.",
155}];
156
157const MESHGRID_SIG_XY_INPUTS: [BuiltinParamDescriptor; 2] = [
158    BuiltinParamDescriptor {
159        name: "x",
160        ty: BuiltinParamType::NumericArray,
161        arity: BuiltinParamArity::Required,
162        default: None,
163        description: "X-axis vector.",
164    },
165    BuiltinParamDescriptor {
166        name: "y",
167        ty: BuiltinParamType::NumericArray,
168        arity: BuiltinParamArity::Required,
169        default: None,
170        description: "Y-axis vector.",
171    },
172];
173
174const MESHGRID_SIG_XYZ_INPUTS: [BuiltinParamDescriptor; 3] = [
175    BuiltinParamDescriptor {
176        name: "x",
177        ty: BuiltinParamType::NumericArray,
178        arity: BuiltinParamArity::Required,
179        default: None,
180        description: "X-axis vector.",
181    },
182    BuiltinParamDescriptor {
183        name: "y",
184        ty: BuiltinParamType::NumericArray,
185        arity: BuiltinParamArity::Required,
186        default: None,
187        description: "Y-axis vector.",
188    },
189    BuiltinParamDescriptor {
190        name: "z",
191        ty: BuiltinParamType::NumericArray,
192        arity: BuiltinParamArity::Optional,
193        default: None,
194        description: "Z-axis vector.",
195    },
196];
197
198const MESHGRID_SIG_X_LIKE_INPUTS: [BuiltinParamDescriptor; 3] = [
199    BuiltinParamDescriptor {
200        name: "x",
201        ty: BuiltinParamType::NumericArray,
202        arity: BuiltinParamArity::Required,
203        default: None,
204        description: "X-axis vector.",
205    },
206    BuiltinParamDescriptor {
207        name: "like_kw",
208        ty: BuiltinParamType::StringScalar,
209        arity: BuiltinParamArity::Required,
210        default: Some("\"like\""),
211        description: "Like keyword.",
212    },
213    BuiltinParamDescriptor {
214        name: "prototype",
215        ty: BuiltinParamType::LikePrototype,
216        arity: BuiltinParamArity::Required,
217        default: None,
218        description: "Prototype controlling class/device residency.",
219    },
220];
221
222const MESHGRID_SIG_XY_LIKE_INPUTS: [BuiltinParamDescriptor; 4] = [
223    BuiltinParamDescriptor {
224        name: "x",
225        ty: BuiltinParamType::NumericArray,
226        arity: BuiltinParamArity::Required,
227        default: None,
228        description: "X-axis vector.",
229    },
230    BuiltinParamDescriptor {
231        name: "y",
232        ty: BuiltinParamType::NumericArray,
233        arity: BuiltinParamArity::Required,
234        default: None,
235        description: "Y-axis vector.",
236    },
237    BuiltinParamDescriptor {
238        name: "like_kw",
239        ty: BuiltinParamType::StringScalar,
240        arity: BuiltinParamArity::Required,
241        default: Some("\"like\""),
242        description: "Like keyword.",
243    },
244    BuiltinParamDescriptor {
245        name: "prototype",
246        ty: BuiltinParamType::LikePrototype,
247        arity: BuiltinParamArity::Required,
248        default: None,
249        description: "Prototype controlling class/device residency.",
250    },
251];
252
253const MESHGRID_SIG_XYZ_LIKE_INPUTS: [BuiltinParamDescriptor; 5] = [
254    BuiltinParamDescriptor {
255        name: "x",
256        ty: BuiltinParamType::NumericArray,
257        arity: BuiltinParamArity::Required,
258        default: None,
259        description: "X-axis vector.",
260    },
261    BuiltinParamDescriptor {
262        name: "y",
263        ty: BuiltinParamType::NumericArray,
264        arity: BuiltinParamArity::Required,
265        default: None,
266        description: "Y-axis vector.",
267    },
268    BuiltinParamDescriptor {
269        name: "z",
270        ty: BuiltinParamType::NumericArray,
271        arity: BuiltinParamArity::Optional,
272        default: None,
273        description: "Z-axis vector.",
274    },
275    BuiltinParamDescriptor {
276        name: "like_kw",
277        ty: BuiltinParamType::StringScalar,
278        arity: BuiltinParamArity::Required,
279        default: Some("\"like\""),
280        description: "Like keyword.",
281    },
282    BuiltinParamDescriptor {
283        name: "prototype",
284        ty: BuiltinParamType::LikePrototype,
285        arity: BuiltinParamArity::Required,
286        default: None,
287        description: "Prototype controlling class/device residency.",
288    },
289];
290
291const MESHGRID_SIGNATURES: [BuiltinSignatureDescriptor; 6] = [
292    BuiltinSignatureDescriptor {
293        label: "[X,Y] = meshgrid(x)",
294        inputs: &MESHGRID_SIG_X_INPUTS,
295        outputs: &MESHGRID_OUTPUT_XY,
296    },
297    BuiltinSignatureDescriptor {
298        label: "[X,Y] = meshgrid(x, y)",
299        inputs: &MESHGRID_SIG_XY_INPUTS,
300        outputs: &MESHGRID_OUTPUT_XY,
301    },
302    BuiltinSignatureDescriptor {
303        label: "[X,Y,Z] = meshgrid(x, y, z)",
304        inputs: &MESHGRID_SIG_XYZ_INPUTS,
305        outputs: &MESHGRID_OUTPUT_XYZ,
306    },
307    BuiltinSignatureDescriptor {
308        label: "[X,Y] = meshgrid(x, \"like\", prototype)",
309        inputs: &MESHGRID_SIG_X_LIKE_INPUTS,
310        outputs: &MESHGRID_OUTPUT_XY,
311    },
312    BuiltinSignatureDescriptor {
313        label: "[X,Y] = meshgrid(x, y, \"like\", prototype)",
314        inputs: &MESHGRID_SIG_XY_LIKE_INPUTS,
315        outputs: &MESHGRID_OUTPUT_XY,
316    },
317    BuiltinSignatureDescriptor {
318        label: "[X,Y,Z] = meshgrid(x, y, z, \"like\", prototype)",
319        inputs: &MESHGRID_SIG_XYZ_LIKE_INPUTS,
320        outputs: &MESHGRID_OUTPUT_XYZ,
321    },
322];
323
324const MESHGRID_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
325    [BuiltinIntegerInputCapability {
326        name: "x, y, z",
327        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
328        availability: BuiltinIntegerInputAvailability::Documented,
329        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
330        notes: "Every documented integer axis class is replicated without conversion and each corresponding grid preserves that axis class.",
331    }];
332
333pub const MESHGRID_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
334    [BuiltinIntegerCapabilityDescriptor {
335        form: "[X,Y,Z] = meshgrid(integer_x, integer_y, integer_z)",
336        inputs: &MESHGRID_INTEGER_INPUTS,
337        computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
338        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
339        overflow: BuiltinIntegerOverflowRule::NotApplicable,
340        backend: BuiltinIntegerBackendRule::GpuRestricted,
341        overload: BuiltinIntegerOverloadKind::Multiple,
342        notes: "Host axes preserve exact native storage independently. MATLAB documents gpuArray axes only for single and double; explicit integer gpuArray axes reject, while automatic integer residency gathers transparently and returns exact host grids.",
343    }];
344
345const MESHGRID_ERRORS: [BuiltinErrorDescriptor; 11] = [
346    BuiltinErrorDescriptor {
347        code: "RM.MESHGRID.MISSING_AXIS",
348        identifier: None,
349        when: "No axis vectors are provided.",
350        message: "meshgrid: at least one input vector is required",
351    },
352    BuiltinErrorDescriptor {
353        code: "RM.MESHGRID.TOO_MANY_AXES",
354        identifier: None,
355        when: "More than three axis vectors are provided.",
356        message: "meshgrid: expected at most three input vectors",
357    },
358    BuiltinErrorDescriptor {
359        code: "RM.MESHGRID.LIKE_EXPECTED_PROTOTYPE",
360        identifier: None,
361        when: "The 'like' keyword is provided without a prototype argument.",
362        message: "meshgrid: expected prototype after 'like'",
363    },
364    BuiltinErrorDescriptor {
365        code: "RM.MESHGRID.MULTIPLE_LIKE",
366        identifier: None,
367        when: "The 'like' keyword is provided multiple times.",
368        message: "meshgrid: multiple 'like' specifications are not supported",
369    },
370    BuiltinErrorDescriptor {
371        code: "RM.MESHGRID.LIKE_POSITION",
372        identifier: None,
373        when: "The 'like' keyword is in an invalid position or not final.",
374        message: "meshgrid: 'like' must be the final argument",
375    },
376    BuiltinErrorDescriptor {
377        code: "RM.MESHGRID.UNRECOGNIZED_OPTION",
378        identifier: None,
379        when: "A trailing option string is not recognized.",
380        message: "meshgrid: unrecognised option",
381    },
382    BuiltinErrorDescriptor {
383        code: "RM.MESHGRID.INVALID_AXIS_INPUT",
384        identifier: None,
385        when: "Axis inputs are non-numeric or non-vector shapes.",
386        message: "meshgrid: input argument must be numeric vector data",
387    },
388    BuiltinErrorDescriptor {
389        code: "RM.MESHGRID.INVALID_PROTOTYPE",
390        identifier: None,
391        when: "The 'like' prototype is unsupported.",
392        message: "meshgrid: prototypes must be numeric arrays",
393    },
394    BuiltinErrorDescriptor {
395        code: "RM.MESHGRID.OUTPUT_COUNT_EXCEEDED",
396        identifier: None,
397        when: "Requested outputs exceed available outputs for provided axes.",
398        message:
399            "meshgrid: supports at most two outputs for 2-axis inputs and three for 3-axis inputs",
400    },
401    BuiltinErrorDescriptor {
402        code: "RM.MESHGRID.THIRD_OUTPUT_UNAVAILABLE",
403        identifier: None,
404        when: "A third output is requested without supplying a Z-axis vector.",
405        message: "meshgrid: third output requested but no Z vector was supplied",
406    },
407    BuiltinErrorDescriptor {
408        code: "RM.MESHGRID.COMPLEX_REAL_CONVERSION",
409        identifier: None,
410        when: "Complex axis values cannot be represented in requested real output class.",
411        message: "meshgrid: cannot represent complex values in a real output",
412    },
413];
414
415pub const MESHGRID_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
416    signatures: &MESHGRID_SIGNATURES,
417    output_mode: BuiltinOutputMode::ByRequestedOutputCount,
418    completion_policy: BuiltinCompletionPolicy::Public,
419    errors: &MESHGRID_ERRORS,
420};
421
422#[runtime_builtin(
423    name = "meshgrid",
424    category = "array/creation",
425    summary = "Generate coordinate matrices for 2-D and 3-D grids.",
426    keywords = "meshgrid,grid,gpu,like,3d",
427    accel = "array_construct",
428    type_resolver(meshgrid_type),
429    descriptor(crate::builtins::array::creation::meshgrid::MESHGRID_DESCRIPTOR),
430    integer_capabilities(
431        crate::builtins::array::creation::meshgrid::MESHGRID_INTEGER_CAPABILITIES
432    ),
433    extensions(crate::builtins::array::creation::meshgrid::MESHGRID_EXTENSIONS),
434    builtin_path = "crate::builtins::array::creation::meshgrid"
435)]
436async fn meshgrid_builtin(rest: Vec<Value>) -> crate::BuiltinResult<Value> {
437    let eval = evaluate(&rest).await?;
438    if let Some(out_count) = crate::output_count::current_output_count() {
439        if out_count == 0 {
440            return Ok(Value::OutputList(Vec::new()));
441        }
442        let available = eval.output_count();
443        if out_count > available {
444            let msg = if available == 2 {
445                "meshgrid with two inputs supports at most two outputs"
446            } else {
447                "meshgrid supports at most three outputs"
448            };
449            return Err(builtin_error(msg));
450        }
451        let mut outputs = Vec::with_capacity(out_count);
452        let first = eval.first().await?;
453        outputs.push(first);
454        if out_count >= 2 {
455            outputs.push(eval.second().await?);
456        }
457        if out_count >= 3 {
458            outputs.push(eval.third().await?);
459        }
460        return Ok(Value::OutputList(outputs));
461    }
462    eval.first().await
463}
464
465/// Evaluate the `meshgrid` builtin once and reuse the result for multiple outputs.
466pub async fn evaluate(args: &[Value]) -> crate::BuiltinResult<MeshgridEval> {
467    let parsed = ParsedMeshgrid::parse(args).await?;
468    let (x_axis, y_axis, z_axis) = normalise_axes(&parsed.axes);
469
470    let require_complex = parsed.axes.iter().any(|axis| axis.is_complex);
471
472    let target_class = match &parsed.template {
473        OutputTemplate::Default => {
474            if require_complex {
475                PrototypeClass::Complex
476            } else {
477                PrototypeClass::Real
478            }
479        }
480        OutputTemplate::Like(spec) => {
481            if require_complex {
482                PrototypeClass::Complex
483            } else {
484                spec.class
485            }
486        }
487    };
488
489    let target_residency = match &parsed.template {
490        OutputTemplate::Default => {
491            if parsed.prefer_gpu {
492                DevicePreference::Gpu(parsed.preferred_device_id.unwrap_or(0))
493            } else {
494                DevicePreference::Host
495            }
496        }
497        OutputTemplate::Like(spec) => spec.residency,
498    };
499
500    let mut outputs: Vec<MeshgridOutput> = Vec::new();
501
502    if matches!(target_residency, DevicePreference::Gpu(_)) {
503        if let Some(gpu) = try_meshgrid_gpu_from_vector_axes(&x_axis, &y_axis, z_axis.as_ref())? {
504            outputs = gpu;
505        }
506    }
507
508    if outputs.is_empty() {
509        // Host fallback: ensure we have host axis values materialized.
510        let x_host = axis_to_host_async(&x_axis).await?;
511        let y_host = axis_to_host_async(&y_axis).await?;
512        let z_host = match z_axis.as_ref() {
513            Some(axis) => Some(axis_to_host_async(axis).await?),
514            None => None,
515        };
516        outputs = build_outputs(&x_host, &y_host, z_host.as_ref())
517            .into_iter()
518            .map(MeshgridOutput::Host)
519            .collect();
520    }
521
522    Ok(MeshgridEval {
523        outputs,
524        target_class,
525        target_residency,
526    })
527}
528
529#[derive(Clone)]
530struct ParsedMeshgrid {
531    axes: Vec<AxisData>,
532    template: OutputTemplate,
533    prefer_gpu: bool,
534    preferred_device_id: Option<u32>,
535}
536
537impl ParsedMeshgrid {
538    async fn parse(args: &[Value]) -> crate::BuiltinResult<Self> {
539        if args.is_empty() {
540            return Err(builtin_error(
541                "meshgrid: at least one input vector is required",
542            ));
543        }
544        let mut axis_values: Vec<Value> = Vec::new();
545        let mut like_proto: Option<Value> = None;
546        let mut prefer_gpu = false;
547        let mut preferred_device_id = None;
548        let mut idx = 0;
549        while idx < args.len() {
550            let value = args[idx].clone();
551            if let Some(keyword) = keyword_of(&value) {
552                match keyword.as_str() {
553                    "like" => {
554                        crate::compatibility::ensure_builtin_extension_enabled(
555                            &MESHGRID_LIKE_EXTENSION,
556                            "meshgrid",
557                        )?;
558                        if like_proto.is_some() {
559                            return Err(builtin_error(
560                                "meshgrid: multiple 'like' specifications are not supported",
561                            ));
562                        }
563                        if axis_values.is_empty() {
564                            return Err(builtin_error(
565                                "meshgrid: 'like' must follow at least one input vector",
566                            ));
567                        }
568                        let Some(proto) = args.get(idx + 1).cloned() else {
569                            return Err(builtin_error("meshgrid: expected prototype after 'like'"));
570                        };
571                        like_proto = Some(proto);
572                        idx += 2;
573                        if idx < args.len() {
574                            return Err(builtin_error(
575                                "meshgrid: 'like' must be the final argument",
576                            ));
577                        }
578                        break;
579                    }
580                    other => {
581                        return Err(builtin_error(format!(
582                            "meshgrid: unrecognised option '{other}'"
583                        )));
584                    }
585                }
586            }
587
588            axis_values.push(value);
589            idx += 1;
590        }
591
592        if axis_values.is_empty() {
593            return Err(builtin_error(
594                "meshgrid: at least one input vector is required",
595            ));
596        }
597        if axis_values.len() > 3 {
598            return Err(builtin_error(
599                "meshgrid: expected at most three input vectors",
600            ));
601        }
602
603        let force_host_for_integer_residency = axis_values.iter().any(|value| {
604            matches!(value, Value::GpuTensor(handle)
605                if runmat_accelerate_api::handle_integer_type(handle).is_some()
606                    && runmat_accelerate_api::handle_storage(handle) == GpuTensorStorage::Real)
607        });
608
609        let mut axes = Vec::with_capacity(max(axis_values.len(), 2));
610        for (i, value) in axis_values.into_iter().enumerate() {
611            if let Value::GpuTensor(handle) = &value {
612                preferred_device_id.get_or_insert(handle.device_id);
613            }
614            let mut consumed_gpu = false;
615            let data = axis_from_value(value, i, &mut consumed_gpu).await?;
616            if data.is_complex {
617                crate::compatibility::ensure_builtin_extension_enabled(
618                    &MESHGRID_COMPLEX_AXES_EXTENSION,
619                    "meshgrid",
620                )?;
621            }
622            if consumed_gpu {
623                prefer_gpu = true;
624            }
625            axes.push(data);
626        }
627
628        if !prefer_gpu {
629            if let Some(max_len) = axes.iter().map(|axis| axis.len).max() {
630                if max_len > 0
631                    && !axes.iter().any(|axis| {
632                        axis.real_storage().is_some_and(real_storage_is_integer)
633                            || matches!(axis.complex_storage(), Some(ComplexStorage::Integer(_)))
634                    })
635                    && sequence_gpu_preference(max_len, SequenceIntent::MeshAxis, false).prefer_gpu
636                {
637                    prefer_gpu = true;
638                }
639            }
640        }
641        if force_host_for_integer_residency {
642            prefer_gpu = false;
643        }
644
645        let template = if let Some(proto) = like_proto {
646            OutputTemplate::Like(analyse_like_prototype(&proto)?)
647        } else {
648            OutputTemplate::Default
649        };
650
651        Ok(Self {
652            axes,
653            template,
654            prefer_gpu,
655            preferred_device_id,
656        })
657    }
658}
659
660#[derive(Clone)]
661enum OutputTemplate {
662    Default,
663    Like(PrototypeSpec),
664}
665
666#[derive(Clone)]
667struct PrototypeSpec {
668    residency: DevicePreference,
669    class: PrototypeClass,
670}
671
672#[derive(Clone, Copy, PartialEq, Eq)]
673enum PrototypeClass {
674    Real,
675    Complex,
676}
677
678#[derive(Clone, Copy)]
679enum DevicePreference {
680    Host,
681    Gpu(u32),
682}
683
684fn analyse_like_prototype(proto: &Value) -> crate::BuiltinResult<PrototypeSpec> {
685    match proto {
686        Value::GpuTensor(handle) => {
687            let class = if runmat_accelerate_api::handle_storage(handle)
688                == GpuTensorStorage::ComplexInterleaved
689            {
690                PrototypeClass::Complex
691            } else {
692                PrototypeClass::Real
693            };
694            Ok(PrototypeSpec {
695                residency: DevicePreference::Gpu(handle.device_id),
696                class,
697            })
698        }
699        Value::ComplexTensor(_) | Value::Complex(_, _) => Ok(PrototypeSpec {
700            residency: DevicePreference::Host,
701            class: PrototypeClass::Complex,
702        }),
703        Value::Tensor(_)
704        | Value::SparseTensor(_)
705        | Value::Num(_)
706        | Value::Int(_)
707        | Value::Bool(_)
708        | Value::LogicalArray(_) => Ok(PrototypeSpec {
709            residency: DevicePreference::Host,
710            class: PrototypeClass::Real,
711        }),
712        Value::CharArray(_) | Value::String(_) | Value::StringArray(_) => Err(builtin_error(
713            "meshgrid: prototypes must be numeric or gpuArray values",
714        )),
715        Value::Symbolic(_) | Value::SymbolicArray(_) => Err(builtin_error(
716            "meshgrid: prototypes must be numeric or gpuArray values",
717        )),
718        Value::Cell(_)
719        | Value::Struct(_)
720        | Value::ObjectArray(_)
721        | Value::Object(_)
722        | Value::HandleObject(_)
723        | Value::Listener(_)
724        | Value::FunctionHandle(_)
725        | Value::ExternalFunctionHandle(_)
726        | Value::MethodFunctionHandle(_)
727        | Value::BoundFunctionHandle { .. }
728        | Value::Closure(_)
729        | Value::ClassRef(_)
730        | Value::MException(_)
731        | Value::Future(_)
732        | Value::Task(_)
733        | Value::Pool(_)
734        | Value::Job(_)
735        | Value::Foreign(_)
736        | Value::OutputList(_) => Err(builtin_error("meshgrid: prototypes must be numeric arrays")),
737    }
738}
739
740#[derive(Clone)]
741struct AxisData {
742    len: usize,
743    is_complex: bool,
744    storage: AxisStorage,
745}
746
747#[derive(Clone)]
748enum AxisStorage {
749    Real(NumericStorage),
750    Complex(ComplexStorage),
751    GpuReal(GpuTensorHandle),
752}
753
754impl AxisData {
755    fn real_storage(&self) -> Option<&NumericStorage> {
756        match &self.storage {
757            AxisStorage::Real(storage) => Some(storage),
758            AxisStorage::Complex(_) | AxisStorage::GpuReal(_) => None,
759        }
760    }
761
762    fn complex_storage(&self) -> Option<&ComplexStorage> {
763        match &self.storage {
764            AxisStorage::Complex(storage) => Some(storage),
765            AxisStorage::Real(_) | AxisStorage::GpuReal(_) => None,
766        }
767    }
768
769    fn gpu_real(&self) -> Option<&GpuTensorHandle> {
770        match &self.storage {
771            AxisStorage::GpuReal(handle) => Some(handle),
772            AxisStorage::Real(_) | AxisStorage::Complex(_) => None,
773        }
774    }
775}
776
777fn real_storage_is_integer(storage: &NumericStorage) -> bool {
778    !matches!(
779        storage.numeric_dtype(),
780        NumericDType::F64 | NumericDType::F32
781    )
782}
783
784async fn axis_from_value(
785    value: Value,
786    index: usize,
787    prefer_gpu: &mut bool,
788) -> crate::BuiltinResult<AxisData> {
789    match value {
790        Value::Tensor(tensor) => axis_from_tensor(tensor, index),
791        Value::LogicalArray(logical) => {
792            let tensor = tensor::logical_to_tensor(&logical)?;
793            axis_from_tensor(tensor, index)
794        }
795        Value::Num(n) => Ok(AxisData {
796            len: 1,
797            is_complex: false,
798            storage: AxisStorage::Real(NumericStorage::F64(vec![n])),
799        }),
800        Value::Int(i) => Ok(AxisData {
801            len: 1,
802            is_complex: false,
803            storage: AxisStorage::Real(NumericStorage::from_integer_storage(
804                IntegerStorage::from_scalar(i),
805            )),
806        }),
807        Value::Bool(b) => Ok(AxisData {
808            len: 1,
809            is_complex: false,
810            storage: AxisStorage::Real(NumericStorage::F64(vec![if b { 1.0 } else { 0.0 }])),
811        }),
812        Value::Complex(re, im) => Ok(AxisData {
813            len: 1,
814            is_complex: im != 0.0,
815            storage: AxisStorage::Complex(ComplexStorage::F64(vec![(re, im)])),
816        }),
817        Value::ComplexTensor(tensor) => axis_from_complex_tensor(tensor, index),
818        Value::GpuTensor(handle) => {
819            if runmat_accelerate_api::handle_integer_type(&handle).is_some() {
820                if runmat_accelerate_api::handle_storage(&handle)
821                    == GpuTensorStorage::ComplexInterleaved
822                {
823                    *prefer_gpu = true;
824                    let gathered =
825                        gpu_helpers::gather_value_async(&Value::GpuTensor(handle)).await?;
826                    return match gathered {
827                        Value::ComplexTensor(tensor) => axis_from_complex_tensor(tensor, index),
828                        other => Err(builtin_error(format!(
829                            "meshgrid: expected paired complex integer GPU axis, got {other:?}"
830                        ))),
831                    };
832                }
833                if runmat_accelerate_api::handle_is_explicit(&handle) {
834                    return Err(builtin_error(
835                        "meshgrid: integer gpuArray axes are not supported",
836                    ));
837                }
838                let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle)).await?;
839                return match gathered {
840                    Value::Tensor(tensor) => axis_from_tensor(tensor, index),
841                    Value::Int(value) => Ok(AxisData {
842                        len: 1,
843                        is_complex: false,
844                        storage: AxisStorage::Real(NumericStorage::from_integer_storage(
845                            IntegerStorage::from_scalar(value),
846                        )),
847                    }),
848                    other => Err(builtin_error(format!(
849                        "meshgrid: expected integer GPU axis, got {other:?}"
850                    ))),
851                };
852            }
853            let is_complex = runmat_accelerate_api::handle_storage(&handle)
854                == GpuTensorStorage::ComplexInterleaved;
855            // Fast path: if the gpuArray is vector-like, keep it on-device and avoid a download.
856            // We'll validate any non-vector shapes by gathering below.
857            if is_vector_shape(&handle.shape) && !is_complex {
858                *prefer_gpu = true;
859                return Ok(AxisData {
860                    len: vector_len_from_shape(&handle.shape),
861                    is_complex,
862                    storage: AxisStorage::GpuReal(handle),
863                });
864            }
865
866            // Fallback: gather to validate / recover axes from meshgrid matrices.
867            *prefer_gpu = true;
868            let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle)).await?;
869            match gathered {
870                Value::Tensor(tensor) => {
871                    if is_vector_shape(&tensor.shape) {
872                        *prefer_gpu = true;
873                    }
874                    axis_from_tensor(tensor, index)
875                }
876                Value::ComplexTensor(tensor) => {
877                    if is_vector_shape(&tensor.shape) {
878                        *prefer_gpu = true;
879                    }
880                    axis_from_complex_tensor(tensor, index)
881                }
882                other => Err(builtin_error(format!(
883                    "meshgrid: input argument {} must be numeric, got {other:?}",
884                    index + 1
885                ))),
886            }
887        }
888        other => Err(builtin_error(format!(
889            "meshgrid: input argument {} must be numeric, got {other:?}",
890            index + 1
891        ))),
892    }
893}
894
895fn axis_from_tensor(tensor: Tensor, index: usize) -> crate::BuiltinResult<AxisData> {
896    if is_vector_shape(&tensor.shape) {
897        let storage = tensor
898            .into_numeric_storage()
899            .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
900        let len = storage.len();
901        return Ok(AxisData {
902            len,
903            is_complex: false,
904            storage: AxisStorage::Real(storage),
905        });
906    }
907
908    // Be slightly more permissive than MATLAB: if the input is already a meshgrid-style
909    // coordinate matrix, accept it and recover the original axis vector.
910    //
911    // This is a pragmatic compatibility shim for cases where callers already have
912    // coordinate matrices (X/Y) and pass them through `meshgrid` again.
913    let shape = tensor.shape.clone();
914    if let Some(axis) = axis_from_meshgrid_matrix_real(tensor, index)? {
915        return Ok(axis);
916    }
917
918    Err(builtin_error(format!(
919        "meshgrid: input argument {} must be a vector (1xN or Nx1), got shape {:?}",
920        index + 1,
921        shape
922    )))
923}
924
925fn axis_from_complex_tensor(tensor: ComplexTensor, index: usize) -> crate::BuiltinResult<AxisData> {
926    if is_vector_shape(&tensor.shape) {
927        let len = tensor::complex_tensor_element_len(&tensor);
928        let is_complex = match tensor.integer_storage() {
929            Some(storage) => storage
930                .imag
931                .exact_values()
932                .into_iter()
933                .any(|value| !value.is_zero()),
934            None => tensor
935                .materialize_f64()
936                .iter()
937                .any(|&(_, imag)| !imag.is_nan() && imag != 0.0),
938        };
939        return Ok(AxisData {
940            len,
941            is_complex,
942            storage: AxisStorage::Complex(tensor.into_complex_storage()),
943        });
944    }
945
946    if tensor.integer_storage().is_some() {
947        return Err(builtin_error(format!(
948            "meshgrid: input argument {} must be a vector (1xN or Nx1), got shape {:?}",
949            index + 1,
950            tensor.shape
951        )));
952    }
953
954    if let Some(axis) = axis_from_meshgrid_matrix_complex(&tensor, index)? {
955        return Ok(axis);
956    }
957
958    Err(builtin_error(format!(
959        "meshgrid: input argument {} must be a vector (1xN or Nx1), got shape {:?}",
960        index + 1,
961        tensor.shape
962    )))
963}
964
965fn axis_from_meshgrid_matrix_real(
966    tensor: Tensor,
967    index: usize,
968) -> crate::BuiltinResult<Option<AxisData>> {
969    let (rows, cols) = match tensor.shape.as_slice() {
970        [r, c] => (*r, *c),
971        _ => return Ok(None),
972    };
973    if rows <= 1 || cols <= 1 {
974        return Ok(None);
975    }
976
977    let storage = tensor
978        .into_numeric_storage()
979        .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
980
981    // Index 0 is expected to be the X-axis: a meshgrid X matrix has identical rows.
982    // Index 1 is expected to be the Y-axis: a meshgrid Y matrix has identical columns.
983    let expect_rows_constant = index == 0;
984
985    if expect_rows_constant {
986        for row in 1..rows {
987            for col in 0..cols {
988                if storage.value_at(row + rows * col) != storage.value_at(rows * col) {
989                    return Ok(None);
990                }
991            }
992        }
993        let indices: Vec<usize> = (0..cols).map(|col| rows * col).collect();
994        let storage = storage
995            .gather(&indices)
996            .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
997        return Ok(Some(AxisData {
998            len: cols,
999            is_complex: false,
1000            storage: AxisStorage::Real(storage),
1001        }));
1002    }
1003
1004    for col in 1..cols {
1005        for row in 0..rows {
1006            if storage.value_at(row + rows * col) != storage.value_at(row) {
1007                return Ok(None);
1008            }
1009        }
1010    }
1011    let indices: Vec<usize> = (0..rows).collect();
1012    let storage = storage
1013        .gather(&indices)
1014        .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
1015    Ok(Some(AxisData {
1016        len: rows,
1017        is_complex: false,
1018        storage: AxisStorage::Real(storage),
1019    }))
1020}
1021
1022fn axis_from_meshgrid_matrix_complex(
1023    tensor: &ComplexTensor,
1024    index: usize,
1025) -> crate::BuiltinResult<Option<AxisData>> {
1026    let (rows, cols) = match tensor.shape.as_slice() {
1027        [r, c] => (*r, *c),
1028        _ => return Ok(None),
1029    };
1030    if rows <= 1 || cols <= 1 {
1031        return Ok(None);
1032    }
1033
1034    let values = tensor.materialize_f64();
1035    let expect_rows_constant = index == 0;
1036    if expect_rows_constant {
1037        if !matrix_rows_are_identical_complex(&values, rows, cols) {
1038            return Ok(None);
1039        }
1040        let indices: Vec<usize> = (0..cols).map(|col| rows * col).collect();
1041        let storage = tensor
1042            .complex_storage()
1043            .gather(&indices)
1044            .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
1045        let is_complex = storage
1046            .materialize_f64()
1047            .iter()
1048            .any(|&(_, im)| !im.is_nan() && im != 0.0);
1049        return Ok(Some(AxisData {
1050            len: storage.len(),
1051            is_complex,
1052            storage: AxisStorage::Complex(storage),
1053        }));
1054    }
1055
1056    if !matrix_cols_are_identical_complex(&values, rows, cols) {
1057        return Ok(None);
1058    }
1059    let indices: Vec<usize> = (0..rows).collect();
1060    let storage = tensor
1061        .complex_storage()
1062        .gather(&indices)
1063        .map_err(|error| builtin_error(format!("meshgrid: {error}")))?;
1064    let is_complex = storage
1065        .materialize_f64()
1066        .iter()
1067        .any(|&(_, im)| !im.is_nan() && im != 0.0);
1068    Ok(Some(AxisData {
1069        len: storage.len(),
1070        is_complex,
1071        storage: AxisStorage::Complex(storage),
1072    }))
1073}
1074
1075fn matrix_rows_are_identical_complex(values: &[(f64, f64)], rows: usize, cols: usize) -> bool {
1076    for row in 1..rows {
1077        for col in 0..cols {
1078            let idx0 = rows * col;
1079            let idx = row + rows * col;
1080            if values[idx] != values[idx0] {
1081                return false;
1082            }
1083        }
1084    }
1085    true
1086}
1087
1088fn matrix_cols_are_identical_complex(values: &[(f64, f64)], rows: usize, cols: usize) -> bool {
1089    for col in 1..cols {
1090        for row in 0..rows {
1091            let idx0 = row;
1092            let idx = row + rows * col;
1093            if values[idx] != values[idx0] {
1094                return false;
1095            }
1096        }
1097    }
1098    true
1099}
1100
1101fn is_vector_shape(shape: &[usize]) -> bool {
1102    if shape.is_empty() {
1103        return true;
1104    }
1105    let mut non_singleton = 0usize;
1106    for &dim in shape {
1107        if dim > 1 {
1108            non_singleton += 1;
1109        }
1110    }
1111    non_singleton <= 1
1112}
1113
1114fn vector_len_from_shape(shape: &[usize]) -> usize {
1115    if shape.is_empty() {
1116        return 1;
1117    }
1118    shape.iter().copied().max().unwrap_or(0)
1119}
1120
1121async fn axis_to_host_async(axis: &AxisData) -> crate::BuiltinResult<AxisData> {
1122    if axis.gpu_real().is_none() {
1123        return Ok(axis.clone());
1124    }
1125    let handle = axis.gpu_real().expect("checked gpu_real is_some");
1126    let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle.clone())).await?;
1127    // Index is only used for error messages; value came from a validated vector-like handle.
1128    match gathered {
1129        Value::Tensor(tensor) => axis_from_tensor(tensor, 0),
1130        Value::ComplexTensor(tensor) => axis_from_complex_tensor(tensor, 0),
1131        Value::Num(n) => Ok(AxisData {
1132            len: 1,
1133            is_complex: false,
1134            storage: AxisStorage::Complex(ComplexStorage::F64(vec![(n, 0.0)])),
1135        }),
1136        Value::Complex(re, im) => Ok(AxisData {
1137            len: 1,
1138            is_complex: im != 0.0,
1139            storage: AxisStorage::Complex(ComplexStorage::F64(vec![(re, im)])),
1140        }),
1141        other => Err(builtin_error(format!(
1142            "meshgrid: expected numeric GPU axis, got {other:?}"
1143        ))),
1144    }
1145}
1146
1147fn try_meshgrid_gpu_from_vector_axes(
1148    x_axis: &AxisData,
1149    y_axis: &AxisData,
1150    z_axis: Option<&AxisData>,
1151) -> crate::BuiltinResult<Option<Vec<MeshgridOutput>>> {
1152    let Some(x_handle) = x_axis.gpu_real() else {
1153        return Ok(None);
1154    };
1155    let Some(y_handle) = y_axis.gpu_real() else {
1156        return Ok(None);
1157    };
1158
1159    let z_handle = match z_axis {
1160        Some(axis) => match axis.gpu_real() {
1161            Some(h) => Some(h),
1162            None => return Ok(None),
1163        },
1164        None => None,
1165    };
1166
1167    let Some(provider) = runmat_accelerate_api::provider_for_handle(x_handle) else {
1168        return Ok(None);
1169    };
1170    let Some(y_provider) = runmat_accelerate_api::provider_for_handle(y_handle) else {
1171        return Ok(None);
1172    };
1173    if y_provider.device_id() != provider.device_id() {
1174        return Ok(None);
1175    }
1176    if let Some(z) = z_handle {
1177        let Some(z_provider) = runmat_accelerate_api::provider_for_handle(z) else {
1178            return Ok(None);
1179        };
1180        if z_provider.device_id() != provider.device_id() {
1181            return Ok(None);
1182        }
1183    }
1184
1185    let nx = x_axis.len;
1186    let ny = y_axis.len;
1187    let nz = z_axis.map(|axis| axis.len).unwrap_or(1);
1188
1189    // Reshape axis vectors (metadata-only) so repmat can build full grids on-device.
1190    let x_row = provider
1191        .reshape(x_handle, &[1, nx])
1192        .map_err(|e| builtin_error(format!("meshgrid: reshape X failed: {e}")))?;
1193    let y_col = provider
1194        .reshape(y_handle, &[ny, 1])
1195        .map_err(|e| builtin_error(format!("meshgrid: reshape Y failed: {e}")))?;
1196
1197    let mut outputs = Vec::with_capacity(if z_handle.is_some() { 3 } else { 2 });
1198    if let Some(z) = z_handle {
1199        let x_base = provider
1200            .reshape(&x_row, &[1, nx, 1])
1201            .map_err(|e| builtin_error(format!("meshgrid: reshape X(3d) failed: {e}")))?;
1202        let y_base = provider
1203            .reshape(&y_col, &[ny, 1, 1])
1204            .map_err(|e| builtin_error(format!("meshgrid: reshape Y(3d) failed: {e}")))?;
1205
1206        let x_grid = provider
1207            .repmat(&x_base, &[ny, 1, nz])
1208            .map_err(|e| builtin_error(format!("meshgrid: repmat X failed: {e}")))?;
1209        let y_grid = provider
1210            .repmat(&y_base, &[1, nx, nz])
1211            .map_err(|e| builtin_error(format!("meshgrid: repmat Y failed: {e}")))?;
1212
1213        outputs.push(MeshgridOutput::Gpu(x_grid));
1214        outputs.push(MeshgridOutput::Gpu(y_grid));
1215        let z_axis_row = provider
1216            .reshape(z, &[1, nz])
1217            .map_err(|e| builtin_error(format!("meshgrid: reshape Z failed: {e}")))?;
1218        let z_base = provider
1219            .reshape(&z_axis_row, &[1, 1, nz])
1220            .map_err(|e| builtin_error(format!("meshgrid: reshape Z(3d) failed: {e}")))?;
1221        let z_grid = provider
1222            .repmat(&z_base, &[ny, nx, 1])
1223            .map_err(|e| builtin_error(format!("meshgrid: repmat Z failed: {e}")))?;
1224        outputs.push(MeshgridOutput::Gpu(z_grid));
1225    } else {
1226        let x_grid = provider
1227            .repmat(&x_row, &[ny, 1])
1228            .map_err(|e| builtin_error(format!("meshgrid: repmat X failed: {e}")))?;
1229        let y_grid = provider
1230            .repmat(&y_col, &[1, nx])
1231            .map_err(|e| builtin_error(format!("meshgrid: repmat Y failed: {e}")))?;
1232        outputs.push(MeshgridOutput::Gpu(x_grid));
1233        outputs.push(MeshgridOutput::Gpu(y_grid));
1234    }
1235
1236    Ok(Some(outputs))
1237}
1238
1239fn normalise_axes(axes: &[AxisData]) -> (AxisData, AxisData, Option<AxisData>) {
1240    match axes.len() {
1241        1 => {
1242            let x = axes[0].clone();
1243            (x.clone(), x, None)
1244        }
1245        2 => {
1246            let x = axes[0].clone();
1247            let y = axes[1].clone();
1248            (x, y, None)
1249        }
1250        3 => {
1251            let x = axes[0].clone();
1252            let y = axes[1].clone();
1253            let z = axes[2].clone();
1254            (x, y, Some(z))
1255        }
1256        _ => unreachable!(),
1257    }
1258}
1259
1260fn build_outputs(
1261    x_axis: &AxisData,
1262    y_axis: &AxisData,
1263    z_axis: Option<&AxisData>,
1264) -> Vec<GridOutput> {
1265    let nx = x_axis.len;
1266    let ny = y_axis.len;
1267    let nz = z_axis.map(|axis| axis.len).unwrap_or(1);
1268    let total = nx * ny * nz;
1269    let mut x_indices = Vec::with_capacity(total);
1270    let mut y_indices = Vec::with_capacity(total);
1271    let mut z_indices = z_axis.map(|_| Vec::with_capacity(total));
1272
1273    for k in 0..nz {
1274        for col in 0..nx {
1275            for row in 0..ny {
1276                x_indices.push(col);
1277                y_indices.push(row);
1278                if let Some(indices) = z_indices.as_mut() {
1279                    indices.push(k);
1280                }
1281            }
1282        }
1283    }
1284
1285    let mut outputs = Vec::new();
1286    let base_shape = if nz == 1 {
1287        vec![ny, nx]
1288    } else {
1289        vec![ny, nx, nz]
1290    };
1291    outputs.push(GridOutput {
1292        shape: base_shape.clone(),
1293        storage: GridStorage::from_axis(x_axis),
1294        indices: x_indices,
1295    });
1296    outputs.push(GridOutput {
1297        shape: base_shape.clone(),
1298        storage: GridStorage::from_axis(y_axis),
1299        indices: y_indices,
1300    });
1301    if let (Some(axis), Some(indices)) = (z_axis, z_indices) {
1302        outputs.push(GridOutput {
1303            shape: base_shape,
1304            storage: GridStorage::from_axis(axis),
1305            indices,
1306        });
1307    }
1308    outputs
1309}
1310
1311struct GridOutput {
1312    shape: Vec<usize>,
1313    storage: GridStorage,
1314    indices: Vec<usize>,
1315}
1316
1317enum GridStorage {
1318    Real(NumericStorage),
1319    Complex(ComplexStorage),
1320}
1321
1322impl GridStorage {
1323    fn from_axis(axis: &AxisData) -> Self {
1324        match &axis.storage {
1325            AxisStorage::Real(storage) => Self::Real(storage.clone()),
1326            AxisStorage::Complex(storage) => Self::Complex(storage.clone()),
1327            AxisStorage::GpuReal(_) => {
1328                unreachable!("meshgrid host output construction requires a gathered axis")
1329            }
1330        }
1331    }
1332}
1333
1334impl GridOutput {
1335    fn to_value(
1336        &self,
1337        class: PrototypeClass,
1338        residency: DevicePreference,
1339    ) -> crate::BuiltinResult<Value> {
1340        match class {
1341            PrototypeClass::Real => self.to_real_value(residency),
1342            PrototypeClass::Complex => self.to_complex_value(residency),
1343        }
1344    }
1345
1346    fn to_real_value(&self, residency: DevicePreference) -> crate::BuiltinResult<Value> {
1347        let storage = match &self.storage {
1348            GridStorage::Real(prototype) => prototype
1349                .gather(&self.indices)
1350                .map_err(|e| builtin_error(format!("meshgrid: {e}")))?,
1351            GridStorage::Complex(storage) => {
1352                let storage = storage
1353                    .gather(&self.indices)
1354                    .map_err(|e| builtin_error(format!("meshgrid: {e}")))?;
1355                complex_storage_into_real(storage)?
1356            }
1357        };
1358        let tensor = Tensor::from_numeric_storage(storage, self.shape.clone())
1359            .map_err(|e| builtin_error(format!("meshgrid: {e}")))?;
1360        match residency {
1361            DevicePreference::Host => Ok(Value::Tensor(tensor)),
1362            DevicePreference::Gpu(device_id) => to_gpu_tensor_value(tensor, device_id),
1363        }
1364    }
1365
1366    fn to_complex_value(&self, residency: DevicePreference) -> crate::BuiltinResult<Value> {
1367        let storage = match &self.storage {
1368            GridStorage::Real(storage) => {
1369                if !matches!(
1370                    storage.numeric_dtype(),
1371                    NumericDType::F64 | NumericDType::F32
1372                ) {
1373                    return Err(builtin_error(
1374                        "meshgrid: complex output for typed integer axes is not supported",
1375                    ));
1376                }
1377                match storage
1378                    .gather(&self.indices)
1379                    .map_err(|e| builtin_error(format!("meshgrid: {e}")))?
1380                {
1381                    NumericStorage::F64(values) => {
1382                        ComplexStorage::F64(values.into_iter().map(|value| (value, 0.0)).collect())
1383                    }
1384                    NumericStorage::F32(values) => {
1385                        ComplexStorage::F32(values.into_iter().map(|value| (value, 0.0)).collect())
1386                    }
1387                    _ => unreachable!("integer storage was rejected above"),
1388                }
1389            }
1390            GridStorage::Complex(storage) => storage
1391                .gather(&self.indices)
1392                .map_err(|e| builtin_error(format!("meshgrid: {e}")))?,
1393        };
1394        let tensor = ComplexTensor::from_complex_storage(storage, self.shape.clone())
1395            .map_err(|e| builtin_error(format!("meshgrid: {e}")))?;
1396        match residency {
1397            DevicePreference::Host => Ok(complex_tensor_into_value(tensor)),
1398            DevicePreference::Gpu(device_id) => to_complex_gpu_tensor_value(tensor, device_id),
1399        }
1400    }
1401}
1402
1403fn complex_storage_into_real(storage: ComplexStorage) -> crate::BuiltinResult<NumericStorage> {
1404    match storage {
1405        ComplexStorage::F64(values) => values
1406            .into_iter()
1407            .map(|(real, imag)| {
1408                if imag == 0.0 {
1409                    Ok(real)
1410                } else {
1411                    Err(builtin_error(
1412                        "meshgrid: cannot represent complex values in a real output",
1413                    ))
1414                }
1415            })
1416            .collect::<crate::BuiltinResult<Vec<_>>>()
1417            .map(NumericStorage::F64),
1418        ComplexStorage::F32(values) => values
1419            .into_iter()
1420            .map(|(real, imag)| {
1421                if imag == 0.0 {
1422                    Ok(real)
1423                } else {
1424                    Err(builtin_error(
1425                        "meshgrid: cannot represent complex values in a real output",
1426                    ))
1427                }
1428            })
1429            .collect::<crate::BuiltinResult<Vec<_>>>()
1430            .map(NumericStorage::F32),
1431        ComplexStorage::Integer(storage) => {
1432            if storage
1433                .imag
1434                .exact_values()
1435                .into_iter()
1436                .any(|value| !value.is_zero())
1437            {
1438                return Err(builtin_error(
1439                    "meshgrid: cannot represent complex values in a real output",
1440                ));
1441            }
1442            Ok(NumericStorage::from(storage.real))
1443        }
1444    }
1445}
1446
1447fn to_gpu_tensor_value(tensor: Tensor, device_id: u32) -> crate::BuiltinResult<Value> {
1448    let provider = runmat_accelerate_api::provider_for_device(device_id)
1449        .ok_or_else(|| builtin_error("meshgrid: no acceleration provider owns the GPU output"))?;
1450    gpu_helpers::upload_tensor(provider, &tensor)
1451        .map(Value::GpuTensor)
1452        .map_err(|error| builtin_error(format!("meshgrid: GPU output upload failed: {error}")))
1453}
1454
1455fn to_complex_gpu_tensor_value(
1456    tensor: ComplexTensor,
1457    device_id: u32,
1458) -> crate::BuiltinResult<Value> {
1459    let provider = runmat_accelerate_api::provider_for_device(device_id)
1460        .ok_or_else(|| builtin_error("meshgrid: no acceleration provider owns the GPU output"))?;
1461    gpu_helpers::upload_complex_tensor(provider, &tensor)
1462        .map(gpu_helpers::complex_gpu_value)
1463        .map_err(|error| builtin_error(format!("meshgrid: GPU output upload failed: {error}")))
1464}
1465
1466fn tensor_to_complex_tensor(tensor: Tensor) -> crate::BuiltinResult<ComplexTensor> {
1467    let shape = tensor.shape.clone();
1468    let storage = match tensor
1469        .into_numeric_storage()
1470        .map_err(|error| builtin_error(format!("meshgrid: {error}")))?
1471    {
1472        NumericStorage::F64(values) => {
1473            ComplexStorage::F64(values.into_iter().map(|real| (real, 0.0)).collect())
1474        }
1475        NumericStorage::F32(values) => {
1476            ComplexStorage::F32(values.into_iter().map(|real| (real, 0.0)).collect())
1477        }
1478        storage => {
1479            let real = storage
1480                .into_integer_storage()
1481                .expect("non-floating meshgrid storage is integer");
1482            let imag = real.zeros_like(real.len());
1483            ComplexStorage::Integer(
1484                IntegerComplexStorage::new(real, imag)
1485                    .map_err(|error| builtin_error(format!("meshgrid: {error}")))?,
1486            )
1487        }
1488    };
1489    ComplexTensor::from_complex_storage(storage, shape)
1490        .map_err(|e| builtin_error(format!("meshgrid: {e}")))
1491}
1492
1493fn tensor_to_complex_value(tensor: Tensor) -> crate::BuiltinResult<Value> {
1494    let complex = tensor_to_complex_tensor(tensor)?;
1495    Ok(complex_tensor_into_value(complex))
1496}
1497
1498enum MeshgridOutput {
1499    Host(GridOutput),
1500    Gpu(GpuTensorHandle),
1501}
1502
1503impl MeshgridOutput {
1504    async fn to_value(
1505        &self,
1506        class: PrototypeClass,
1507        residency: DevicePreference,
1508    ) -> crate::BuiltinResult<Value> {
1509        match self {
1510            MeshgridOutput::Host(host) => host.to_value(class, residency),
1511            MeshgridOutput::Gpu(handle) => match (class, residency) {
1512                (PrototypeClass::Real, DevicePreference::Gpu(_)) => {
1513                    Ok(Value::GpuTensor(handle.clone()))
1514                }
1515                (PrototypeClass::Real, DevicePreference::Host) => {
1516                    let tensor = gpu_helpers::gather_tensor_async(handle).await?;
1517                    Ok(tensor::tensor_into_value(tensor))
1518                }
1519                (PrototypeClass::Complex, DevicePreference::Host) => {
1520                    match gpu_helpers::gather_value_async(&Value::GpuTensor(handle.clone())).await?
1521                    {
1522                        Value::ComplexTensor(tensor) => Ok(complex_tensor_into_value(tensor)),
1523                        Value::Complex(re, im) => Ok(Value::Complex(re, im)),
1524                        Value::Tensor(tensor) => tensor_to_complex_value(tensor),
1525                        Value::Num(n) => Ok(Value::Complex(n, 0.0)),
1526                        other => Err(builtin_error(format!(
1527                            "meshgrid: expected numeric GPU output, got {other:?}"
1528                        ))),
1529                    }
1530                }
1531                (PrototypeClass::Complex, DevicePreference::Gpu(device_id)) => {
1532                    if runmat_accelerate_api::handle_storage(handle)
1533                        == GpuTensorStorage::ComplexInterleaved
1534                    {
1535                        Ok(gpu_helpers::complex_gpu_value(handle.clone()))
1536                    } else {
1537                        let tensor = gpu_helpers::gather_tensor_async(handle).await?;
1538                        to_complex_gpu_tensor_value(tensor_to_complex_tensor(tensor)?, device_id)
1539                    }
1540                }
1541            },
1542        }
1543    }
1544}
1545
1546/// Holds the results of a `meshgrid` evaluation so multiple outputs can be
1547/// materialised without recomputing the grid.
1548pub struct MeshgridEval {
1549    outputs: Vec<MeshgridOutput>,
1550    target_class: PrototypeClass,
1551    target_residency: DevicePreference,
1552}
1553
1554impl MeshgridEval {
1555    pub fn output_count(&self) -> usize {
1556        self.outputs.len()
1557    }
1558
1559    pub async fn first(&self) -> crate::BuiltinResult<Value> {
1560        self.outputs[0]
1561            .to_value(self.target_class, self.target_residency)
1562            .await
1563    }
1564
1565    pub async fn second(&self) -> crate::BuiltinResult<Value> {
1566        if self.outputs.len() < 2 {
1567            Err(builtin_error("meshgrid: second output unavailable"))
1568        } else {
1569            self.outputs[1]
1570                .to_value(self.target_class, self.target_residency)
1571                .await
1572        }
1573    }
1574
1575    pub async fn third(&self) -> crate::BuiltinResult<Value> {
1576        if self.outputs.len() < 3 {
1577            Err(builtin_error(
1578                "meshgrid: third output requested but no Z vector was supplied",
1579            ))
1580        } else {
1581            self.outputs[2]
1582                .to_value(self.target_class, self.target_residency)
1583                .await
1584        }
1585    }
1586}
1587
1588#[cfg(test)]
1589pub(crate) mod tests {
1590    use super::*;
1591    use crate::builtins::common::test_support;
1592    use futures::executor::block_on;
1593    #[cfg(feature = "wgpu")]
1594    use runmat_accelerate_api::AccelProvider;
1595
1596    use runmat_accelerate_api::HostTensorView;
1597    use runmat_value::IntValue;
1598
1599    fn evaluate(args: &[Value]) -> crate::BuiltinResult<MeshgridEval> {
1600        block_on(super::evaluate(args))
1601    }
1602
1603    fn eval_first(eval: &MeshgridEval) -> crate::BuiltinResult<Value> {
1604        block_on(eval.first())
1605    }
1606
1607    fn eval_second(eval: &MeshgridEval) -> crate::BuiltinResult<Value> {
1608        block_on(eval.second())
1609    }
1610
1611    fn eval_third(eval: &MeshgridEval) -> crate::BuiltinResult<Value> {
1612        block_on(eval.third())
1613    }
1614
1615    #[test]
1616    fn meshgrid_preserves_all_exact_real_integer_classes() {
1617        let storages = [
1618            IntegerStorage::I8(vec![-2, 7, 9]),
1619            IntegerStorage::I16(vec![-300, 400, 900]),
1620            IntegerStorage::I32(vec![i32::MIN, 0, i32::MAX]),
1621            IntegerStorage::I64(vec![i64::MIN, 0, i64::MAX]),
1622            IntegerStorage::U8(vec![0, 7, u8::MAX]),
1623            IntegerStorage::U16(vec![0, 700, u16::MAX]),
1624            IntegerStorage::U32(vec![0, 9_007_199, u32::MAX]),
1625            IntegerStorage::U64(vec![0, 9_007_199_254_740_993, u64::MAX]),
1626        ];
1627
1628        for storage in storages {
1629            let values = storage.exact_values();
1630            let axis = Tensor::new_integer(storage.clone(), vec![1, 3]).expect("axis");
1631            let eval = evaluate(&[Value::Tensor(axis)]).expect("meshgrid");
1632            let Value::Tensor(x) = eval_first(&eval).expect("X") else {
1633                panic!("expected real integer X output");
1634            };
1635            let Value::Tensor(y) = eval_second(&eval).expect("Y") else {
1636                panic!("expected real integer Y output");
1637            };
1638            assert_eq!(x.shape, vec![3, 3]);
1639            assert_eq!(
1640                x.integer_storage(),
1641                Some(
1642                    &storage
1643                        .from_exact_values_like(vec![
1644                            values[0].clone(),
1645                            values[0].clone(),
1646                            values[0].clone(),
1647                            values[1].clone(),
1648                            values[1].clone(),
1649                            values[1].clone(),
1650                            values[2].clone(),
1651                            values[2].clone(),
1652                            values[2].clone(),
1653                        ])
1654                        .expect("expected X")
1655                )
1656            );
1657            assert_eq!(
1658                y.integer_storage(),
1659                Some(
1660                    &storage
1661                        .from_exact_values_like(vec![
1662                            values[0].clone(),
1663                            values[1].clone(),
1664                            values[2].clone(),
1665                            values[0].clone(),
1666                            values[1].clone(),
1667                            values[2].clone(),
1668                            values[0].clone(),
1669                            values[1].clone(),
1670                            values[2].clone(),
1671                        ])
1672                        .expect("expected Y")
1673                )
1674            );
1675        }
1676
1677        let eval = evaluate(&[Value::Int(IntValue::U64(u64::MAX))]).expect("scalar meshgrid");
1678        let Value::Tensor(output) = eval_first(&eval).expect("scalar X") else {
1679            panic!("expected real integer scalar output");
1680        };
1681        assert_eq!(
1682            output.integer_storage(),
1683            Some(&IntegerStorage::U64(vec![u64::MAX]))
1684        );
1685    }
1686
1687    #[test]
1688    fn meshgrid_reads_typed_integer_axis_length_from_storage_without_mirror() {
1689        let axis = Tensor::new_integer(
1690            IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1691            vec![1, 2],
1692        )
1693        .expect("axis");
1694
1695        let eval = evaluate(&[Value::Tensor(axis)]).expect("meshgrid");
1696        let Value::Tensor(x) = eval_first(&eval).expect("X") else {
1697            panic!("expected real integer X output");
1698        };
1699        let Value::Tensor(y) = eval_second(&eval).expect("Y") else {
1700            panic!("expected real integer Y output");
1701        };
1702        assert_eq!(x.shape, vec![2, 2]);
1703        assert_eq!(
1704            x.integer_storage(),
1705            Some(&IntegerStorage::U64(vec![
1706                9_007_199_254_740_993,
1707                9_007_199_254_740_993,
1708                u64::MAX,
1709                u64::MAX,
1710            ]))
1711        );
1712        assert_eq!(
1713            y.integer_storage(),
1714            Some(&IntegerStorage::U64(vec![
1715                9_007_199_254_740_993,
1716                u64::MAX,
1717                9_007_199_254_740_993,
1718                u64::MAX,
1719            ]))
1720        );
1721    }
1722
1723    #[test]
1724    fn meshgrid_automatic_integer_residency_gathers_exactly_but_explicit_rejects() {
1725        test_support::with_test_provider(|provider| {
1726            let axis = Tensor::new_integer(
1727                IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1728                vec![1, 2],
1729            )
1730            .expect("axis");
1731            let automatic = gpu_helpers::upload_tensor(provider, &axis).expect("upload");
1732            let automatic =
1733                automatic.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
1734            let eval = evaluate(&[Value::GpuTensor(automatic)]).expect("automatic gather");
1735            let Value::Tensor(output) = eval_first(&eval).expect("host grid") else {
1736                panic!("automatic integer residency must return exact host output");
1737            };
1738            assert_eq!(
1739                output.integer_storage(),
1740                Some(&IntegerStorage::U64(vec![
1741                    9_007_199_254_740_993,
1742                    9_007_199_254_740_993,
1743                    u64::MAX,
1744                    u64::MAX,
1745                ]))
1746            );
1747
1748            let explicit = gpu_helpers::upload_tensor(provider, &axis).expect("upload");
1749            let explicit =
1750                explicit.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
1751            let error = match evaluate(&[Value::GpuTensor(explicit)]) {
1752                Ok(_) => panic!("explicit integer gpuArray must reject"),
1753                Err(error) => error,
1754            };
1755            assert!(error.message().contains("integer gpuArray axes"));
1756        });
1757    }
1758
1759    #[test]
1760    fn meshgrid_reads_typed_complex_integer_axis_length_from_storage_without_mirror() {
1761        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1762        let storage = IntegerComplexStorage::new(
1763            IntegerStorage::I16(vec![-3, 5]),
1764            IntegerStorage::I16(vec![7, -11]),
1765        )
1766        .unwrap();
1767        let axis = ComplexTensor::new_integer(storage, vec![1, 2]).expect("axis");
1768
1769        let eval = evaluate(&[Value::ComplexTensor(axis)]).expect("meshgrid");
1770        let Value::ComplexTensor(x) = eval_first(&eval).expect("X") else {
1771            panic!("expected typed complex integer X output");
1772        };
1773        assert_eq!(x.shape, vec![2, 2]);
1774        assert_eq!(
1775            x.integer_storage().cloned(),
1776            Some(
1777                IntegerComplexStorage::new(
1778                    IntegerStorage::I16(vec![-3, -3, 5, 5]),
1779                    IntegerStorage::I16(vec![7, 7, -11, -11]),
1780                )
1781                .unwrap()
1782            )
1783        );
1784    }
1785
1786    #[test]
1787    fn meshgrid_recovers_exact_integer_axes_from_coordinate_matrices() {
1788        let x = Tensor::new_integer(
1789            IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1790            vec![1, 2],
1791        )
1792        .expect("X axis");
1793        let y = Tensor::new_integer(IntegerStorage::I64(vec![i64::MIN, 7]), vec![2, 1])
1794            .expect("Y axis");
1795        let initial = evaluate(&[Value::Tensor(x), Value::Tensor(y)]).expect("initial meshgrid");
1796        let x_grid = eval_first(&initial).expect("X grid");
1797        let y_grid = eval_second(&initial).expect("Y grid");
1798        let recovered = evaluate(&[x_grid, y_grid]).expect("recovered meshgrid");
1799        let Value::Tensor(x_out) = eval_first(&recovered).expect("recovered X") else {
1800            panic!("expected real integer X output");
1801        };
1802        let Value::Tensor(y_out) = eval_second(&recovered).expect("recovered Y") else {
1803            panic!("expected real integer Y output");
1804        };
1805        assert_eq!(
1806            x_out.integer_storage(),
1807            Some(&IntegerStorage::U64(vec![
1808                9_007_199_254_740_993,
1809                9_007_199_254_740_993,
1810                u64::MAX,
1811                u64::MAX,
1812            ]))
1813        );
1814        assert_eq!(
1815            y_out.integer_storage(),
1816            Some(&IntegerStorage::I64(vec![i64::MIN, 7, i64::MIN, 7]))
1817        );
1818    }
1819
1820    #[test]
1821    fn meshgrid_does_not_lossily_promote_typed_integer_axes_to_complex() {
1822        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1823        let axis = Tensor::new_integer(
1824            IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
1825            vec![1, 2],
1826        )
1827        .expect("axis");
1828        let prototype = ComplexTensor::new(vec![(1.0, 1.0)], vec![1, 1]).expect("prototype");
1829        let eval = evaluate(&[
1830            Value::Tensor(axis),
1831            Value::String("like".to_string()),
1832            Value::ComplexTensor(prototype),
1833        ])
1834        .expect("parse meshgrid");
1835        let error = eval_first(&eval).expect_err("typed integer complex output must reject");
1836        assert!(error
1837            .message()
1838            .contains("complex output for typed integer axes is not supported"));
1839    }
1840
1841    #[test]
1842    fn meshgrid_tensor_to_complex_reads_typed_integer_storage_exactly() {
1843        let tensor =
1844            Tensor::new_integer(IntegerStorage::I64(vec![-3, 5]), vec![1, 2]).expect("tensor");
1845
1846        let out = tensor_to_complex_tensor(tensor).expect("complex tensor");
1847        assert_eq!(out.shape, vec![1, 2]);
1848        assert_eq!(out.materialize_f64(), vec![(-3.0, 0.0), (5.0, 0.0)]);
1849        assert_eq!(
1850            out.integer_storage().cloned(),
1851            Some(
1852                IntegerComplexStorage::new(
1853                    IntegerStorage::I64(vec![-3, 5]),
1854                    IntegerStorage::I64(vec![0, 0]),
1855                )
1856                .expect("typed complex storage")
1857            )
1858        );
1859    }
1860
1861    fn tensor_from_vec(data: Vec<f64>, rows: usize, cols: usize) -> Tensor {
1862        Tensor::new(data, vec![rows, cols]).unwrap()
1863    }
1864
1865    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1866    #[test]
1867    fn meshgrid_single_input_duplicates_axis() {
1868        let x = tensor_from_vec(vec![-1.0, 0.0, 1.0], 1, 3);
1869        let eval = evaluate(&[Value::Tensor(x)]).expect("meshgrid");
1870        assert_eq!(eval.output_count(), 2);
1871        let x_out = test_support::gather(eval_first(&eval).expect("X")).expect("host");
1872        assert_eq!(x_out.shape, vec![3, 3]);
1873        assert_eq!(
1874            x_out.materialize_f64(),
1875            vec![-1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0]
1876        );
1877        let y_out = test_support::gather(eval_second(&eval).expect("Y")).expect("host");
1878        assert_eq!(y_out.shape, vec![3, 3]);
1879        assert_eq!(
1880            y_out.materialize_f64(),
1881            vec![-1.0, 0.0, 1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0]
1882        );
1883    }
1884
1885    #[test]
1886    fn meshgrid_preserves_native_single_storage() {
1887        let x = Tensor::from_f32(vec![-1.0, 0.0, 1.0], vec![1, 3]).expect("single axis");
1888        let eval = evaluate(&[Value::Tensor(x)]).expect("meshgrid");
1889        let Value::Tensor(x_out) = eval_first(&eval).expect("X") else {
1890            panic!("expected tensor X output");
1891        };
1892        let Value::Tensor(y_out) = eval_second(&eval).expect("Y") else {
1893            panic!("expected tensor Y output");
1894        };
1895        assert_eq!(
1896            x_out.into_numeric_storage().expect("single X storage"),
1897            NumericStorage::F32(vec![-1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0])
1898        );
1899        assert_eq!(
1900            y_out.into_numeric_storage().expect("single Y storage"),
1901            NumericStorage::F32(vec![-1.0, 0.0, 1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0])
1902        );
1903    }
1904
1905    #[test]
1906    fn meshgrid_like_follows_compatibility_mode() {
1907        let axis = || Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("axis");
1908        {
1909            let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1910            let error =
1911                match evaluate(&[Value::Tensor(axis()), Value::from("like"), Value::Num(0.0)]) {
1912                    Ok(_) => panic!("MATLAB mode rejects meshgrid like"),
1913                    Err(error) => error,
1914                };
1915            assert_eq!(
1916                error.identifier(),
1917                Some("RunMat:compatibility:MeshgridLikeExtension")
1918            );
1919        }
1920        {
1921            let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1922            evaluate(&[Value::Tensor(axis()), Value::from("like"), Value::Num(0.0)])
1923                .expect("RunMat mode accepts meshgrid like");
1924        }
1925    }
1926
1927    #[test]
1928    fn meshgrid_complex_axes_follow_compatibility_mode() {
1929        let axis = || ComplexTensor::new(vec![(1.0, 2.0), (3.0, 4.0)], vec![1, 2]).expect("axis");
1930        {
1931            let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1932            let error = match evaluate(&[Value::ComplexTensor(axis())]) {
1933                Ok(_) => panic!("MATLAB mode rejects complex meshgrid axes"),
1934                Err(error) => error,
1935            };
1936            assert_eq!(
1937                error.identifier(),
1938                Some("RunMat:compatibility:MeshgridComplexAxesExtension")
1939            );
1940        }
1941        {
1942            let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1943            evaluate(&[Value::ComplexTensor(axis())])
1944                .expect("RunMat mode accepts complex meshgrid axes");
1945        }
1946    }
1947
1948    #[test]
1949    fn meshgrid_type_infers_rank_from_axis_count() {
1950        let ctx = ResolveContext::new(Vec::new());
1951        assert_eq!(
1952            meshgrid_type(&[Type::Num, Type::Num], &ctx),
1953            Type::Tensor {
1954                shape: Some(vec![Some(1), Some(1)])
1955            }
1956        );
1957        assert_eq!(
1958            meshgrid_type(&[Type::Num, Type::Num, Type::Num], &ctx),
1959            Type::Tensor {
1960                shape: Some(vec![Some(1), Some(1), Some(1)])
1961            }
1962        );
1963    }
1964
1965    #[test]
1966    fn meshgrid_type_uses_vector_lengths() {
1967        let ctx = ResolveContext::new(Vec::new());
1968        assert_eq!(
1969            meshgrid_type(
1970                &[
1971                    Type::Tensor {
1972                        shape: Some(vec![Some(1), Some(201)]),
1973                    },
1974                    Type::Tensor {
1975                        shape: Some(vec![Some(1), Some(101)]),
1976                    },
1977                ],
1978                &ctx,
1979            ),
1980            Type::Tensor {
1981                shape: Some(vec![Some(101), Some(201)])
1982            }
1983        );
1984    }
1985
1986    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1987    #[test]
1988    fn meshgrid_rectangular_inputs() {
1989        let x = tensor_from_vec(vec![0.0, 0.5, 1.0], 1, 3);
1990        let y = tensor_from_vec(vec![10.0, 20.0], 2, 1);
1991        let eval = evaluate(&[Value::Tensor(x), Value::Tensor(y)]).expect("meshgrid");
1992        assert_eq!(eval.output_count(), 2);
1993        let x_out = test_support::gather(eval_first(&eval).expect("X")).expect("host");
1994        assert_eq!(x_out.shape, vec![2, 3]);
1995        assert_eq!(x_out.materialize_f64(), vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0]);
1996        let y_out = test_support::gather(eval_second(&eval).expect("Y")).expect("host");
1997        assert_eq!(y_out.shape, vec![2, 3]);
1998        assert_eq!(
1999            y_out.materialize_f64(),
2000            vec![10.0, 20.0, 10.0, 20.0, 10.0, 20.0]
2001        );
2002    }
2003
2004    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2005    #[test]
2006    fn meshgrid_three_inputs_volume() {
2007        let x = tensor_from_vec(vec![1.0, 2.0], 1, 2);
2008        let y = tensor_from_vec(vec![5.0, 6.0, 7.0], 3, 1);
2009        let z = tensor_from_vec(vec![0.0, 1.0], 1, 2);
2010        let eval =
2011            evaluate(&[Value::Tensor(x), Value::Tensor(y), Value::Tensor(z)]).expect("meshgrid");
2012        assert_eq!(eval.output_count(), 3);
2013        let x_out = test_support::gather(eval_first(&eval).expect("X")).expect("host");
2014        assert_eq!(x_out.shape, vec![3, 2, 2]);
2015        assert_eq!(
2016            x_out.materialize_f64(),
2017            vec![1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0]
2018        );
2019        let z_out = test_support::gather(eval_third(&eval).expect("Z")).expect("host");
2020        assert_eq!(z_out.shape, vec![3, 2, 2]);
2021        assert_eq!(
2022            z_out.materialize_f64(),
2023            vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
2024        );
2025    }
2026
2027    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2028    #[test]
2029    fn meshgrid_like_keeps_gpu_residency() {
2030        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2031        test_support::with_test_provider(|provider| {
2032            let x = tensor_from_vec(vec![-1.0, 0.0, 1.0], 1, 3);
2033            let y = tensor_from_vec(vec![2.0, 4.0], 2, 1);
2034            let proto = Tensor::new(vec![0.0], vec![1, 1]).unwrap();
2035            let proto_view = HostTensorView {
2036                data: &proto.materialize_f64(),
2037                shape: &proto.shape,
2038            };
2039            let proto_handle = provider.upload(&proto_view).expect("upload");
2040            let eval = evaluate(&[
2041                Value::Tensor(x),
2042                Value::Tensor(y),
2043                Value::from("like"),
2044                Value::GpuTensor(proto_handle),
2045            ])
2046            .expect("meshgrid");
2047            let x_value = eval_first(&eval).expect("X");
2048            assert!(matches!(x_value, Value::GpuTensor(_)));
2049            let gathered = test_support::gather(x_value).expect("gather");
2050            assert_eq!(gathered.shape, vec![2, 3]);
2051        });
2052    }
2053
2054    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2055    #[test]
2056    fn meshgrid_gpu_inputs_roundtrip() {
2057        test_support::with_test_provider(|provider| {
2058            let x = tensor_from_vec(vec![0.0, 0.5], 1, 2);
2059            let y = tensor_from_vec(vec![1.0, 2.0], 2, 1);
2060            let x_view = HostTensorView {
2061                data: &x.materialize_f64(),
2062                shape: &x.shape,
2063            };
2064            let y_view = HostTensorView {
2065                data: &y.materialize_f64(),
2066                shape: &y.shape,
2067            };
2068            let x_handle = provider.upload(&x_view).expect("upload");
2069            let y_handle = provider.upload(&y_view).expect("upload");
2070            let eval = evaluate(&[Value::GpuTensor(x_handle), Value::GpuTensor(y_handle)])
2071                .expect("meshgrid");
2072            assert!(matches!(eval_first(&eval).expect("X"), Value::GpuTensor(_)));
2073            assert!(matches!(
2074                eval_second(&eval).expect("Y"),
2075                Value::GpuTensor(_)
2076            ));
2077        });
2078    }
2079
2080    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2081    #[test]
2082    #[cfg(feature = "wgpu")]
2083    fn meshgrid_wgpu_matches_cpu() {
2084        let _guard = test_support::accel_test_lock();
2085        let Ok(provider) = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2086            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2087        ) else {
2088            return;
2089        };
2090
2091        let x = tensor_from_vec(vec![-1.0, 0.0, 1.0, 2.0], 1, 4);
2092        let y = tensor_from_vec(vec![5.0, 6.0], 2, 1);
2093
2094        let cpu_eval =
2095            evaluate(&[Value::Tensor(x.clone()), Value::Tensor(y.clone())]).expect("meshgrid cpu");
2096        let cpu_x =
2097            test_support::gather(eval_first(&cpu_eval).expect("X cpu")).expect("gather X cpu");
2098        let cpu_y =
2099            test_support::gather(eval_second(&cpu_eval).expect("Y cpu")).expect("gather Y cpu");
2100
2101        let x_view = HostTensorView {
2102            data: &x.materialize_f64(),
2103            shape: &x.shape,
2104        };
2105        let y_view = HostTensorView {
2106            data: &y.materialize_f64(),
2107            shape: &y.shape,
2108        };
2109        let x_gpu = provider.upload(&x_view).expect("upload x");
2110        let y_gpu = provider.upload(&y_view).expect("upload y");
2111
2112        let gpu_eval =
2113            evaluate(&[Value::GpuTensor(x_gpu), Value::GpuTensor(y_gpu)]).expect("meshgrid gpu");
2114        let gpu_x_value = eval_first(&gpu_eval).expect("X gpu");
2115        let gpu_y_value = eval_second(&gpu_eval).expect("Y gpu");
2116
2117        assert!(matches!(gpu_x_value, Value::GpuTensor(_)));
2118        assert!(matches!(gpu_y_value, Value::GpuTensor(_)));
2119
2120        let gathered_x = test_support::gather(gpu_x_value).expect("gather X gpu");
2121        let gathered_y = test_support::gather(gpu_y_value).expect("gather Y gpu");
2122
2123        assert_eq!(gathered_x.shape, cpu_x.shape);
2124        assert_eq!(gathered_x.materialize_f64(), cpu_x.materialize_f64());
2125        assert_eq!(gathered_y.shape, cpu_y.shape);
2126        assert_eq!(gathered_y.materialize_f64(), cpu_y.materialize_f64());
2127    }
2128
2129    #[test]
2130    #[cfg(feature = "wgpu")]
2131    fn meshgrid_wgpu_integer_residency_obeys_automatic_and_explicit_policy() {
2132        let _guard = test_support::accel_test_lock();
2133        let Ok(provider) = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2134            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2135        ) else {
2136            return;
2137        };
2138        let axis = Tensor::new_integer(
2139            IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
2140            vec![1, 2],
2141        )
2142        .expect("axis");
2143
2144        let automatic = gpu_helpers::upload_tensor(provider, &axis).expect("automatic upload");
2145        let automatic =
2146            automatic.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
2147        let eval = evaluate(&[Value::GpuTensor(automatic)]).expect("automatic gather");
2148        let Value::Tensor(output) = eval_first(&eval).expect("exact host output") else {
2149            panic!("automatic integer residency must become host output");
2150        };
2151        assert_eq!(
2152            output.integer_storage(),
2153            Some(&IntegerStorage::U64(vec![
2154                9_007_199_254_740_993,
2155                9_007_199_254_740_993,
2156                u64::MAX,
2157                u64::MAX,
2158            ]))
2159        );
2160
2161        let explicit = gpu_helpers::upload_tensor(provider, &axis).expect("explicit upload");
2162        let explicit =
2163            explicit.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
2164        let error = match evaluate(&[Value::GpuTensor(explicit)]) {
2165            Ok(_) => panic!("explicit integer gpuArray must reject"),
2166            Err(error) => error,
2167        };
2168        assert!(error.message().contains("integer gpuArray axes"));
2169    }
2170
2171    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2172    #[test]
2173    fn meshgrid_complex_inputs_produce_complex_outputs() {
2174        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2175        let complex = ComplexTensor::new(vec![(1.0, 1.0), (2.0, -1.0)], vec![1, 2]).unwrap();
2176        let eval = evaluate(&[Value::ComplexTensor(complex)]).expect("meshgrid");
2177        let x_value = eval_first(&eval).expect("X");
2178        match x_value {
2179            Value::ComplexTensor(ct) => {
2180                assert_eq!(ct.shape, vec![2, 2]);
2181            }
2182            Value::Complex(_, _) => {}
2183            other => panic!("expected complex output, got {other:?}"),
2184        }
2185    }
2186
2187    #[test]
2188    fn meshgrid_preserves_native_complex_single_storage() {
2189        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2190        let complex = ComplexTensor::from_f32(vec![(1.25, -2.5), (3.75, 4.5)], vec![1, 2]).unwrap();
2191        let eval = evaluate(&[Value::ComplexTensor(complex)]).expect("meshgrid");
2192        let Value::ComplexTensor(output) = eval_first(&eval).expect("X") else {
2193            panic!("expected complex single output");
2194        };
2195        assert_eq!(output.numeric_dtype(), NumericDType::F32);
2196        assert_eq!(output.shape, vec![2, 2]);
2197        assert_eq!(
2198            output.as_f32_slice(),
2199            Some(&[(1.25, -2.5), (1.25, -2.5), (3.75, 4.5), (3.75, 4.5),][..])
2200        );
2201    }
2202
2203    #[test]
2204    fn meshgrid_like_complex_gpu_prototype_keeps_complex_residency() {
2205        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2206        test_support::with_test_provider(|provider| {
2207            let x = tensor_from_vec(vec![1.0, 2.0], 1, 2);
2208            let proto = ComplexTensor::new(vec![(0.0, 1.0)], vec![1, 1]).unwrap();
2209            let proto_handle =
2210                gpu_helpers::upload_complex_tensor(provider, &proto).expect("upload");
2211
2212            let eval = evaluate(&[
2213                Value::Tensor(x),
2214                Value::from("like"),
2215                Value::GpuTensor(proto_handle),
2216            ])
2217            .expect("meshgrid");
2218            let x_value = eval_first(&eval).expect("X");
2219            let Value::GpuTensor(handle) = x_value else {
2220                panic!("expected complex gpu tensor");
2221            };
2222            assert_eq!(
2223                runmat_accelerate_api::handle_storage(&handle),
2224                GpuTensorStorage::ComplexInterleaved
2225            );
2226            let gathered = block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(handle)))
2227                .expect("gather");
2228            let Value::ComplexTensor(tensor) = gathered else {
2229                panic!("expected complex tensor");
2230            };
2231            assert_eq!(tensor.shape, vec![2, 2]);
2232            assert_eq!(
2233                tensor.materialize_f64(),
2234                vec![(1.0, 0.0), (1.0, 0.0), (2.0, 0.0), (2.0, 0.0)]
2235            );
2236        });
2237    }
2238
2239    #[test]
2240    fn meshgrid_complex_gpu_axis_stays_resident() {
2241        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2242        test_support::with_test_provider(|provider| {
2243            let axis = ComplexTensor::new(vec![(1.0, 1.0), (2.0, -1.0)], vec![1, 2]).unwrap();
2244            let axis_handle = gpu_helpers::upload_complex_tensor(provider, &axis).expect("upload");
2245
2246            let eval = evaluate(&[Value::GpuTensor(axis_handle)]).expect("meshgrid");
2247            let x_value = eval_first(&eval).expect("X");
2248            let Value::GpuTensor(handle) = x_value else {
2249                panic!("expected complex gpu tensor");
2250            };
2251            assert_eq!(
2252                runmat_accelerate_api::handle_storage(&handle),
2253                GpuTensorStorage::ComplexInterleaved
2254            );
2255            let gathered = block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(handle)))
2256                .expect("gather");
2257            let Value::ComplexTensor(tensor) = gathered else {
2258                panic!("expected complex tensor");
2259            };
2260            assert_eq!(tensor.shape, vec![2, 2]);
2261            assert_eq!(
2262                tensor.materialize_f64(),
2263                vec![(1.0, 1.0), (1.0, 1.0), (2.0, -1.0), (2.0, -1.0)]
2264            );
2265        });
2266    }
2267
2268    #[test]
2269    fn meshgrid_paired_complex_integer_gpu_axis_stays_resident_and_exact() {
2270        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2271        test_support::with_test_provider(|provider| {
2272            let wide = (1_u64 << 53) + 1;
2273            let axis = ComplexTensor::new_integer(
2274                IntegerComplexStorage::new(
2275                    IntegerStorage::U64(vec![wide, u64::MAX]),
2276                    IntegerStorage::U64(vec![u64::MAX, wide]),
2277                )
2278                .expect("paired axis storage"),
2279                vec![1, 2],
2280            )
2281            .expect("paired axis");
2282            let axis_handle =
2283                gpu_helpers::upload_complex_tensor(provider, &axis).expect("upload paired axis");
2284
2285            let eval = evaluate(&[Value::GpuTensor(axis_handle.clone())]).expect("meshgrid");
2286            let Value::GpuTensor(output) = eval_first(&eval).expect("X") else {
2287                panic!("expected resident paired grid");
2288            };
2289            assert_eq!(
2290                runmat_accelerate_api::handle_storage(&output),
2291                GpuTensorStorage::ComplexInterleaved
2292            );
2293            assert_eq!(
2294                runmat_accelerate_api::handle_integer_type(&output),
2295                Some(runmat_accelerate_api::IntegerElementType::U64)
2296            );
2297            assert!(runmat_accelerate_api::provider_for_handle(&output)
2298                .is_some_and(|owner| std::ptr::eq(owner, provider)));
2299            let gathered = block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(
2300                output.clone(),
2301            )))
2302            .expect("gather paired grid");
2303            let Value::ComplexTensor(gathered) = gathered else {
2304                panic!("expected paired tensor");
2305            };
2306            let storage = gathered.integer_storage().expect("paired storage");
2307            assert_eq!(
2308                storage.real,
2309                IntegerStorage::U64(vec![wide, wide, u64::MAX, u64::MAX])
2310            );
2311            assert_eq!(
2312                storage.imag,
2313                IntegerStorage::U64(vec![u64::MAX, u64::MAX, wide, wide])
2314            );
2315            provider.free(&axis_handle).expect("free axis");
2316            provider.free(&output).expect("free output");
2317        });
2318    }
2319
2320    #[test]
2321    #[cfg(feature = "wgpu")]
2322    fn meshgrid_wgpu_complex_axis_matches_cpu_and_stays_resident() {
2323        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2324        let _guard = test_support::accel_test_lock();
2325        let Ok(provider) = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2326            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2327        ) else {
2328            return;
2329        };
2330
2331        let axis = ComplexTensor::new(vec![(1.0, 1.0), (2.0, -1.0)], vec![1, 2]).unwrap();
2332        let cpu_eval = evaluate(&[Value::ComplexTensor(axis.clone())]).expect("meshgrid cpu");
2333        let cpu_x = match eval_first(&cpu_eval).expect("X cpu") {
2334            Value::ComplexTensor(tensor) => tensor,
2335            other => panic!("expected cpu complex tensor, got {other:?}"),
2336        };
2337
2338        let axis_handle = gpu_helpers::upload_complex_tensor(provider, &axis).expect("upload");
2339        let gpu_eval = evaluate(&[Value::GpuTensor(axis_handle)]).expect("meshgrid gpu");
2340        let gpu_x = eval_first(&gpu_eval).expect("X gpu");
2341        let Value::GpuTensor(handle) = gpu_x else {
2342            panic!("expected complex gpu tensor");
2343        };
2344        assert_eq!(
2345            runmat_accelerate_api::handle_storage(&handle),
2346            GpuTensorStorage::ComplexInterleaved
2347        );
2348        let gathered =
2349            block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(handle))).expect("gather");
2350        let Value::ComplexTensor(gpu_tensor) = gathered else {
2351            panic!("expected complex tensor");
2352        };
2353        assert_eq!(gpu_tensor.shape, cpu_x.shape);
2354        assert_eq!(gpu_tensor.materialize_f64(), cpu_x.materialize_f64());
2355    }
2356
2357    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2358    #[test]
2359    fn meshgrid_like_host_prototype() {
2360        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2361        let x = tensor_from_vec(vec![1.0, 2.0], 1, 2);
2362        let eval =
2363            evaluate(&[Value::Tensor(x), Value::from("like"), Value::Num(0.0)]).expect("meshgrid");
2364        let x_out = eval_first(&eval).expect("X");
2365        assert!(matches!(x_out, Value::Tensor(_) | Value::Num(_)));
2366    }
2367}