Skip to main content

runmat_runtime/builtins/missing/
mod.rs

1//! MATLAB-compatible missing-value construction, predicates, cleanup, and NaN-aware aliases.
2
3use runmat_builtins::{
4    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
5    BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
6    BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
7    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
8    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
9    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
10    ResolveContext, Type,
11};
12use runmat_macros::runtime_builtin;
13use runmat_value::{
14    CellArray, CharArray, IntValue, LogicalArray, NumericDType, ObjectInstance, StringArray,
15    StructValue, Tensor, Value,
16};
17
18use crate::builtins::common::{gpu_helpers, tensor as tensor_utils};
19use crate::builtins::math::reduction::{mean, median, min, std as std_reduction, sum, var};
20use crate::builtins::table::{
21    is_tabular_object, select_rows, selected_row_names, table_from_columns_like, table_height,
22    table_variable_names_from_object, table_variables, table_width,
23};
24use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
25
26const MISSING_TEXT: &str = "<missing>";
27
28const VALUE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
29    name: "B",
30    ty: BuiltinParamType::Any,
31    arity: BuiltinParamArity::Required,
32    default: None,
33    description: "Result value.",
34}];
35const LOGICAL_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
36    name: "TF",
37    ty: BuiltinParamType::LogicalArray,
38    arity: BuiltinParamArity::Required,
39    default: None,
40    description: "Logical missing-value mask.",
41}];
42const VALUE_AND_MASK_OUTPUTS: [BuiltinParamDescriptor; 2] = [
43    BuiltinParamDescriptor {
44        name: "B",
45        ty: BuiltinParamType::Any,
46        arity: BuiltinParamArity::Required,
47        default: None,
48        description: "Result value.",
49    },
50    BuiltinParamDescriptor {
51        name: "TF",
52        ty: BuiltinParamType::LogicalArray,
53        arity: BuiltinParamArity::Optional,
54        default: None,
55        description: "Logical mask of entries, rows, or columns that were filled or removed.",
56    },
57];
58const VALUE_INPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
59    name: "A",
60    ty: BuiltinParamType::Any,
61    arity: BuiltinParamArity::Required,
62    default: None,
63    description: "Input value.",
64}];
65const VARIADIC_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
66    name: "args",
67    ty: BuiltinParamType::Any,
68    arity: BuiltinParamArity::Variadic,
69    default: None,
70    description: "Size, method, dimension, or option arguments.",
71}];
72const VALUE_AND_ARGS_INPUTS: [BuiltinParamDescriptor; 2] = [
73    BuiltinParamDescriptor {
74        name: "A",
75        ty: BuiltinParamType::Any,
76        arity: BuiltinParamArity::Required,
77        default: None,
78        description: "Input value.",
79    },
80    BuiltinParamDescriptor {
81        name: "args",
82        ty: BuiltinParamType::Any,
83        arity: BuiltinParamArity::Variadic,
84        default: None,
85        description: "Method, dimension, or option arguments.",
86    },
87];
88
89const MISSING_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
90    BuiltinSignatureDescriptor {
91        label: "missing",
92        inputs: &[],
93        outputs: &VALUE_OUTPUT,
94    },
95    BuiltinSignatureDescriptor {
96        label: "missing(sz)",
97        inputs: &VARIADIC_INPUTS,
98        outputs: &VALUE_OUTPUT,
99    },
100];
101const ONE_VALUE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
102    label: "TF = ismissing(A)",
103    inputs: &VALUE_INPUT,
104    outputs: &LOGICAL_OUTPUT,
105}];
106const ANYMISSING_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
107    label: "TF = anymissing(A)",
108    inputs: &VALUE_INPUT,
109    outputs: &LOGICAL_OUTPUT,
110}];
111const FILLMISSING_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
112    label: "B = fillmissing(A, method, ...)",
113    inputs: &VALUE_AND_ARGS_INPUTS,
114    outputs: &VALUE_AND_MASK_OUTPUTS,
115}];
116const RMMISSING_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
117    label: "B = rmmissing(A, ...)",
118    inputs: &VALUE_AND_ARGS_INPUTS,
119    outputs: &VALUE_AND_MASK_OUTPUTS,
120}];
121const STANDARDIZE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
122    label: "B = standardizeMissing(A, indicators)",
123    inputs: &VALUE_AND_ARGS_INPUTS,
124    outputs: &VALUE_OUTPUT,
125}];
126const NANAWARE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
127    label: "B = nanmean(A, ...)",
128    inputs: &VALUE_AND_ARGS_INPUTS,
129    outputs: &VALUE_OUTPUT,
130}];
131
132const MISSING_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
133    code: "RM.MISSING.INVALID_ARGUMENT",
134    identifier: Some("RunMat:missing:InvalidArgument"),
135    when: "Arguments do not match a supported missing-value syntax.",
136    message: "missing-value builtin: invalid argument",
137};
138const MISSING_ERROR_UNSUPPORTED_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
139    code: "RM.MISSING.UNSUPPORTED_TYPE",
140    identifier: Some("RunMat:missing:UnsupportedType"),
141    when: "The input type has no missing-value representation in RunMat.",
142    message: "missing-value builtin: unsupported input type",
143};
144const MISSING_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
145    code: "RM.MISSING.INTERNAL",
146    identifier: Some("RunMat:missing:InternalError"),
147    when: "Internal shape or table materialization fails.",
148    message: "missing-value builtin: internal error",
149};
150const MISSING_ERRORS: [BuiltinErrorDescriptor; 3] = [
151    MISSING_ERROR_INVALID_ARGUMENT,
152    MISSING_ERROR_UNSUPPORTED_TYPE,
153    MISSING_ERROR_INTERNAL,
154];
155
156const MISSING_SHAPED_ARRAY_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
157    id: "missing-shaped-array",
158    mode: BuiltinExtensionMode::RunMatOnly,
159    description: "missing(size...) is a RunMat convenience; the documented MATLAB missing function accepts no input arguments",
160    error_identifier: Some("RunMat:compatibility:MissingShapedArrayExtension"),
161};
162pub const MISSING_EXTENSIONS: [BuiltinExtensionDescriptor; 1] = [MISSING_SHAPED_ARRAY_EXTENSION];
163
164const STANDARDIZE_MISSING_INTEGER_DATA_EXTENSION: BuiltinExtensionDescriptor =
165    BuiltinExtensionDescriptor {
166        id: "standardize-missing-integer-data",
167        mode: BuiltinExtensionMode::RunMatOnly,
168        description:
169            "standardizeMissing with a bare typed-integer input array is a RunMat extension",
170        error_identifier: Some("RunMat:compatibility:StandardizeMissingIntegerDataExtension"),
171    };
172const STANDARDIZE_MISSING_EXPLICIT_GPU_INDICATOR_EXTENSION: BuiltinExtensionDescriptor =
173    BuiltinExtensionDescriptor {
174        id: "standardize-missing-explicit-gpu-indicator",
175        mode: BuiltinExtensionMode::RunMatOnly,
176        description:
177            "standardizeMissing with an explicitly GPU-resident indicator is a RunMat extension",
178        error_identifier: Some(
179            "RunMat:compatibility:StandardizeMissingExplicitGpuIndicatorExtension",
180        ),
181    };
182pub const STANDARDIZE_MISSING_EXTENSIONS: [BuiltinExtensionDescriptor; 2] = [
183    STANDARDIZE_MISSING_INTEGER_DATA_EXTENSION,
184    STANDARDIZE_MISSING_EXPLICIT_GPU_INDICATOR_EXTENSION,
185];
186
187const STANDARDIZE_MISSING_INTEGER_DATA_INPUTS: [BuiltinIntegerInputCapability; 1] =
188    [BuiltinIntegerInputCapability {
189        name: "A",
190        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
191        availability: BuiltinIntegerInputAvailability::RunMatOnly,
192        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
193        notes: "The compatibility target's array-input datatype table excludes integer arrays. RunMat mode treats a bare integer array as an exact no-op because integer classes have no standard missing value.",
194    }];
195const STANDARDIZE_MISSING_INTEGER_INDICATOR_INPUTS: [BuiltinIntegerInputCapability; 1] =
196    [BuiltinIntegerInputCapability {
197        name: "indicator",
198        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
199        availability: BuiltinIntegerInputAvailability::Documented,
200        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
201        notes: "The compatibility target explicitly states that single, integer, and logical indicators also match double entries of A.",
202    }];
203const STANDARDIZE_MISSING_TABLE_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
204    [BuiltinIntegerInputCapability {
205        name: "integer table variables",
206        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
207        availability: BuiltinIntegerInputAvailability::Documented,
208        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
209        notes: "Table input is documented and preserves each variable datatype. Integer variables have no standard missing representation and therefore remain unchanged.",
210    }];
211pub const STANDARDIZE_MISSING_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 3] = [
212    BuiltinIntegerCapabilityDescriptor {
213        form: "B = standardizeMissing(integer_A, indicator)",
214        inputs: &STANDARDIZE_MISSING_INTEGER_DATA_INPUTS,
215        computation_domain: BuiltinIntegerComputationDomain::Structural,
216        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
217        overflow: BuiltinIntegerOverflowRule::NotApplicable,
218        backend: BuiltinIntegerBackendRule::GatherFallback,
219        overload: BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
220        notes: "The RunMat-only bare-array form preserves class, shape, and exact storage. Compatibility admission precedes provider access; automatic residency may gather transparently after admission.",
221    },
222    BuiltinIntegerCapabilityDescriptor {
223        form: "B = standardizeMissing(A, integer_indicator)",
224        inputs: &STANDARDIZE_MISSING_INTEGER_INDICATOR_INPUTS,
225        computation_domain: BuiltinIntegerComputationDomain::FunctionSpecific,
226        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
227        overflow: BuiltinIntegerOverflowRule::NotApplicable,
228        backend: BuiltinIntegerBackendRule::GatherFallback,
229        overload: BuiltinIntegerOverloadKind::Multiple,
230        notes: "Integer indicators are read from authoritative storage and compared in the documented target-class matching domain. Explicit gpuArray indicators are separately gated; automatic residency remains transparent.",
231    },
232    BuiltinIntegerCapabilityDescriptor {
233        form: "B = standardizeMissing(table_with_integer_variables, indicator)",
234        inputs: &STANDARDIZE_MISSING_TABLE_INTEGER_INPUTS,
235        computation_domain: BuiltinIntegerComputationDomain::Structural,
236        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
237        overflow: BuiltinIntegerOverflowRule::NotApplicable,
238        backend: BuiltinIntegerBackendRule::GatherFallback,
239        overload: BuiltinIntegerOverloadKind::Multiple,
240        notes: "Integer table variables pass through with exact native storage while supported floating or textual variables are standardized independently.",
241    },
242];
243
244const MISSING_INTEGER_SIZE_INPUTS: [BuiltinIntegerInputCapability; 1] =
245    [BuiltinIntegerInputCapability {
246        name: "size arguments",
247        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
248        availability: BuiltinIntegerInputAvailability::RunMatOnly,
249        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
250        notes: "Every native integer size is read exactly and checked against nonnegative platform allocation limits; the entire shaped-array syntax is a RunMat-only convenience.",
251    }];
252pub const MISSING_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
253    [BuiltinIntegerCapabilityDescriptor { form: "missing(integer_size, ...)", inputs: &MISSING_INTEGER_SIZE_INPUTS, computation_domain: BuiltinIntegerComputationDomain::Structural, output_class: BuiltinIntegerOutputClassRule::FunctionSpecific, overflow: BuiltinIntegerOverflowRule::Error, backend: BuiltinIntegerBackendRule::GatherFallback, overload: BuiltinIntegerOverloadKind::StructuralParameter, notes: "MATLAB-compatible modes reject every argument before provider access because public missing has only a zero-argument syntax. RunMat mode gathers admitted automatic or explicit size controls through their exact owner and creates a host string array." }];
254
255macro_rules! descriptor {
256    ($name:ident, $signatures:ident, $mode:expr) => {
257        pub const $name: BuiltinDescriptor = BuiltinDescriptor {
258            signatures: &$signatures,
259            output_mode: $mode,
260            completion_policy: BuiltinCompletionPolicy::Public,
261            errors: &MISSING_ERRORS,
262        };
263    };
264}
265
266descriptor!(
267    MISSING_DESCRIPTOR,
268    MISSING_SIGNATURES,
269    BuiltinOutputMode::Fixed
270);
271descriptor!(
272    ISMISSING_DESCRIPTOR,
273    ONE_VALUE_SIGNATURES,
274    BuiltinOutputMode::Fixed
275);
276const ISMISSING_RESIDENT_INPUT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
277    id: "ismissing-resident-input",
278    mode: BuiltinExtensionMode::RunMatOnly,
279    description: "ismissing with an interactive GPU-resident input is a RunMat extension",
280    error_identifier: Some("RunMat:compatibility:IsmissingResidentInputExtension"),
281};
282const ISMISSING_EXTENSIONS: [BuiltinExtensionDescriptor; 1] = [ISMISSING_RESIDENT_INPUT_EXTENSION];
283const ISMISSING_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
284    [BuiltinIntegerInputCapability {
285        name: "A",
286        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
287        availability: BuiltinIntegerInputAvailability::Documented,
288        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
289        notes: "All eight fixed-width integer classes have no standard missing value.",
290    }];
291pub const ISMISSING_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
292    [BuiltinIntegerCapabilityDescriptor {
293        form: "TF = ismissing(integer_A)",
294        inputs: &ISMISSING_INTEGER_INPUTS,
295        computation_domain: BuiltinIntegerComputationDomain::Predicate,
296        output_class: BuiltinIntegerOutputClassRule::Logical,
297        overflow: BuiltinIntegerOverflowRule::NotApplicable,
298        backend: BuiltinIntegerBackendRule::GatherFallback,
299        overload: BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
300        notes: "Returns a same-shaped all-false logical mask. Interactive resident input is a separately gated RunMat extension; admitted resident integers are validated from owner and class metadata without reading the payload and preserve this CPU builtin's host logical output policy.",
301    }];
302descriptor!(
303    ANYMISSING_DESCRIPTOR,
304    ANYMISSING_SIGNATURES,
305    BuiltinOutputMode::Fixed
306);
307
308const ANYMISSING_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
309    [BuiltinIntegerInputCapability {
310        name: "A",
311        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
312        availability: BuiltinIntegerInputAvailability::Documented,
313        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
314        notes: "All eight built-in integer classes have no standard missing value.",
315    }];
316pub const ANYMISSING_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
317    [BuiltinIntegerCapabilityDescriptor {
318        form: "TF = anymissing(integer_A)",
319        inputs: &ANYMISSING_INTEGER_INPUTS,
320        computation_domain: BuiltinIntegerComputationDomain::Predicate,
321        output_class: BuiltinIntegerOutputClassRule::Logical,
322        overflow: BuiltinIntegerOverflowRule::NotApplicable,
323        backend: BuiltinIntegerBackendRule::GatherFallback,
324        overload: BuiltinIntegerOverloadKind::Multiple,
325        notes: "Integer scalars and arrays return logical false because integer classes have no default missing representation; resident inputs gather without floating conversion.",
326    }];
327
328const RMMISSING_INTEGER_DIM_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
329    id: "rmmissing-integer-dimension",
330    mode: BuiltinExtensionMode::RunMatOnly,
331    description: "rmmissing accepts a typed-integer dimension control as a RunMat extension",
332    error_identifier: Some("RunMat:compatibility:RmmissingIntegerDimensionExtension"),
333};
334pub const RMMISSING_EXTENSIONS: [BuiltinExtensionDescriptor; 1] = [RMMISSING_INTEGER_DIM_EXTENSION];
335const RMMISSING_INTEGER_DATA_INPUTS: [BuiltinIntegerInputCapability; 1] =
336    [BuiltinIntegerInputCapability {
337        name: "A",
338        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
339        availability: BuiltinIntegerInputAvailability::Documented,
340        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
341        notes: "The R2022a-and-later surface accepts datatypes without a standard missing definition; all eight integer classes therefore remain unchanged.",
342    }];
343const RMMISSING_INTEGER_DIM_INPUTS: [BuiltinIntegerInputCapability; 1] =
344    [BuiltinIntegerInputCapability {
345        name: "dim",
346        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
347        availability: BuiltinIntegerInputAvailability::RunMatOnly,
348        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
349        notes: "RunMat additionally accepts a typed integer dimension scalar and reads it exactly as a structural control.",
350    }];
351pub const RMMISSING_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
352    BuiltinIntegerCapabilityDescriptor {
353        form: "[R,TF] = rmmissing(integer_A, ...)",
354        inputs: &RMMISSING_INTEGER_DATA_INPUTS,
355        computation_domain: BuiltinIntegerComputationDomain::Structural,
356        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
357        overflow: BuiltinIntegerOverflowRule::NotApplicable,
358        backend: BuiltinIntegerBackendRule::GatherFallback,
359        overload: BuiltinIntegerOverloadKind::Multiple,
360        notes: "Integer A is an exact no-op because it has no standard missing value; R preserves class and storage, TF is all-false logical, and documented resident outputs return through the owning provider.",
361    },
362    BuiltinIntegerCapabilityDescriptor {
363        form: "R = rmmissing(A, integer_dim)",
364        inputs: &RMMISSING_INTEGER_DIM_INPUTS,
365        computation_domain: BuiltinIntegerComputationDomain::Structural,
366        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
367        overflow: BuiltinIntegerOverflowRule::Error,
368        backend: BuiltinIntegerBackendRule::GatherFallback,
369        overload: BuiltinIntegerOverloadKind::StructuralParameter,
370        notes: "The dimension extension is mode-gated before provider access and never crosses a floating boundary.",
371    },
372];
373
374const FILLMISSING_INTEGER_DATA_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
375    id: "fillmissing-integer-data",
376    mode: BuiltinExtensionMode::RunMatOnly,
377    description: "fillmissing with typed-integer input data is a RunMat extension",
378    error_identifier: Some("RunMat:compatibility:FillmissingIntegerDataExtension"),
379};
380const FILLMISSING_AGGREGATE_INTEGER_DATA_EXTENSION: BuiltinExtensionDescriptor =
381    BuiltinExtensionDescriptor {
382        id: "fillmissing-aggregate-integer-data",
383        mode: BuiltinExtensionMode::RunMatOnly,
384        description:
385            "fillmissing with integer data nested in a table or cell array is a RunMat extension",
386        error_identifier: Some("RunMat:compatibility:FillmissingAggregateIntegerDataExtension"),
387    };
388pub const FILLMISSING_EXTENSIONS: [BuiltinExtensionDescriptor; 2] = [
389    FILLMISSING_INTEGER_DATA_EXTENSION,
390    FILLMISSING_AGGREGATE_INTEGER_DATA_EXTENSION,
391];
392const FILLMISSING_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
393    [BuiltinIntegerInputCapability {
394        name: "A",
395        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
396        availability: BuiltinIntegerInputAvailability::RunMatOnly,
397        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
398        notes: "Integer arrays have no standard missing value. RunMat mode preserves authoritative same-class storage and returns an all-false filled-entry mask.",
399    }];
400const FILLMISSING_AGGREGATE_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
401    [BuiltinIntegerInputCapability {
402        name: "table variables or nested cell contents",
403        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
404        availability: BuiltinIntegerInputAvailability::RunMatOnly,
405        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
406        notes: "The aggregate is recursively classified before any resident child can be gathered; integer children retain exact same-class storage and contribute false entries to the filled mask.",
407    }];
408pub const FILLMISSING_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
409    BuiltinIntegerCapabilityDescriptor {
410        form: "[F, TF] = fillmissing(integer_A, method, ...)",
411        inputs: &FILLMISSING_INTEGER_INPUTS,
412        computation_domain: BuiltinIntegerComputationDomain::Structural,
413        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
414        overflow: BuiltinIntegerOverflowRule::NotApplicable,
415        backend: BuiltinIntegerBackendRule::GatherFallback,
416        overload: BuiltinIntegerOverloadKind::Multiple,
417        notes: "The RunMat-only integer form is an exact no-op because integer classes have no default missing representation; TF is logical false with the input shape.",
418    },
419    BuiltinIntegerCapabilityDescriptor {
420        form: "[F, TF] = fillmissing(table_or_cell_with_integer_data, method, ...)",
421        inputs: &FILLMISSING_AGGREGATE_INTEGER_INPUTS,
422        computation_domain: BuiltinIntegerComputationDomain::Structural,
423        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
424        overflow: BuiltinIntegerOverflowRule::NotApplicable,
425        backend: BuiltinIntegerBackendRule::GatherFallback,
426        overload: BuiltinIntegerOverloadKind::Multiple,
427        notes: "Nested table/cell integer data is a separately declared RunMat-only aggregate extension and is classified recursively before provider access.",
428    },
429];
430descriptor!(
431    FILLMISSING_DESCRIPTOR,
432    FILLMISSING_SIGNATURES,
433    BuiltinOutputMode::ByRequestedOutputCount
434);
435descriptor!(
436    RMMISSING_DESCRIPTOR,
437    RMMISSING_SIGNATURES,
438    BuiltinOutputMode::ByRequestedOutputCount
439);
440descriptor!(
441    STANDARDIZE_MISSING_DESCRIPTOR,
442    STANDARDIZE_SIGNATURES,
443    BuiltinOutputMode::Fixed
444);
445descriptor!(
446    NAN_AWARE_DESCRIPTOR,
447    NANAWARE_SIGNATURES,
448    BuiltinOutputMode::Fixed
449);
450
451macro_rules! integer_extension {
452    ($descriptor:ident, $extensions:ident, $id:literal, $description:literal, $error:literal) => {
453        const $descriptor: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
454            id: $id,
455            mode: BuiltinExtensionMode::RunMatOnly,
456            description: $description,
457            error_identifier: Some($error),
458        };
459        pub const $extensions: [BuiltinExtensionDescriptor; 1] = [$descriptor];
460    };
461}
462
463integer_extension!(
464    NANMEAN_INTEGER_EXTENSION,
465    NANMEAN_EXTENSIONS,
466    "nanmean-typed-integer-input",
467    "nanmean with a typed-integer data or control input is a RunMat extension",
468    "RunMat:compatibility:NanmeanTypedIntegerInputExtension"
469);
470integer_extension!(
471    NANSUM_INTEGER_EXTENSION,
472    NANSUM_EXTENSIONS,
473    "nansum-typed-integer-input",
474    "nansum with a typed-integer data or control input is a RunMat extension",
475    "RunMat:compatibility:NansumTypedIntegerInputExtension"
476);
477integer_extension!(
478    NANMIN_INTEGER_EXTENSION,
479    NANMIN_EXTENSIONS,
480    "nanmin-typed-integer-input",
481    "nanmin with a typed-integer data or control input is a RunMat extension",
482    "RunMat:compatibility:NanminTypedIntegerInputExtension"
483);
484integer_extension!(
485    NANMEDIAN_INTEGER_EXTENSION,
486    NANMEDIAN_EXTENSIONS,
487    "nanmedian-typed-integer-input",
488    "nanmedian with a typed-integer data or control input is a RunMat extension",
489    "RunMat:compatibility:NanmedianTypedIntegerInputExtension"
490);
491integer_extension!(
492    NANSTD_INTEGER_CONTROL_EXTENSION,
493    NANSTD_EXTENSIONS,
494    "nanstd-typed-integer-control",
495    "nanstd with a typed-integer normalization or dimension input is a RunMat extension",
496    "RunMat:compatibility:NanstdTypedIntegerControlExtension"
497);
498integer_extension!(
499    NANVAR_INTEGER_CONTROL_EXTENSION,
500    NANVAR_EXTENSIONS,
501    "nanvar-typed-integer-control",
502    "nanvar with a typed-integer normalization or dimension input is a RunMat extension",
503    "RunMat:compatibility:NanvarTypedIntegerControlExtension"
504);
505
506const MOVMAD_GPU_LARGE_WINDOW_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
507    id: "movmad-gpu-large-window",
508    mode: BuiltinExtensionMode::RunMatOnly,
509    description: "movmad with a window longer than 31 on a GPU input is a RunMat extension",
510    error_identifier: Some("RunMat:compatibility:MovmadGpuLargeWindowExtension"),
511};
512
513pub const MOVMAD_EXTENSIONS: [BuiltinExtensionDescriptor; 1] = [MOVMAD_GPU_LARGE_WINDOW_EXTENSION];
514
515const MOVMAD_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 3] = [
516    BuiltinIntegerInputCapability {
517        name: "A",
518        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
519        availability: BuiltinIntegerInputAvailability::Documented,
520        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
521        notes: "Moving median absolute deviation accepts every real integer input class and returns double deviation values.",
522    },
523    BuiltinIntegerInputCapability {
524        name: "k",
525        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
526        availability: BuiltinIntegerInputAvailability::Documented,
527        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
528        notes: "Count windows accept exact positive typed-integer lengths or integer-valued floating lengths.",
529    },
530    BuiltinIntegerInputCapability {
531        name: "dim",
532        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
533        availability: BuiltinIntegerInputAvailability::Documented,
534        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
535        notes: "The optional positive scalar dimension accepts exact typed integers or integer-valued floating values.",
536    },
537];
538
539pub const MOVMAD_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
540    [BuiltinIntegerCapabilityDescriptor {
541        form: "M = movmad(A, k, dim, nanflag)",
542        inputs: &MOVMAD_INTEGER_INPUTS,
543        computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
544        output_class: BuiltinIntegerOutputClassRule::Double,
545        overflow: BuiltinIntegerOverflowRule::NotApplicable,
546        backend: BuiltinIntegerBackendRule::GatherFallback,
547        overload: BuiltinIntegerOverloadKind::Multiple,
548        notes: "On the currently supported scalar-window surface, integer observations materialize once into the double median-absolute-deviation domain; supported resident inputs gather and re-upload double output, while GPU windows longer than 31 are separately mode-gated.",
549    }];
550
551const RUNMAT_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
552    [BuiltinIntegerInputCapability {
553        name: "A_or_control",
554        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
555        availability: BuiltinIntegerInputAvailability::RunMatOnly,
556        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
557        notes: "Typed-integer data, pairwise operands, and dimension controls are accepted only with the builtin's declared RunMat extension; documented single- and double-valued forms remain available in MATLAB-compatible mode.",
558    }];
559
560const REJECTED_INTEGER_DATA: [BuiltinIntegerInputCapability; 1] = [BuiltinIntegerInputCapability {
561    name: "A",
562    classes: &[],
563    availability: BuiltinIntegerInputAvailability::Rejected,
564    scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
565    notes: "Typed-integer data is rejected before host or provider reduction.",
566}];
567
568const RUNMAT_INTEGER_CONTROLS: [BuiltinIntegerInputCapability; 1] =
569    [BuiltinIntegerInputCapability {
570        name: "normalization_or_dimension",
571        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
572        availability: BuiltinIntegerInputAvailability::RunMatOnly,
573        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
574        notes: "Typed-integer normalization or dimension controls are accepted only with the builtin's declared RunMat extension; integer-valued double controls remain documented-compatible.",
575    }];
576
577pub const NANMEAN_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
578    [BuiltinIntegerCapabilityDescriptor {
579        form: "y = nanmean(A, args...)",
580        inputs: &RUNMAT_INTEGER_INPUTS,
581        computation_domain: BuiltinIntegerComputationDomain::FunctionSpecific,
582        output_class: BuiltinIntegerOutputClassRule::OptionDependent,
583        overflow: BuiltinIntegerOverflowRule::NotApplicable,
584        backend: BuiltinIntegerBackendRule::HostAndGpu,
585        overload: BuiltinIntegerOverloadKind::Multiple,
586        notes: "RunMat extends legacy nanmean by routing typed-integer forms through mean(...,\"omitnan\"); default/double output is double, native output preserves the input class, and modern mean-only options remain extension syntax.",
587    }];
588
589pub const NANSUM_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
590    [BuiltinIntegerCapabilityDescriptor {
591        form: "y = nansum(A, args...)",
592        inputs: &RUNMAT_INTEGER_INPUTS,
593        computation_domain: BuiltinIntegerComputationDomain::FunctionSpecific,
594        output_class: BuiltinIntegerOutputClassRule::OptionDependent,
595        overflow: BuiltinIntegerOverflowRule::FunctionSpecific,
596        backend: BuiltinIntegerBackendRule::HostAndGpu,
597        overload: BuiltinIntegerOverloadKind::Multiple,
598        notes: "RunMat extends legacy nansum by routing typed-integer forms through sum(...,\"omitnan\"); default/double output is double, native output preserves the input class with saturating accumulation, and modern sum-only options remain extension syntax.",
599    }];
600
601pub const NANMIN_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
602    [BuiltinIntegerCapabilityDescriptor {
603        form: "y = nanmin(A, args...) or nanmin(A, B)",
604        inputs: &RUNMAT_INTEGER_INPUTS,
605        computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
606        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
607        overflow: BuiltinIntegerOverflowRule::NotApplicable,
608        backend: BuiltinIntegerBackendRule::FunctionSpecific,
609        overload: BuiltinIntegerOverloadKind::Multiple,
610        notes: "RunMat extends legacy nanmin with exact typed-integer reduction and compatible pairwise forms; omit-NaN resident reductions use host fallback, while pairwise execution follows min's same-class-or-scalar-double rules.",
611    }];
612
613pub const NANMEDIAN_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
614    [BuiltinIntegerCapabilityDescriptor {
615        form: "y = nanmedian(A, args...)",
616        inputs: &RUNMAT_INTEGER_INPUTS,
617        computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
618        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
619        overflow: BuiltinIntegerOverflowRule::NotApplicable,
620        backend: BuiltinIntegerBackendRule::GatherFallback,
621        overload: BuiltinIntegerOverloadKind::Multiple,
622        notes: "RunMat extends legacy nanmedian by routing typed-integer forms through median(...,\"omitnan\"); exact host reduction preserves all eight classes and resident fallback output is re-uploaded.",
623    }];
624
625pub const NANSTD_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
626    BuiltinIntegerCapabilityDescriptor {
627        form: "y = nanstd(A, args...) with typed-integer A",
628        inputs: &REJECTED_INTEGER_DATA,
629        computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
630        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
631        overflow: BuiltinIntegerOverflowRule::NotApplicable,
632        backend: BuiltinIntegerBackendRule::HostAndGpu,
633        overload: BuiltinIntegerOverloadKind::Multiple,
634        notes: "Typed-integer data is unsupported in both compatibility modes and is rejected before provider dispatch.",
635    },
636    BuiltinIntegerCapabilityDescriptor {
637        form: "y = nanstd(A, flag_or_dimension)",
638        inputs: &RUNMAT_INTEGER_CONTROLS,
639        computation_domain: BuiltinIntegerComputationDomain::Structural,
640        output_class: BuiltinIntegerOutputClassRule::Double,
641        overflow: BuiltinIntegerOverflowRule::NotApplicable,
642        backend: BuiltinIntegerBackendRule::HostAndGpu,
643        overload: BuiltinIntegerOverloadKind::Multiple,
644        notes: "With floating data, RunMat accepts exact typed-integer normalization and dimension controls only when the nanstd typed-integer-control extension is enabled.",
645    },
646];
647
648pub const NANVAR_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
649    BuiltinIntegerCapabilityDescriptor {
650        form: "y = nanvar(A, args...) with typed-integer A",
651        inputs: &REJECTED_INTEGER_DATA,
652        computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
653        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
654        overflow: BuiltinIntegerOverflowRule::NotApplicable,
655        backend: BuiltinIntegerBackendRule::HostAndGpu,
656        overload: BuiltinIntegerOverloadKind::Multiple,
657        notes: "Typed-integer data is unsupported in both compatibility modes and is rejected before provider dispatch.",
658    },
659    BuiltinIntegerCapabilityDescriptor {
660        form: "y = nanvar(A, normalization_or_dimension)",
661        inputs: &RUNMAT_INTEGER_CONTROLS,
662        computation_domain: BuiltinIntegerComputationDomain::Structural,
663        output_class: BuiltinIntegerOutputClassRule::Double,
664        overflow: BuiltinIntegerOverflowRule::NotApplicable,
665        backend: BuiltinIntegerBackendRule::HostAndGpu,
666        overload: BuiltinIntegerOverloadKind::Multiple,
667        notes: "With floating data, RunMat accepts exact typed-integer normalization and dimension controls only when the nanvar typed-integer-control extension is enabled; non-scalar weighted variance remains separately unsupported.",
668    },
669];
670
671fn logical_type(_args: &[Type], _ctx: &ResolveContext) -> Type {
672    Type::Logical { shape: None }
673}
674
675fn any_type(_args: &[Type], _ctx: &ResolveContext) -> Type {
676    Type::Unknown
677}
678
679fn missing_error(
680    error: &'static BuiltinErrorDescriptor,
681    detail: impl Into<String>,
682) -> RuntimeError {
683    let mut builder = build_runtime_error(format!("{}: {}", error.message, detail.into()))
684        .with_builtin("missing");
685    if let Some(identifier) = error.identifier {
686        builder = builder.with_identifier(identifier);
687    }
688    builder.build()
689}
690
691fn invalid_argument(detail: impl Into<String>) -> RuntimeError {
692    missing_error(&MISSING_ERROR_INVALID_ARGUMENT, detail)
693}
694
695fn unsupported_type(detail: impl Into<String>) -> RuntimeError {
696    missing_error(&MISSING_ERROR_UNSUPPORTED_TYPE, detail)
697}
698
699fn internal_error(detail: impl Into<String>) -> RuntimeError {
700    missing_error(&MISSING_ERROR_INTERNAL, detail)
701}
702
703#[runtime_builtin(
704    name = "missing",
705    category = "missing",
706    summary = "Create MATLAB missing string scalars or arrays.",
707    keywords = "missing,string,missing values",
708    accel = "cpu",
709    type_resolver(any_type),
710    descriptor(crate::builtins::missing::MISSING_DESCRIPTOR),
711    extensions(crate::builtins::missing::MISSING_EXTENSIONS),
712    integer_capabilities(crate::builtins::missing::MISSING_INTEGER_CAPABILITIES),
713    builtin_path = "crate::builtins::missing"
714)]
715async fn missing_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
716    if !args.is_empty() {
717        crate::compatibility::ensure_builtin_extension_enabled(
718            &MISSING_SHAPED_ARRAY_EXTENSION,
719            "missing",
720        )?;
721    }
722    let packed = Value::OutputList(args);
723    let gathered = gather_if_needed_async(&packed)
724        .await
725        .map_err(|err| invalid_argument(format!("missing: failed to gather arguments: {err}")))?;
726    let args = match gathered {
727        Value::OutputList(values) => values,
728        _ => Vec::new(),
729    };
730    let shape = parse_size_args(&args)?;
731    missing_string_array(shape)
732}
733
734#[runtime_builtin(
735    name = "ismissing",
736    category = "missing",
737    summary = "Return a logical mask identifying missing values.",
738    keywords = "ismissing,missing,NaN,NaT,string,table",
739    accel = "cpu",
740    type_resolver(logical_type),
741    descriptor(crate::builtins::missing::ISMISSING_DESCRIPTOR),
742    extensions(crate::builtins::missing::ISMISSING_EXTENSIONS),
743    integer_capabilities(crate::builtins::missing::ISMISSING_INTEGER_CAPABILITIES),
744    builtin_path = "crate::builtins::missing"
745)]
746async fn ismissing_builtin(value: Value) -> BuiltinResult<Value> {
747    if let Value::GpuTensor(handle) = &value {
748        if runmat_accelerate_api::handle_is_explicit(handle) {
749            crate::compatibility::ensure_builtin_extension_enabled(
750                &ISMISSING_RESIDENT_INPUT_EXTENSION,
751                "ismissing",
752            )?;
753        }
754        if runmat_accelerate_api::handle_integer_type(handle).is_some() {
755            return ismissing_resident_integer(handle);
756        }
757    }
758    let value = gather_if_needed_async(&value)
759        .await
760        .map_err(|err| invalid_argument(format!("ismissing: failed to gather input: {err}")))?;
761    ismissing_value(&value)
762}
763
764fn ismissing_resident_integer(
765    handle: &runmat_accelerate_api::GpuTensorHandle,
766) -> BuiltinResult<Value> {
767    let integer = runmat_accelerate_api::handle_integer_type(handle)
768        .expect("resident integer predicate requires integer metadata");
769    let storage = runmat_accelerate_api::handle_storage(handle);
770    if gpu_helpers::exact_provider_for_handle(handle).is_none()
771        || storage != runmat_accelerate_api::GpuTensorStorage::Real
772        || runmat_accelerate_api::handle_precision(handle).is_some()
773        || runmat_accelerate_api::handle_is_logical(handle)
774        || !gpu_helpers::gpu_class_metadata_matches(handle, None, Some(integer), false)
775    {
776        return Err(internal_error(
777            "ismissing: resident integer metadata is contradictory",
778        ));
779    }
780    Ok(Value::LogicalArray(LogicalArray::zeros(
781        handle.shape.clone(),
782    )))
783}
784
785#[runtime_builtin(
786    name = "anymissing",
787    category = "missing",
788    summary = "Return true when an input contains at least one missing value.",
789    keywords = "anymissing,missing,NaN,string,table",
790    accel = "cpu",
791    type_resolver(logical_type),
792    descriptor(crate::builtins::missing::ANYMISSING_DESCRIPTOR),
793    integer_capabilities(crate::builtins::missing::ANYMISSING_INTEGER_CAPABILITIES),
794    builtin_path = "crate::builtins::missing"
795)]
796async fn anymissing_builtin(value: Value) -> BuiltinResult<Value> {
797    let value = gather_if_needed_async(&value)
798        .await
799        .map_err(|err| invalid_argument(format!("anymissing: failed to gather input: {err}")))?;
800    Ok(Value::Bool(any_missing(&value)?))
801}
802
803#[runtime_builtin(
804    name = "standardizeMissing",
805    category = "missing",
806    summary = "Replace user-specified missing indicators with canonical missing values.",
807    keywords = "standardizeMissing,missing,NaN,string,table",
808    accel = "cpu",
809    type_resolver(any_type),
810    descriptor(crate::builtins::missing::STANDARDIZE_MISSING_DESCRIPTOR),
811    extensions(crate::builtins::missing::STANDARDIZE_MISSING_EXTENSIONS),
812    integer_capabilities(crate::builtins::missing::STANDARDIZE_MISSING_INTEGER_CAPABILITIES),
813    builtin_path = "crate::builtins::missing"
814)]
815async fn standardize_missing_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
816    if crate::builtins::common::validation::value_has_native_integer_class(&value) {
817        crate::compatibility::ensure_builtin_extension_enabled(
818            &STANDARDIZE_MISSING_INTEGER_DATA_EXTENSION,
819            "standardizeMissing",
820        )?;
821    }
822    let indicator = rest
823        .first()
824        .ok_or_else(|| invalid_argument("standardizeMissing: missing indicators argument"))?;
825    if crate::builtins::common::validation::value_contains_explicit_gpu(indicator) {
826        crate::compatibility::ensure_builtin_extension_enabled(
827            &STANDARDIZE_MISSING_EXPLICIT_GPU_INDICATOR_EXTENSION,
828            "standardizeMissing",
829        )?;
830    }
831    let value = gather_if_needed_async(&value).await.map_err(|err| {
832        invalid_argument(format!("standardizeMissing: failed to gather input: {err}"))
833    })?;
834    let indicator = gather_if_needed_async(indicator).await.map_err(|err| {
835        invalid_argument(format!(
836            "standardizeMissing: failed to gather indicators: {err}"
837        ))
838    })?;
839    let indicators = indicator_set(&indicator)?;
840    standardize_missing_value(value, &indicators)
841}
842
843#[runtime_builtin(
844    name = "rmmissing",
845    category = "missing",
846    summary = "Remove missing elements, rows, or columns.",
847    keywords = "rmmissing,missing,NaN,string,table",
848    accel = "cpu",
849    type_resolver(any_type),
850    descriptor(crate::builtins::missing::RMMISSING_DESCRIPTOR),
851    extensions(crate::builtins::missing::RMMISSING_EXTENSIONS),
852    integer_capabilities(crate::builtins::missing::RMMISSING_INTEGER_CAPABILITIES),
853    builtin_path = "crate::builtins::missing"
854)]
855async fn rmmissing_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
856    if rest.iter().any(is_real_typed_integer_value) {
857        crate::compatibility::ensure_builtin_extension_enabled(
858            &RMMISSING_INTEGER_DIM_EXTENSION,
859            "rmmissing",
860        )?;
861    }
862    let source = match &value {
863        Value::GpuTensor(handle) => Some(handle.clone()),
864        _ => None,
865    };
866    let value = if let Some(handle) = source.as_ref() {
867        let owner = gpu_helpers::exact_provider_for_handle(handle)
868            .ok_or_else(|| invalid_argument("rmmissing: no provider owns the resident input"))?;
869        gpu_helpers::download_value_preserving_residency_async(owner, handle)
870            .await
871            .map_err(|err| invalid_argument(format!("rmmissing: failed to gather input: {err}")))?
872    } else {
873        value
874    };
875    let options = RemoveOptions::parse(&rest)?;
876    let (result, removed) = remove_missing_value(value, options)?;
877    let (result, removed) = if let Some(source) = source.as_ref() {
878        (
879            gpu_helpers::restore_class_preserving_value(source, result, "rmmissing")?,
880            gpu_helpers::restore_class_preserving_value(
881                source,
882                Value::LogicalArray(removed),
883                "rmmissing",
884            )?,
885        )
886    } else {
887        (result, Value::LogicalArray(removed))
888    };
889    match crate::output_count::current_output_count() {
890        Some(0) => Ok(Value::OutputList(Vec::new())),
891        Some(1) => Ok(Value::OutputList(vec![result])),
892        Some(n) => Ok(crate::output_count::output_list_with_padding(
893            n,
894            vec![result, removed],
895        )),
896        None => Ok(result),
897    }
898}
899
900#[runtime_builtin(
901    name = "fillmissing",
902    category = "missing",
903    summary = "Fill missing entries using constant, neighbor, or summary methods.",
904    keywords = "fillmissing,missing,NaN,string,table,previous,next,linear,constant",
905    accel = "cpu",
906    type_resolver(any_type),
907    descriptor(crate::builtins::missing::FILLMISSING_DESCRIPTOR),
908    extensions(crate::builtins::missing::FILLMISSING_EXTENSIONS),
909    integer_capabilities(crate::builtins::missing::FILLMISSING_INTEGER_CAPABILITIES),
910    builtin_path = "crate::builtins::missing"
911)]
912async fn fillmissing_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
913    if is_real_typed_integer_value(&value) {
914        crate::compatibility::ensure_builtin_extension_enabled(
915            &FILLMISSING_INTEGER_DATA_EXTENSION,
916            "fillmissing",
917        )?;
918    } else if fillmissing_aggregate_contains_integer(&value)? {
919        crate::compatibility::ensure_builtin_extension_enabled(
920            &FILLMISSING_AGGREGATE_INTEGER_DATA_EXTENSION,
921            "fillmissing",
922        )?;
923    }
924    let value = gather_if_needed_async(&value)
925        .await
926        .map_err(|err| invalid_argument(format!("fillmissing: failed to gather input: {err}")))?;
927    let options = FillOptions::parse(&rest)?;
928    let (result, mask) = fill_missing_value(value, &options)?;
929    match crate::output_count::current_output_count() {
930        Some(0) => Ok(Value::OutputList(Vec::new())),
931        Some(1) => Ok(Value::OutputList(vec![result])),
932        Some(n) => Ok(crate::output_count::output_list_with_padding(
933            n,
934            vec![result, Value::LogicalArray(mask)],
935        )),
936        None => Ok(result),
937    }
938}
939
940fn fillmissing_aggregate_contains_integer(value: &Value) -> BuiltinResult<bool> {
941    match value {
942        Value::Cell(cell) => {
943            for child in &cell.data {
944                if is_real_typed_integer_value(child)
945                    || fillmissing_aggregate_contains_integer(child)?
946                {
947                    return Ok(true);
948                }
949            }
950            Ok(false)
951        }
952        Value::Object(object) if is_tabular_object(object) => {
953            let variables = table_variables(object)?;
954            for child in variables.fields.values() {
955                if is_real_typed_integer_value(child)
956                    || fillmissing_aggregate_contains_integer(child)?
957                {
958                    return Ok(true);
959                }
960            }
961            Ok(false)
962        }
963        _ => Ok(false),
964    }
965}
966
967#[runtime_builtin(
968    name = "nanmean",
969    category = "missing",
970    summary = "Mean that ignores NaN values.",
971    keywords = "nanmean,mean,omitnan,missing",
972    accel = "reduction",
973    type_resolver(any_type),
974    descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
975    extensions(crate::builtins::missing::NANMEAN_EXTENSIONS),
976    integer_capabilities(crate::builtins::missing::NANMEAN_INTEGER_CAPABILITIES),
977    builtin_path = "crate::builtins::missing"
978)]
979async fn nanmean_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
980    ensure_nan_integer_extension(&NANMEAN_INTEGER_EXTENSION, "nanmean", &value, &rest)?;
981    mean::mean_builtin(value, rest_with_omitnan(rest)).await
982}
983
984#[runtime_builtin(
985    name = "nansum",
986    category = "missing",
987    summary = "Sum that ignores NaN values.",
988    keywords = "nansum,sum,omitnan,missing",
989    accel = "reduction",
990    type_resolver(any_type),
991    descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
992    extensions(crate::builtins::missing::NANSUM_EXTENSIONS),
993    integer_capabilities(crate::builtins::missing::NANSUM_INTEGER_CAPABILITIES),
994    builtin_path = "crate::builtins::missing"
995)]
996async fn nansum_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
997    ensure_nan_integer_extension(&NANSUM_INTEGER_EXTENSION, "nansum", &value, &rest)?;
998    sum::sum_builtin(value, rest_with_omitnan(rest)).await
999}
1000
1001#[runtime_builtin(
1002    name = "nanmin",
1003    category = "missing",
1004    summary = "Minimum that ignores NaN values.",
1005    keywords = "nanmin,min,omitnan,missing",
1006    accel = "reduction",
1007    type_resolver(any_type),
1008    descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
1009    extensions(crate::builtins::missing::NANMIN_EXTENSIONS),
1010    integer_capabilities(crate::builtins::missing::NANMIN_INTEGER_CAPABILITIES),
1011    builtin_path = "crate::builtins::missing"
1012)]
1013async fn nanmin_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1014    ensure_nan_integer_extension(&NANMIN_INTEGER_EXTENSION, "nanmin", &value, &rest)?;
1015    if let Some(first) = rest.first() {
1016        if is_numeric_data_like(first) {
1017            if rest.len() != 1 {
1018                return Err(invalid_argument(
1019                    "nanmin: pairwise form accepts exactly two numeric inputs",
1020                ));
1021            }
1022            return pairwise_nan_min(value, first.clone());
1023        }
1024    }
1025    min::min_builtin(value, nanmin_rest_with_omitnan(rest)).await
1026}
1027
1028#[runtime_builtin(
1029    name = "nanmedian",
1030    category = "missing",
1031    summary = "Median that ignores NaN values.",
1032    keywords = "nanmedian,median,omitnan,missing",
1033    accel = "reduction",
1034    type_resolver(any_type),
1035    descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
1036    extensions(crate::builtins::missing::NANMEDIAN_EXTENSIONS),
1037    integer_capabilities(crate::builtins::missing::NANMEDIAN_INTEGER_CAPABILITIES),
1038    builtin_path = "crate::builtins::missing"
1039)]
1040async fn nanmedian_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1041    ensure_nan_integer_extension(&NANMEDIAN_INTEGER_EXTENSION, "nanmedian", &value, &rest)?;
1042    median::median_builtin(value, rest_with_omitnan(rest)).await
1043}
1044
1045#[runtime_builtin(
1046    name = "nanstd",
1047    category = "missing",
1048    summary = "Standard deviation that ignores NaN values.",
1049    keywords = "nanstd,std,omitnan,missing",
1050    accel = "reduction",
1051    type_resolver(any_type),
1052    descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
1053    extensions(crate::builtins::missing::NANSTD_EXTENSIONS),
1054    integer_capabilities(crate::builtins::missing::NANSTD_INTEGER_CAPABILITIES),
1055    builtin_path = "crate::builtins::missing"
1056)]
1057async fn nanstd_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1058    ensure_nan_integer_control_extension(
1059        &NANSTD_INTEGER_CONTROL_EXTENSION,
1060        "nanstd",
1061        &value,
1062        &rest,
1063    )?;
1064    std_reduction::std_builtin(value, rest_with_omitnan(rest)).await
1065}
1066
1067#[runtime_builtin(
1068    name = "nanvar",
1069    category = "missing",
1070    summary = "Variance that ignores NaN values.",
1071    keywords = "nanvar,var,omitnan,missing",
1072    accel = "reduction",
1073    type_resolver(any_type),
1074    descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
1075    extensions(crate::builtins::missing::NANVAR_EXTENSIONS),
1076    integer_capabilities(crate::builtins::missing::NANVAR_INTEGER_CAPABILITIES),
1077    builtin_path = "crate::builtins::missing"
1078)]
1079async fn nanvar_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1080    ensure_nan_integer_control_extension(
1081        &NANVAR_INTEGER_CONTROL_EXTENSION,
1082        "nanvar",
1083        &value,
1084        &rest,
1085    )?;
1086    var::var_builtin(value, rest_with_omitnan(rest)).await
1087}
1088
1089#[runtime_builtin(
1090    name = "movmad",
1091    category = "missing",
1092    summary = "Moving median absolute deviation over vectors and matrix dimensions.",
1093    keywords = "movmad,moving,median,absolute,deviation,missing",
1094    accel = "cpu",
1095    type_resolver(any_type),
1096    descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
1097    extensions(crate::builtins::missing::MOVMAD_EXTENSIONS),
1098    integer_capabilities(crate::builtins::missing::MOVMAD_INTEGER_CAPABILITIES),
1099    builtin_path = "crate::builtins::missing"
1100)]
1101async fn movmad_builtin(value: Value, window: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1102    let window = scalar_usize(&window, "movmad window")?;
1103    let provider = match &value {
1104        Value::GpuTensor(handle) => runmat_accelerate_api::provider_for_handle(handle)
1105            .or_else(runmat_accelerate_api::provider),
1106        _ => None,
1107    };
1108    if provider.is_some() && window > 31 {
1109        crate::compatibility::ensure_builtin_extension_enabled(
1110            &MOVMAD_GPU_LARGE_WINDOW_EXTENSION,
1111            "movmad",
1112        )?;
1113    }
1114    let value = gather_if_needed_async(&value)
1115        .await
1116        .map_err(|err| invalid_argument(format!("movmad: failed to gather input: {err}")))?;
1117    let tensor = numeric_tensor(value, "movmad")?;
1118    let options = MovingOptions::parse(&rest)?;
1119    let result = moving_mad(tensor, window, options)?;
1120    match (provider, result) {
1121        (Some(provider), Value::Tensor(tensor)) => {
1122            let handle = crate::builtins::common::gpu_helpers::upload_tensor(provider, &tensor)
1123                .map_err(|err| internal_error(format!("movmad: failed to upload result: {err}")))?;
1124            Ok(Value::GpuTensor(handle))
1125        }
1126        (_, result) => Ok(result),
1127    }
1128}
1129
1130fn is_real_typed_integer_value(value: &Value) -> bool {
1131    matches!(value, Value::Int(_))
1132        || matches!(value, Value::Tensor(tensor) if tensor.integer_storage().is_some())
1133        || matches!(
1134            value,
1135            Value::GpuTensor(handle)
1136                if runmat_accelerate_api::handle_integer_type(handle).is_some()
1137        )
1138}
1139
1140fn ensure_nan_integer_extension(
1141    extension: &BuiltinExtensionDescriptor,
1142    builtin: &str,
1143    value: &Value,
1144    rest: &[Value],
1145) -> BuiltinResult<()> {
1146    if is_real_typed_integer_value(value) || rest.iter().any(is_real_typed_integer_value) {
1147        crate::compatibility::ensure_builtin_extension_enabled(extension, builtin)?;
1148    }
1149    Ok(())
1150}
1151
1152fn ensure_nan_integer_control_extension(
1153    extension: &BuiltinExtensionDescriptor,
1154    builtin: &str,
1155    value: &Value,
1156    rest: &[Value],
1157) -> BuiltinResult<()> {
1158    if !is_real_typed_integer_value(value) && rest.iter().any(is_real_typed_integer_value) {
1159        crate::compatibility::ensure_builtin_extension_enabled(extension, builtin)?;
1160    }
1161    Ok(())
1162}
1163
1164fn rest_with_omitnan(mut rest: Vec<Value>) -> Vec<Value> {
1165    let insert_at = rest
1166        .iter()
1167        .position(|arg| scalar_text(arg).is_some_and(|text| text.eq_ignore_ascii_case("like")))
1168        .unwrap_or(rest.len());
1169    rest.insert(insert_at, Value::from("omitnan"));
1170    rest
1171}
1172
1173fn nanmin_rest_with_omitnan(mut rest: Vec<Value>) -> Vec<Value> {
1174    if rest.is_empty() {
1175        rest.push(Value::Tensor(
1176            Tensor::new(Vec::<f64>::new(), vec![0, 0]).expect("empty placeholder shape"),
1177        ));
1178    }
1179    rest.push(Value::from("omitnan"));
1180    rest
1181}
1182
1183fn parse_size_args(args: &[Value]) -> BuiltinResult<Vec<usize>> {
1184    if args.is_empty() {
1185        return Ok(vec![1, 1]);
1186    }
1187    if args.len() == 1 {
1188        match &args[0] {
1189            Value::Tensor(tensor) => return tensor_shape_as_size(tensor),
1190            Value::Int(_) | Value::Num(_) => {
1191                let n = scalar_usize(&args[0], "missing size")?;
1192                return Ok(vec![n, n]);
1193            }
1194            Value::String(s) if s.eq_ignore_ascii_case("like") => {
1195                return Err(invalid_argument(
1196                    "missing: 'like' requires a prototype value",
1197                ));
1198            }
1199            _ => {}
1200        }
1201    }
1202    let mut out = Vec::with_capacity(args.len());
1203    let mut idx = 0;
1204    while idx < args.len() {
1205        if scalar_text(&args[idx])
1206            .map(|text| text.eq_ignore_ascii_case("like"))
1207            .unwrap_or(false)
1208        {
1209            idx += 2;
1210            continue;
1211        }
1212        out.push(scalar_usize(&args[idx], "missing size")?);
1213        idx += 1;
1214    }
1215    if out.is_empty() {
1216        Ok(vec![1, 1])
1217    } else {
1218        Ok(out)
1219    }
1220}
1221
1222fn tensor_shape_as_size(tensor: &Tensor) -> BuiltinResult<Vec<usize>> {
1223    if let Some(storage) = tensor.integer_storage() {
1224        return (0..storage.len())
1225            .map(|index| {
1226                let value = storage.value_at(index).ok_or_else(|| {
1227                    internal_error("missing: integer size vector storage length mismatch")
1228                })?;
1229                integer_size_to_usize(&value, "missing size")
1230            })
1231            .collect();
1232    }
1233    let values = tensor_utils::tensor_values_f64_cow(tensor);
1234    if values.is_empty() {
1235        return Ok(vec![0, 0]);
1236    }
1237    values
1238        .iter()
1239        .map(|value| {
1240            if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 {
1241                return Err(invalid_argument(
1242                    "missing: sizes must be nonnegative finite integers",
1243                ));
1244            }
1245            if *value > usize::MAX as f64 {
1246                return Err(invalid_argument("missing: size exceeds platform limits"));
1247            }
1248            if usize::BITS == 64 && *value == usize::MAX as f64 {
1249                return Err(invalid_argument("missing: size exceeds platform limits"));
1250            }
1251            Ok(*value as usize)
1252        })
1253        .collect()
1254}
1255
1256fn missing_string_array(shape: Vec<usize>) -> BuiltinResult<Value> {
1257    let count = element_count(&shape)?;
1258    let array =
1259        StringArray::new(vec![MISSING_TEXT.to_string(); count], shape).map_err(internal_error)?;
1260    Ok(Value::StringArray(array))
1261}
1262
1263fn element_count(shape: &[usize]) -> BuiltinResult<usize> {
1264    shape.iter().try_fold(1usize, |acc, dim| {
1265        acc.checked_mul(*dim)
1266            .ok_or_else(|| invalid_argument("array size is too large"))
1267    })
1268}
1269
1270fn ismissing_value(value: &Value) -> BuiltinResult<Value> {
1271    match value {
1272        Value::Num(n) => Ok(Value::Bool(n.is_nan())),
1273        Value::Complex(re, im) => Ok(Value::Bool(re.is_nan() || im.is_nan())),
1274        Value::Int(_) | Value::Bool(_) | Value::FunctionHandle(_) | Value::ClassRef(_) => {
1275            Ok(Value::Bool(false))
1276        }
1277        Value::String(s) => Ok(Value::Bool(is_missing_text(s))),
1278        Value::CharArray(array) => Ok(Value::LogicalArray(
1279            LogicalArray::new(
1280                char_rows(array)
1281                    .into_iter()
1282                    .map(|text| u8::from(text.trim().is_empty() || is_missing_text(&text)))
1283                    .collect(),
1284                vec![array.rows, 1],
1285            )
1286            .map_err(internal_error)?,
1287        )),
1288        Value::StringArray(array) => logical_from_iter(
1289            array.data.iter().map(|text| is_missing_text(text)),
1290            array.shape.clone(),
1291        ),
1292        Value::Tensor(tensor) if tensor.integer_storage().is_some() => logical_from_iter(
1293            vec![false; tensor_utils::tensor_element_len(tensor)],
1294            tensor.shape.clone(),
1295        ),
1296        Value::Tensor(tensor) => {
1297            let values = tensor_utils::tensor_values_f64_cow(tensor);
1298            logical_from_iter(
1299                values.iter().map(|value| value.is_nan()),
1300                tensor.shape.clone(),
1301            )
1302        }
1303        Value::ComplexTensor(tensor) => logical_from_iter(
1304            tensor
1305                .materialize_f64()
1306                .iter()
1307                .map(|(re, im)| re.is_nan() || im.is_nan()),
1308            tensor.shape.clone(),
1309        ),
1310        Value::SparseTensor(tensor) => {
1311            let mut data = vec![0u8; tensor.rows * tensor.cols];
1312            if tensor.integer_storage().is_none() {
1313                for col in 0..tensor.cols {
1314                    for idx in tensor.col_ptrs[col]..tensor.col_ptrs[col + 1] {
1315                        if tensor
1316                            .numeric_value_at(idx)
1317                            .expect("sparse storage index is valid")
1318                            .materialize_f64()
1319                            .is_nan()
1320                        {
1321                            data[tensor.row_indices[idx] + col * tensor.rows] = 1;
1322                        }
1323                    }
1324                }
1325            }
1326            Ok(Value::LogicalArray(
1327                LogicalArray::new(data, vec![tensor.rows, tensor.cols]).map_err(internal_error)?,
1328            ))
1329        }
1330        Value::LogicalArray(array) => Ok(Value::LogicalArray(LogicalArray::zeros(
1331            array.shape.clone(),
1332        ))),
1333        Value::Cell(cell) => {
1334            let mut data = Vec::with_capacity(cell.data.len());
1335            for item in &cell.data {
1336                data.push(u8::from(any_missing(item)?));
1337            }
1338            Ok(Value::LogicalArray(
1339                LogicalArray::new(data, vec![cell.rows, cell.cols]).map_err(internal_error)?,
1340            ))
1341        }
1342        Value::Struct(st) => {
1343            let mut out = StructValue::new();
1344            for (name, field) in &st.fields {
1345                out.insert(name.clone(), ismissing_value(field)?);
1346            }
1347            Ok(Value::Struct(out))
1348        }
1349        Value::Object(object) if is_tabular_object(object) => ismissing_table(object),
1350        Value::Object(object) if object.is_class("datetime") => {
1351            let serials = crate::builtins::datetime::serials_from_datetime_value(value)?;
1352            let values = tensor_utils::tensor_values_f64_cow(&serials);
1353            logical_from_iter(
1354                values.iter().map(|serial| serial.is_nan()),
1355                serials.shape.clone(),
1356            )
1357        }
1358        Value::Object(object) if object.is_class("duration") => {
1359            let days = crate::builtins::duration::duration_tensor_from_duration_value(value)?;
1360            let values = tensor_utils::tensor_values_f64_cow(&days);
1361            logical_from_iter(values.iter().map(|day| day.is_nan()), days.shape.clone())
1362        }
1363        Value::OutputList(values) => {
1364            let mut data = Vec::with_capacity(values.len());
1365            for item in values {
1366                data.push(u8::from(any_missing(item)?));
1367            }
1368            Ok(Value::LogicalArray(
1369                LogicalArray::new(data, vec![1, values.len()]).map_err(internal_error)?,
1370            ))
1371        }
1372        _ => Ok(Value::Bool(false)),
1373    }
1374}
1375
1376fn ismissing_table(object: &ObjectInstance) -> BuiltinResult<Value> {
1377    let height = table_height(object)?;
1378    let width = table_width(object)?;
1379    let names = table_variable_names_from_object(object)?;
1380    let variables = table_variables(object)?;
1381    let mut data = vec![0u8; height * width];
1382    for (col, name) in names.iter().enumerate() {
1383        let Some(value) = variables.fields.get(name) else {
1384            continue;
1385        };
1386        let mask = logical_mask_for_rows(value, height)?;
1387        for row in 0..height {
1388            if mask.get(row).copied().unwrap_or(0) != 0 {
1389                data[row + col * height] = 1;
1390            }
1391        }
1392    }
1393    Ok(Value::LogicalArray(
1394        LogicalArray::new(data, vec![height, width]).map_err(internal_error)?,
1395    ))
1396}
1397
1398fn logical_from_iter<I>(iter: I, shape: Vec<usize>) -> BuiltinResult<Value>
1399where
1400    I: IntoIterator<Item = bool>,
1401{
1402    Ok(Value::LogicalArray(
1403        LogicalArray::new(iter.into_iter().map(u8::from).collect(), shape)
1404            .map_err(internal_error)?,
1405    ))
1406}
1407
1408fn any_missing(value: &Value) -> BuiltinResult<bool> {
1409    match ismissing_value(value)? {
1410        Value::Bool(flag) => Ok(flag),
1411        Value::LogicalArray(array) => Ok(array.data.iter().any(|flag| *flag != 0)),
1412        Value::Struct(st) => {
1413            for field in st.fields.values() {
1414                if any_missing(field)? {
1415                    return Ok(true);
1416                }
1417            }
1418            Ok(false)
1419        }
1420        _ => Ok(false),
1421    }
1422}
1423
1424fn logical_mask_for_rows(value: &Value, expected_rows: usize) -> BuiltinResult<Vec<u8>> {
1425    let mask_value = ismissing_value(value)?;
1426    match mask_value {
1427        Value::Bool(flag) => Ok(vec![u8::from(flag); expected_rows]),
1428        Value::LogicalArray(mask) => logical_array_mask_for_rows(&mask, expected_rows),
1429        _ => Err(unsupported_type("cannot build row missing mask for value")),
1430    }
1431}
1432
1433fn logical_array_mask_for_rows(
1434    mask: &LogicalArray,
1435    expected_rows: usize,
1436) -> BuiltinResult<Vec<u8>> {
1437    let rows = mask.shape.first().copied().unwrap_or(mask.data.len());
1438    let cols = mask.shape.get(1).copied().unwrap_or(1);
1439    if rows == expected_rows {
1440        let mut out = vec![0u8; expected_rows];
1441        for col in 0..cols {
1442            for (row, slot) in out.iter_mut().enumerate().take(expected_rows) {
1443                let idx = row + col * rows;
1444                if mask.data.get(idx).copied().unwrap_or(0) != 0 {
1445                    *slot = 1;
1446                }
1447            }
1448        }
1449        Ok(out)
1450    } else if mask.data.len() == expected_rows {
1451        Ok(mask.data.clone())
1452    } else {
1453        Err(invalid_argument(
1454            "missing mask shape does not match table height",
1455        ))
1456    }
1457}
1458
1459#[derive(Clone, Copy)]
1460enum RemoveDim {
1461    Auto,
1462    Rows,
1463    Columns,
1464}
1465
1466#[derive(Clone, Copy)]
1467struct RemoveOptions {
1468    dim: RemoveDim,
1469}
1470
1471impl RemoveOptions {
1472    fn parse(args: &[Value]) -> BuiltinResult<Self> {
1473        let mut dim = RemoveDim::Auto;
1474        let mut idx = 0;
1475        while idx < args.len() {
1476            if let Some(text) = scalar_text(&args[idx]) {
1477                match text.to_ascii_lowercase().as_str() {
1478                    "dim" if idx + 1 < args.len() => {
1479                        dim = dim_from_value(&args[idx + 1])?;
1480                        idx += 2;
1481                        continue;
1482                    }
1483                    "dim" => return Err(invalid_argument("rmmissing: 'dim' requires a value")),
1484                    "rows" => {
1485                        dim = RemoveDim::Rows;
1486                        idx += 1;
1487                        continue;
1488                    }
1489                    "columns" | "cols" => {
1490                        dim = RemoveDim::Columns;
1491                        idx += 1;
1492                        continue;
1493                    }
1494                    other => {
1495                        return Err(invalid_argument(format!(
1496                            "rmmissing: unsupported option '{other}'"
1497                        )))
1498                    }
1499                }
1500            }
1501            if matches!(args[idx], Value::Num(_) | Value::Int(_)) {
1502                dim = dim_from_value(&args[idx])?;
1503                idx += 1;
1504                continue;
1505            }
1506            return Err(invalid_argument(format!(
1507                "rmmissing: unsupported option argument {:?}",
1508                args[idx]
1509            )));
1510        }
1511        Ok(Self { dim })
1512    }
1513}
1514
1515fn dim_from_value(value: &Value) -> BuiltinResult<RemoveDim> {
1516    match scalar_usize(value, "dimension")? {
1517        1 => Ok(RemoveDim::Rows),
1518        2 => Ok(RemoveDim::Columns),
1519        _ => Err(invalid_argument("dimension must be 1 or 2 for rmmissing")),
1520    }
1521}
1522
1523fn remove_missing_value(
1524    value: Value,
1525    options: RemoveOptions,
1526) -> BuiltinResult<(Value, LogicalArray)> {
1527    match value {
1528        Value::Object(object) if is_tabular_object(&object) => {
1529            remove_missing_table(object, options)
1530        }
1531        Value::Tensor(tensor) => remove_missing_tensor(tensor, options),
1532        Value::StringArray(array) => remove_missing_string_array(array, options),
1533        Value::LogicalArray(array) => remove_missing_logical_array(array, options),
1534        Value::Cell(cell) => remove_missing_cell(cell, options),
1535        other => {
1536            if any_missing(&other)? {
1537                Ok((
1538                    empty_like(other)?,
1539                    LogicalArray::new(vec![1], vec![1, 1]).map_err(internal_error)?,
1540                ))
1541            } else {
1542                Ok((
1543                    other,
1544                    LogicalArray::new(vec![0], vec![1, 1]).map_err(internal_error)?,
1545                ))
1546            }
1547        }
1548    }
1549}
1550
1551fn remove_missing_table(
1552    object: ObjectInstance,
1553    options: RemoveOptions,
1554) -> BuiltinResult<(Value, LogicalArray)> {
1555    let height = table_height(&object)?;
1556    let width = table_width(&object)?;
1557    let names = table_variable_names_from_object(&object)?;
1558    let variables = table_variables(&object)?;
1559    let remove_columns = matches!(options.dim, RemoveDim::Columns);
1560    if remove_columns {
1561        let mut keep_names = Vec::new();
1562        let mut removed = Vec::with_capacity(width);
1563        let mut keep_values = Vec::new();
1564        for name in &names {
1565            let value = variables
1566                .fields
1567                .get(name)
1568                .ok_or_else(|| internal_error(format!("table missing variable {name}")))?;
1569            let has_missing = any_missing(value)?;
1570            removed.push(u8::from(has_missing));
1571            if !has_missing {
1572                keep_names.push(name.clone());
1573                keep_values.push(value.clone());
1574            }
1575        }
1576        let row_names = selected_row_names(&object, &(0..height).collect::<Vec<_>>())?;
1577        Ok((
1578            table_from_columns_like(&object, keep_names, keep_values, row_names, None)?,
1579            LogicalArray::new(removed, vec![1, width]).map_err(internal_error)?,
1580        ))
1581    } else {
1582        let mut row_has_missing = vec![0u8; height];
1583        for name in &names {
1584            if let Some(value) = variables.fields.get(name) {
1585                let mask = logical_mask_for_rows(value, height)?;
1586                for row in 0..height {
1587                    if mask[row] != 0 {
1588                        row_has_missing[row] = 1;
1589                    }
1590                }
1591            }
1592        }
1593        let keep_rows: Vec<usize> = row_has_missing
1594            .iter()
1595            .enumerate()
1596            .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1597            .collect();
1598        let mut values = Vec::with_capacity(names.len());
1599        for name in &names {
1600            let value = variables
1601                .fields
1602                .get(name)
1603                .ok_or_else(|| internal_error(format!("table missing variable {name}")))?;
1604            values.push(select_rows(value, &keep_rows)?);
1605        }
1606        let row_names = selected_row_names(&object, &keep_rows)?;
1607        Ok((
1608            table_from_columns_like(&object, names, values, row_names, Some(&keep_rows))?,
1609            LogicalArray::new(row_has_missing, vec![height, 1]).map_err(internal_error)?,
1610        ))
1611    }
1612}
1613
1614fn remove_missing_tensor(
1615    tensor: Tensor,
1616    options: RemoveOptions,
1617) -> BuiltinResult<(Value, LogicalArray)> {
1618    if tensor.integer_storage().is_some() {
1619        let rows = tensor.rows();
1620        let cols = tensor.cols();
1621        let mask = if rows == 1 || cols == 1 {
1622            let len = tensor_utils::tensor_element_len(&tensor);
1623            LogicalArray::new(vec![0; len], vec![1, len])
1624        } else if matches!(options.dim, RemoveDim::Columns) {
1625            LogicalArray::new(vec![0; cols], vec![1, cols])
1626        } else {
1627            LogicalArray::new(vec![0; rows], vec![rows, 1])
1628        }
1629        .map_err(internal_error)?;
1630        return Ok((Value::Tensor(tensor), mask));
1631    }
1632    if tensor.rows() == 1 || tensor.cols() == 1 {
1633        let dtype = tensor.numeric_dtype();
1634        let source_rows = tensor.rows();
1635        let values = tensor_utils::tensor_into_values_f64(tensor);
1636        let mut data = Vec::new();
1637        let mut removed = Vec::with_capacity(values.len());
1638        let source_is_row = source_rows == 1;
1639        for value in values {
1640            if value.is_nan() {
1641                removed.push(1);
1642            } else {
1643                removed.push(0);
1644                data.push(value);
1645            }
1646        }
1647        let shape = if source_is_row {
1648            vec![1, data.len()]
1649        } else {
1650            vec![data.len(), 1]
1651        };
1652        let removed_len = removed.len();
1653        return Ok((
1654            Value::Tensor(Tensor::new_with_dtype(data, shape, dtype).map_err(internal_error)?),
1655            LogicalArray::new(removed, vec![1, removed_len]).map_err(internal_error)?,
1656        ));
1657    }
1658    let remove_columns = matches!(options.dim, RemoveDim::Columns);
1659    if remove_columns {
1660        remove_missing_columns_tensor(tensor)
1661    } else {
1662        remove_missing_rows_tensor(tensor)
1663    }
1664}
1665
1666fn remove_missing_rows_tensor(tensor: Tensor) -> BuiltinResult<(Value, LogicalArray)> {
1667    let rows = tensor.rows();
1668    let cols = tensor.cols();
1669    let mut removed = vec![0u8; rows];
1670    for col in 0..cols {
1671        for (row, slot) in removed.iter_mut().enumerate().take(rows) {
1672            if tensor.get2(row, col).map_err(internal_error)?.is_nan() {
1673                *slot = 1;
1674            }
1675        }
1676    }
1677    let keep: Vec<usize> = removed
1678        .iter()
1679        .enumerate()
1680        .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1681        .collect();
1682    let out = select_rows(&Value::Tensor(tensor), &keep)?;
1683    Ok((
1684        out,
1685        LogicalArray::new(removed, vec![rows, 1]).map_err(internal_error)?,
1686    ))
1687}
1688
1689fn remove_missing_columns_tensor(tensor: Tensor) -> BuiltinResult<(Value, LogicalArray)> {
1690    let rows = tensor.rows();
1691    let cols = tensor.cols();
1692    let mut removed = vec![0u8; cols];
1693    for (col, slot) in removed.iter_mut().enumerate().take(cols) {
1694        for row in 0..rows {
1695            if tensor.get2(row, col).map_err(internal_error)?.is_nan() {
1696                *slot = 1;
1697            }
1698        }
1699    }
1700    let keep_cols: Vec<usize> = removed
1701        .iter()
1702        .enumerate()
1703        .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1704        .collect();
1705    let mut data = Vec::with_capacity(rows * keep_cols.len());
1706    for col in keep_cols {
1707        for row in 0..rows {
1708            data.push(tensor.get2(row, col).map_err(internal_error)?);
1709        }
1710    }
1711    Ok((
1712        Value::Tensor(
1713            Tensor::new_with_dtype(
1714                data,
1715                vec![rows, removed.iter().filter(|f| **f == 0).count()],
1716                tensor.numeric_dtype(),
1717            )
1718            .map_err(internal_error)?,
1719        ),
1720        LogicalArray::new(removed, vec![1, cols]).map_err(internal_error)?,
1721    ))
1722}
1723
1724fn remove_missing_string_array(
1725    array: StringArray,
1726    options: RemoveOptions,
1727) -> BuiltinResult<(Value, LogicalArray)> {
1728    let rows = array.rows();
1729    let cols = array.cols();
1730    let shape = array.shape.clone();
1731    remove_missing_column_major(
1732        array.data,
1733        rows,
1734        cols,
1735        shape,
1736        options,
1737        |text| is_missing_text(text),
1738        |data, shape| {
1739            StringArray::new(data, shape)
1740                .map(Value::StringArray)
1741                .map_err(internal_error)
1742        },
1743    )
1744}
1745
1746fn remove_missing_logical_array(
1747    array: LogicalArray,
1748    options: RemoveOptions,
1749) -> BuiltinResult<(Value, LogicalArray)> {
1750    let rows = array.shape.first().copied().unwrap_or(array.data.len());
1751    let cols = array.shape.get(1).copied().unwrap_or(1);
1752    remove_missing_column_major(
1753        array.data,
1754        rows,
1755        cols,
1756        array.shape.clone(),
1757        options,
1758        |_| false,
1759        |data, shape| {
1760            LogicalArray::new(data, shape)
1761                .map(Value::LogicalArray)
1762                .map_err(internal_error)
1763        },
1764    )
1765}
1766
1767fn remove_missing_cell(
1768    cell: CellArray,
1769    options: RemoveOptions,
1770) -> BuiltinResult<(Value, LogicalArray)> {
1771    let rows = cell.rows;
1772    let cols = cell.cols;
1773    let is_missing = |row: usize, col: usize| -> bool {
1774        cell.get(row, col)
1775            .ok()
1776            .and_then(|value| any_missing(&value).ok())
1777            .unwrap_or(false)
1778    };
1779
1780    if rows == 1 || cols == 1 {
1781        let mut out = Vec::new();
1782        let mut removed = Vec::with_capacity(cell.data.len());
1783        for value in cell.data {
1784            if any_missing(&value).unwrap_or(false) {
1785                removed.push(1);
1786            } else {
1787                removed.push(0);
1788                out.push(value);
1789            }
1790        }
1791        let out_shape = if rows == 1 {
1792            vec![1, out.len()]
1793        } else {
1794            vec![out.len(), 1]
1795        };
1796        let removed_len = removed.len();
1797        let out_rows = out_shape.first().copied().unwrap_or(0);
1798        let out_cols = out_shape.get(1).copied().unwrap_or(0);
1799        return Ok((
1800            CellArray::new(out, out_rows, out_cols)
1801                .map(Value::Cell)
1802                .map_err(internal_error)?,
1803            LogicalArray::new(removed, vec![1, removed_len]).map_err(internal_error)?,
1804        ));
1805    }
1806
1807    if matches!(options.dim, RemoveDim::Columns) {
1808        let mut removed = vec![0u8; cols];
1809        for col in 0..cols {
1810            for row in 0..rows {
1811                if is_missing(row, col) {
1812                    removed[col] = 1;
1813                }
1814            }
1815        }
1816        let kept_cols: Vec<usize> = removed
1817            .iter()
1818            .enumerate()
1819            .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1820            .collect();
1821        let mut out = Vec::with_capacity(rows * kept_cols.len());
1822        for row in 0..rows {
1823            for col in &kept_cols {
1824                out.push(cell.get(row, *col).map_err(internal_error)?);
1825            }
1826        }
1827        let kept = kept_cols.len();
1828        Ok((
1829            CellArray::new(out, rows, kept)
1830                .map(Value::Cell)
1831                .map_err(internal_error)?,
1832            LogicalArray::new(removed, vec![1, cols]).map_err(internal_error)?,
1833        ))
1834    } else {
1835        let mut removed = vec![0u8; rows];
1836        for row in 0..rows {
1837            for col in 0..cols {
1838                if is_missing(row, col) {
1839                    removed[row] = 1;
1840                }
1841            }
1842        }
1843        let kept_rows: Vec<usize> = removed
1844            .iter()
1845            .enumerate()
1846            .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1847            .collect();
1848        let mut out = Vec::with_capacity(kept_rows.len() * cols);
1849        for row in &kept_rows {
1850            for col in 0..cols {
1851                out.push(cell.get(*row, col).map_err(internal_error)?);
1852            }
1853        }
1854        Ok((
1855            CellArray::new(out, kept_rows.len(), cols)
1856                .map(Value::Cell)
1857                .map_err(internal_error)?,
1858            LogicalArray::new(removed, vec![rows, 1]).map_err(internal_error)?,
1859        ))
1860    }
1861}
1862
1863fn remove_missing_column_major<T: Clone>(
1864    data: Vec<T>,
1865    rows: usize,
1866    cols: usize,
1867    _shape: Vec<usize>,
1868    options: RemoveOptions,
1869    is_missing: impl Fn(&T) -> bool,
1870    build: impl Fn(Vec<T>, Vec<usize>) -> BuiltinResult<Value>,
1871) -> BuiltinResult<(Value, LogicalArray)> {
1872    if rows == 1 || cols == 1 {
1873        let mut out = Vec::new();
1874        let mut removed = Vec::with_capacity(data.len());
1875        for value in data {
1876            if is_missing(&value) {
1877                removed.push(1);
1878            } else {
1879                removed.push(0);
1880                out.push(value);
1881            }
1882        }
1883        let out_shape = if rows == 1 {
1884            vec![1, out.len()]
1885        } else {
1886            vec![out.len(), 1]
1887        };
1888        let removed_len = removed.len();
1889        return Ok((
1890            build(out, out_shape)?,
1891            LogicalArray::new(removed, vec![1, removed_len]).map_err(internal_error)?,
1892        ));
1893    }
1894    if matches!(options.dim, RemoveDim::Columns) {
1895        let mut removed = vec![0u8; cols];
1896        for col in 0..cols {
1897            for row in 0..rows {
1898                if is_missing(&data[row + col * rows]) {
1899                    removed[col] = 1;
1900                }
1901            }
1902        }
1903        let kept_cols: Vec<usize> = removed
1904            .iter()
1905            .enumerate()
1906            .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1907            .collect();
1908        let mut out = Vec::with_capacity(rows * kept_cols.len());
1909        for col in kept_cols {
1910            for row in 0..rows {
1911                out.push(data[row + col * rows].clone());
1912            }
1913        }
1914        let kept = removed.iter().filter(|flag| **flag == 0).count();
1915        Ok((
1916            build(out, vec![rows, kept])?,
1917            LogicalArray::new(removed, vec![1, cols]).map_err(internal_error)?,
1918        ))
1919    } else {
1920        let mut removed = vec![0u8; rows];
1921        for col in 0..cols {
1922            for row in 0..rows {
1923                if is_missing(&data[row + col * rows]) {
1924                    removed[row] = 1;
1925                }
1926            }
1927        }
1928        let kept_rows: Vec<usize> = removed
1929            .iter()
1930            .enumerate()
1931            .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1932            .collect();
1933        let mut out = Vec::with_capacity(kept_rows.len() * cols);
1934        for col in 0..cols {
1935            for row in &kept_rows {
1936                out.push(data[*row + col * rows].clone());
1937            }
1938        }
1939        Ok((
1940            build(out, vec![kept_rows.len(), cols])?,
1941            LogicalArray::new(removed, vec![rows, 1]).map_err(internal_error)?,
1942        ))
1943    }
1944}
1945
1946fn empty_like(value: Value) -> BuiltinResult<Value> {
1947    match value {
1948        Value::Tensor(tensor) => {
1949            Tensor::new_with_dtype(Vec::new(), vec![0, 0], tensor.numeric_dtype())
1950                .map(Value::Tensor)
1951                .map_err(internal_error)
1952        }
1953        Value::StringArray(_) | Value::String(_) => StringArray::new(Vec::new(), vec![0, 0])
1954            .map(Value::StringArray)
1955            .map_err(internal_error),
1956        Value::Cell(_) => CellArray::new(Vec::new(), 0, 0)
1957            .map(Value::Cell)
1958            .map_err(internal_error),
1959        _ => Ok(Value::OutputList(Vec::new())),
1960    }
1961}
1962
1963#[derive(Clone)]
1964enum FillMethod {
1965    Constant(Value),
1966    Previous,
1967    Next,
1968    Nearest,
1969    Linear,
1970    Mean,
1971    Median,
1972}
1973
1974#[derive(Clone)]
1975struct FillOptions {
1976    method: FillMethod,
1977    dim: Option<usize>,
1978}
1979
1980impl FillOptions {
1981    fn parse(args: &[Value]) -> BuiltinResult<Self> {
1982        if args.is_empty() {
1983            return Err(invalid_argument("fillmissing: method is required"));
1984        }
1985        let mut idx = 0;
1986        let method_text = scalar_text(&args[idx])
1987            .ok_or_else(|| invalid_argument("fillmissing: method must be a string"))?
1988            .to_ascii_lowercase();
1989        idx += 1;
1990        let method = match method_text.as_str() {
1991            "constant" => {
1992                let fill = args
1993                    .get(idx)
1994                    .ok_or_else(|| {
1995                        invalid_argument("fillmissing: constant method needs a fill value")
1996                    })?
1997                    .clone();
1998                idx += 1;
1999                FillMethod::Constant(fill)
2000            }
2001            "previous" => FillMethod::Previous,
2002            "next" => FillMethod::Next,
2003            "nearest" => FillMethod::Nearest,
2004            "linear" => FillMethod::Linear,
2005            "mean" => FillMethod::Mean,
2006            "median" => FillMethod::Median,
2007            other => {
2008                return Err(invalid_argument(format!(
2009                    "fillmissing: unsupported method '{other}'"
2010                )))
2011            }
2012        };
2013        let mut dim = None;
2014        while idx < args.len() {
2015            if let Some(text) = scalar_text(&args[idx]) {
2016                if text.eq_ignore_ascii_case("dim") && idx + 1 < args.len() {
2017                    dim = Some(scalar_usize(&args[idx + 1], "fillmissing dimension")?);
2018                    idx += 2;
2019                    continue;
2020                }
2021                if text.eq_ignore_ascii_case("dim") {
2022                    return Err(invalid_argument("fillmissing: 'dim' requires a value"));
2023                }
2024                return Err(invalid_argument(format!(
2025                    "fillmissing: unsupported option '{text}'"
2026                )));
2027            }
2028            if matches!(args[idx], Value::Num(_) | Value::Int(_)) {
2029                dim = Some(scalar_usize(&args[idx], "fillmissing dimension")?);
2030                idx += 1;
2031                continue;
2032            }
2033            return Err(invalid_argument(format!(
2034                "fillmissing: unsupported option argument {:?}",
2035                args[idx]
2036            )));
2037        }
2038        if dim.is_some_and(|dim| dim != 1 && dim != 2) {
2039            return Err(invalid_argument("fillmissing: dimension must be 1 or 2"));
2040        }
2041        Ok(Self { method, dim })
2042    }
2043}
2044
2045fn fill_missing_value(value: Value, options: &FillOptions) -> BuiltinResult<(Value, LogicalArray)> {
2046    match value {
2047        Value::Tensor(tensor) => fill_missing_tensor(tensor, options),
2048        Value::StringArray(array) => fill_missing_string_array(array, options),
2049        Value::Object(object) if is_tabular_object(&object) => fill_missing_table(object, options),
2050        Value::Cell(cell) => fill_missing_cell(cell, options),
2051        Value::ComplexTensor(_) => Err(unsupported_type(
2052            "fillmissing: complex arrays are not supported yet",
2053        )),
2054        Value::SparseTensor(_) => Err(unsupported_type(
2055            "fillmissing: sparse arrays are not supported yet",
2056        )),
2057        other => {
2058            let missing = any_missing(&other)?;
2059            let mask =
2060                LogicalArray::new(vec![u8::from(missing)], vec![1, 1]).map_err(internal_error)?;
2061            if missing {
2062                match &options.method {
2063                    FillMethod::Constant(fill) => Ok((fill.clone(), mask)),
2064                    _ => Err(unsupported_type(
2065                        "fillmissing: scalar fill requires the constant method",
2066                    )),
2067                }
2068            } else {
2069                Ok((other, mask))
2070            }
2071        }
2072    }
2073}
2074
2075fn fill_missing_table(
2076    mut object: ObjectInstance,
2077    options: &FillOptions,
2078) -> BuiltinResult<(Value, LogicalArray)> {
2079    let height = table_height(&object)?;
2080    let width = table_width(&object)?;
2081    let names = table_variable_names_from_object(&object)?;
2082    let variables = table_variables(&object)?;
2083    let mut output_vars = StructValue::new();
2084    let mut mask_data = vec![0u8; height * width];
2085    for (col, name) in names.iter().enumerate() {
2086        let value = variables
2087            .fields
2088            .get(name)
2089            .ok_or_else(|| internal_error(format!("table missing variable {name}")))?
2090            .clone();
2091        let (filled, mask) = fill_missing_value(value, options)?;
2092        let row_mask = logical_array_mask_for_rows(&mask, height)?;
2093        for row in 0..height {
2094            if row_mask.get(row).copied().unwrap_or(0) != 0 {
2095                mask_data[row + col * height] = 1;
2096            }
2097        }
2098        output_vars.insert(name.clone(), filled);
2099    }
2100    object
2101        .properties
2102        .insert("__table_variables".to_string(), Value::Struct(output_vars));
2103    Ok((
2104        Value::Object(object),
2105        LogicalArray::new(mask_data, vec![height, width]).map_err(internal_error)?,
2106    ))
2107}
2108
2109fn fill_missing_tensor(
2110    tensor: Tensor,
2111    options: &FillOptions,
2112) -> BuiltinResult<(Value, LogicalArray)> {
2113    let rows = tensor.rows();
2114    let cols = tensor.cols();
2115    if tensor.integer_storage().is_some() {
2116        return Ok((
2117            Value::Tensor(tensor),
2118            LogicalArray::new(vec![0; rows * cols], vec![rows, cols]).map_err(internal_error)?,
2119        ));
2120    }
2121    let dim = options
2122        .dim
2123        .unwrap_or_else(|| first_nonsingleton_dim(rows, cols));
2124    validate_matrix_dim(dim, "fillmissing")?;
2125    let dtype = tensor.numeric_dtype();
2126    let shape = tensor.shape.clone();
2127    let mut data = tensor_utils::tensor_into_values_f64(tensor);
2128    let mut mask: Vec<u8> = data.iter().map(|value| u8::from(value.is_nan())).collect();
2129    match &options.method {
2130        FillMethod::Constant(fill) => {
2131            let fill = numeric_scalar(fill, "fillmissing constant")?;
2132            for (idx, value) in data.iter_mut().enumerate() {
2133                if mask[idx] != 0 {
2134                    *value = fill;
2135                }
2136            }
2137        }
2138        FillMethod::Mean => fill_summary_numeric(&mut data, rows, cols, dim, Summary::Mean),
2139        FillMethod::Median => fill_summary_numeric(&mut data, rows, cols, dim, Summary::Median),
2140        FillMethod::Previous => {
2141            fill_neighbor_numeric(&mut data, rows, cols, dim, Neighbor::Previous)
2142        }
2143        FillMethod::Next => fill_neighbor_numeric(&mut data, rows, cols, dim, Neighbor::Next),
2144        FillMethod::Nearest => fill_nearest_numeric(&mut data, rows, cols, dim),
2145        FillMethod::Linear => fill_linear_numeric(&mut data, rows, cols, dim),
2146    }
2147    for (flag, value) in mask.iter_mut().zip(&data) {
2148        if value.is_nan() {
2149            *flag = 0;
2150        }
2151    }
2152    Ok((
2153        Value::Tensor(Tensor::new_with_dtype(data, shape, dtype).map_err(internal_error)?),
2154        LogicalArray::new(mask, vec![rows, cols]).map_err(internal_error)?,
2155    ))
2156}
2157
2158fn fill_missing_string_array(
2159    array: StringArray,
2160    options: &FillOptions,
2161) -> BuiltinResult<(Value, LogicalArray)> {
2162    let rows = array.rows();
2163    let cols = array.cols();
2164    let dim = options
2165        .dim
2166        .unwrap_or_else(|| first_nonsingleton_dim(rows, cols));
2167    validate_matrix_dim(dim, "fillmissing")?;
2168    let mut data = array.data.clone();
2169    let mut mask: Vec<u8> = data
2170        .iter()
2171        .map(|text| u8::from(is_missing_text(text)))
2172        .collect();
2173    match &options.method {
2174        FillMethod::Constant(fill) => {
2175            let fill = scalar_text(fill)
2176                .ok_or_else(|| invalid_argument("fillmissing: string constant must be text"))?;
2177            for (idx, text) in data.iter_mut().enumerate() {
2178                if mask[idx] != 0 {
2179                    *text = fill.clone();
2180                }
2181            }
2182        }
2183        FillMethod::Previous => fill_neighbor_text(&mut data, rows, cols, dim, Neighbor::Previous),
2184        FillMethod::Next => fill_neighbor_text(&mut data, rows, cols, dim, Neighbor::Next),
2185        FillMethod::Nearest => fill_nearest_text(&mut data, rows, cols, dim),
2186        _ => {
2187            return Err(unsupported_type(
2188                "fillmissing: string arrays support constant, previous, next, and nearest",
2189            ))
2190        }
2191    }
2192    for (flag, text) in mask.iter_mut().zip(&data) {
2193        if is_missing_text(text) {
2194            *flag = 0;
2195        }
2196    }
2197    Ok((
2198        Value::StringArray(StringArray::new(data, array.shape).map_err(internal_error)?),
2199        LogicalArray::new(mask, vec![rows, cols]).map_err(internal_error)?,
2200    ))
2201}
2202
2203fn fill_missing_cell(
2204    cell: CellArray,
2205    options: &FillOptions,
2206) -> BuiltinResult<(Value, LogicalArray)> {
2207    let mut data = cell.data.clone();
2208    let mut mask = Vec::with_capacity(data.len());
2209    for item in &data {
2210        mask.push(u8::from(any_missing(item)?));
2211    }
2212    match &options.method {
2213        FillMethod::Constant(fill) => {
2214            for (idx, item) in data.iter_mut().enumerate() {
2215                if mask[idx] != 0 {
2216                    *item = fill.clone();
2217                }
2218            }
2219        }
2220        _ => {
2221            return Err(unsupported_type(
2222                "fillmissing: cell arrays currently support the constant method",
2223            ))
2224        }
2225    }
2226    for (flag, item) in mask.iter_mut().zip(&data) {
2227        if any_missing(item)? {
2228            *flag = 0;
2229        }
2230    }
2231    Ok((
2232        Value::Cell(CellArray::new(data, cell.rows, cell.cols).map_err(internal_error)?),
2233        LogicalArray::new(mask, vec![cell.rows, cell.cols]).map_err(internal_error)?,
2234    ))
2235}
2236
2237enum Summary {
2238    Mean,
2239    Median,
2240}
2241
2242fn fill_summary_numeric(data: &mut [f64], rows: usize, cols: usize, dim: usize, summary: Summary) {
2243    if dim == 1 {
2244        for col in 0..cols {
2245            let vals = finite_slice(data, rows, col, true);
2246            let replacement = summary_value(vals, &summary);
2247            for row in 0..rows {
2248                let idx = row + col * rows;
2249                if data[idx].is_nan() {
2250                    data[idx] = replacement;
2251                }
2252            }
2253        }
2254    } else {
2255        for row in 0..rows {
2256            let vals = finite_slice(data, rows, row, false);
2257            let replacement = summary_value(vals, &summary);
2258            for col in 0..cols {
2259                let idx = row + col * rows;
2260                if data[idx].is_nan() {
2261                    data[idx] = replacement;
2262                }
2263            }
2264        }
2265    }
2266}
2267
2268fn finite_slice(data: &[f64], rows: usize, fixed: usize, along_rows: bool) -> Vec<f64> {
2269    let mut out = Vec::new();
2270    if along_rows {
2271        let cols = data.len() / rows;
2272        for row in 0..rows {
2273            let value = data[row + fixed * rows];
2274            if !value.is_nan() {
2275                out.push(value);
2276            }
2277        }
2278        debug_assert!(fixed < cols);
2279    } else {
2280        let cols = data.len() / rows;
2281        for col in 0..cols {
2282            let value = data[fixed + col * rows];
2283            if !value.is_nan() {
2284                out.push(value);
2285            }
2286        }
2287    }
2288    out
2289}
2290
2291fn summary_value(mut vals: Vec<f64>, summary: &Summary) -> f64 {
2292    if vals.is_empty() {
2293        return f64::NAN;
2294    }
2295    match summary {
2296        Summary::Mean => vals.iter().sum::<f64>() / vals.len() as f64,
2297        Summary::Median => {
2298            vals.sort_by(|a, b| a.total_cmp(b));
2299            let mid = vals.len() / 2;
2300            if vals.len().is_multiple_of(2) {
2301                (vals[mid - 1] + vals[mid]) / 2.0
2302            } else {
2303                vals[mid]
2304            }
2305        }
2306    }
2307}
2308
2309#[derive(Clone, Copy)]
2310enum Neighbor {
2311    Previous,
2312    Next,
2313}
2314
2315fn fill_neighbor_numeric(data: &mut [f64], rows: usize, cols: usize, dim: usize, dir: Neighbor) {
2316    if dim == 1 {
2317        for col in 0..cols {
2318            fill_line_numeric(data, rows, col, rows, 1, dir);
2319        }
2320    } else {
2321        for row in 0..rows {
2322            fill_line_numeric(data, rows, row, cols, rows, dir);
2323        }
2324    }
2325}
2326
2327fn fill_line_numeric(
2328    data: &mut [f64],
2329    _rows: usize,
2330    start: usize,
2331    len: usize,
2332    step: usize,
2333    dir: Neighbor,
2334) {
2335    match dir {
2336        Neighbor::Previous => {
2337            let mut last = None;
2338            for i in 0..len {
2339                let idx = start + i * step;
2340                if data[idx].is_nan() {
2341                    if let Some(value) = last {
2342                        data[idx] = value;
2343                    }
2344                } else {
2345                    last = Some(data[idx]);
2346                }
2347            }
2348        }
2349        Neighbor::Next => {
2350            let mut next = None;
2351            for i in (0..len).rev() {
2352                let idx = start + i * step;
2353                if data[idx].is_nan() {
2354                    if let Some(value) = next {
2355                        data[idx] = value;
2356                    }
2357                } else {
2358                    next = Some(data[idx]);
2359                }
2360            }
2361        }
2362    }
2363}
2364
2365fn fill_nearest_numeric(data: &mut [f64], rows: usize, cols: usize, dim: usize) {
2366    let original = data.to_vec();
2367    if dim == 1 {
2368        for col in 0..cols {
2369            fill_nearest_line_numeric(&original, data, col * rows, rows, 1);
2370        }
2371    } else {
2372        for row in 0..rows {
2373            fill_nearest_line_numeric(&original, data, row, cols, rows);
2374        }
2375    }
2376}
2377
2378fn fill_nearest_line_numeric(
2379    original: &[f64],
2380    data: &mut [f64],
2381    start: usize,
2382    len: usize,
2383    step: usize,
2384) {
2385    for i in 0..len {
2386        let idx = start + i * step;
2387        if !original[idx].is_nan() {
2388            continue;
2389        }
2390        let prev = (0..i).rev().find_map(|j| {
2391            let value = original[start + j * step];
2392            (!value.is_nan()).then_some((i - j, value))
2393        });
2394        let next = ((i + 1)..len).find_map(|j| {
2395            let value = original[start + j * step];
2396            (!value.is_nan()).then_some((j - i, value))
2397        });
2398        data[idx] = match (prev, next) {
2399            (Some((pd, _)), Some((nd, nv))) if nd < pd => nv,
2400            (Some((_, pv)), _) => pv,
2401            (_, Some((_, nv))) => nv,
2402            _ => f64::NAN,
2403        };
2404    }
2405}
2406
2407fn fill_linear_numeric(data: &mut [f64], rows: usize, cols: usize, dim: usize) {
2408    if dim == 1 {
2409        for col in 0..cols {
2410            fill_linear_line(data, col * rows, rows, 1);
2411        }
2412    } else {
2413        for row in 0..rows {
2414            fill_linear_line(data, row, cols, rows);
2415        }
2416    }
2417}
2418
2419fn fill_linear_line(data: &mut [f64], start: usize, len: usize, step: usize) {
2420    let mut i = 0;
2421    while i < len {
2422        let idx = start + i * step;
2423        if !data[idx].is_nan() {
2424            i += 1;
2425            continue;
2426        }
2427        let run_start = i;
2428        while i < len && data[start + i * step].is_nan() {
2429            i += 1;
2430        }
2431        let run_end = i;
2432        let prev = (run_start > 0).then(|| data[start + (run_start - 1) * step]);
2433        let next = (run_end < len).then(|| data[start + run_end * step]);
2434        match (prev, next) {
2435            (Some(a), Some(b)) if !a.is_nan() && !b.is_nan() => {
2436                let span = (run_end - run_start + 1) as f64;
2437                for (offset, pos) in (run_start..run_end).enumerate() {
2438                    data[start + pos * step] = a + (b - a) * ((offset + 1) as f64 / span);
2439                }
2440            }
2441            (Some(a), _) if !a.is_nan() => {
2442                for pos in run_start..run_end {
2443                    data[start + pos * step] = a;
2444                }
2445            }
2446            (_, Some(b)) if !b.is_nan() => {
2447                for pos in run_start..run_end {
2448                    data[start + pos * step] = b;
2449                }
2450            }
2451            _ => {}
2452        }
2453    }
2454}
2455
2456fn fill_neighbor_text(data: &mut [String], rows: usize, cols: usize, dim: usize, dir: Neighbor) {
2457    if dim == 1 {
2458        for col in 0..cols {
2459            fill_line_text(data, col * rows, rows, 1, dir);
2460        }
2461    } else {
2462        for row in 0..rows {
2463            fill_line_text(data, row, cols, rows, dir);
2464        }
2465    }
2466}
2467
2468fn fill_line_text(data: &mut [String], start: usize, len: usize, step: usize, dir: Neighbor) {
2469    match dir {
2470        Neighbor::Previous => {
2471            let mut last: Option<String> = None;
2472            for i in 0..len {
2473                let idx = start + i * step;
2474                if is_missing_text(&data[idx]) {
2475                    if let Some(value) = &last {
2476                        data[idx] = value.clone();
2477                    }
2478                } else {
2479                    last = Some(data[idx].clone());
2480                }
2481            }
2482        }
2483        Neighbor::Next => {
2484            let mut next: Option<String> = None;
2485            for i in (0..len).rev() {
2486                let idx = start + i * step;
2487                if is_missing_text(&data[idx]) {
2488                    if let Some(value) = &next {
2489                        data[idx] = value.clone();
2490                    }
2491                } else {
2492                    next = Some(data[idx].clone());
2493                }
2494            }
2495        }
2496    }
2497}
2498
2499fn fill_nearest_text(data: &mut [String], rows: usize, cols: usize, dim: usize) {
2500    let original = data.to_vec();
2501    if dim == 1 {
2502        for col in 0..cols {
2503            fill_nearest_line_text(&original, data, col * rows, rows, 1);
2504        }
2505    } else {
2506        for row in 0..rows {
2507            fill_nearest_line_text(&original, data, row, cols, rows);
2508        }
2509    }
2510}
2511
2512fn fill_nearest_line_text(
2513    original: &[String],
2514    data: &mut [String],
2515    start: usize,
2516    len: usize,
2517    step: usize,
2518) {
2519    for i in 0..len {
2520        let idx = start + i * step;
2521        if !is_missing_text(&original[idx]) {
2522            continue;
2523        }
2524        let prev = (0..i).rev().find_map(|j| {
2525            let value = &original[start + j * step];
2526            (!is_missing_text(value)).then_some((i - j, value.clone()))
2527        });
2528        let next = ((i + 1)..len).find_map(|j| {
2529            let value = &original[start + j * step];
2530            (!is_missing_text(value)).then_some((j - i, value.clone()))
2531        });
2532        data[idx] = match (prev, next) {
2533            (Some((pd, _)), Some((nd, nv))) if nd < pd => nv,
2534            (Some((_, pv)), _) => pv,
2535            (_, Some((_, nv))) => nv,
2536            _ => MISSING_TEXT.to_string(),
2537        };
2538    }
2539}
2540
2541#[derive(Clone, Copy)]
2542struct MovingOptions {
2543    dim: Option<usize>,
2544    omit_nan: bool,
2545}
2546
2547impl MovingOptions {
2548    fn parse(args: &[Value]) -> BuiltinResult<Self> {
2549        let mut dim = None;
2550        let mut omit_nan = false;
2551        let mut idx = 0;
2552        while idx < args.len() {
2553            if let Some(text) = scalar_text(&args[idx]) {
2554                match text.to_ascii_lowercase().as_str() {
2555                    "omitnan" | "omitmissing" => omit_nan = true,
2556                    "includenan" | "includemissing" => omit_nan = false,
2557                    "dim" if idx + 1 < args.len() => {
2558                        dim = Some(scalar_usize(&args[idx + 1], "movmad dimension")?);
2559                        idx += 1;
2560                    }
2561                    "dim" => return Err(invalid_argument("movmad: 'dim' requires a value")),
2562                    other => {
2563                        return Err(invalid_argument(format!(
2564                            "movmad: unsupported option '{other}'"
2565                        )))
2566                    }
2567                }
2568            } else if matches!(args[idx], Value::Num(_) | Value::Int(_)) {
2569                dim = Some(scalar_usize(&args[idx], "movmad dimension")?);
2570            } else {
2571                return Err(invalid_argument(format!(
2572                    "movmad: unsupported option argument {:?}",
2573                    args[idx]
2574                )));
2575            }
2576            idx += 1;
2577        }
2578        if dim.is_some_and(|dim| dim != 1 && dim != 2) {
2579            return Err(invalid_argument("movmad: dimension must be 1 or 2"));
2580        }
2581        Ok(Self { dim, omit_nan })
2582    }
2583}
2584
2585fn moving_mad(tensor: Tensor, window: usize, options: MovingOptions) -> BuiltinResult<Value> {
2586    if window == 0 {
2587        return Err(invalid_argument("movmad: window length must be positive"));
2588    }
2589    let rows = tensor.rows();
2590    let cols = tensor.cols();
2591    let dim = options
2592        .dim
2593        .unwrap_or_else(|| first_nonsingleton_dim(rows, cols));
2594    validate_matrix_dim(dim, "movmad")?;
2595    let shape = tensor.shape.clone();
2596    let output_dtype = if tensor.integer_storage().is_some() {
2597        NumericDType::F64
2598    } else {
2599        tensor.numeric_dtype()
2600    };
2601    let values = tensor_utils::tensor_values_f64_cow(&tensor);
2602    let mut out = vec![f64::NAN; values.len()];
2603    if dim == 1 {
2604        for col in 0..cols {
2605            for row in 0..rows {
2606                out[row + col * rows] = moving_mad_at(
2607                    &values,
2608                    rows,
2609                    col * rows,
2610                    row,
2611                    rows,
2612                    1,
2613                    window,
2614                    options.omit_nan,
2615                );
2616            }
2617        }
2618    } else {
2619        for row in 0..rows {
2620            for col in 0..cols {
2621                out[row + col * rows] = moving_mad_at(
2622                    &values,
2623                    rows,
2624                    row,
2625                    col,
2626                    cols,
2627                    rows,
2628                    window,
2629                    options.omit_nan,
2630                );
2631            }
2632        }
2633    }
2634    Tensor::new_with_dtype(out, shape, output_dtype)
2635        .map(Value::Tensor)
2636        .map_err(internal_error)
2637}
2638
2639fn moving_mad_at(
2640    data: &[f64],
2641    _rows: usize,
2642    start: usize,
2643    pos: usize,
2644    len: usize,
2645    step: usize,
2646    window: usize,
2647    omit_nan: bool,
2648) -> f64 {
2649    let before = (window - 1) / 2;
2650    let after = window / 2;
2651    let lo = pos.saturating_sub(before);
2652    let hi = pos.saturating_add(after).saturating_add(1).min(len);
2653    let mut vals = Vec::new();
2654    for idx in lo..hi {
2655        let value = data[start + idx * step];
2656        if value.is_nan() && omit_nan {
2657            continue;
2658        }
2659        vals.push(value);
2660    }
2661    if vals.is_empty() || vals.iter().any(|value| value.is_nan()) {
2662        return f64::NAN;
2663    }
2664    let med = summary_value(vals.clone(), &Summary::Median);
2665    let mut devs: Vec<f64> = vals.into_iter().map(|value| (value - med).abs()).collect();
2666    summary_value(::std::mem::take(&mut devs), &Summary::Median)
2667}
2668
2669struct IndicatorSet {
2670    numeric: Vec<f64>,
2671    text: Vec<String>,
2672}
2673
2674fn indicator_set(value: &Value) -> BuiltinResult<IndicatorSet> {
2675    let mut set = IndicatorSet {
2676        numeric: Vec::new(),
2677        text: Vec::new(),
2678    };
2679    collect_indicators(value, &mut set)?;
2680    Ok(set)
2681}
2682
2683fn collect_indicators(value: &Value, set: &mut IndicatorSet) -> BuiltinResult<()> {
2684    match value {
2685        Value::Num(n) => set.numeric.push(*n),
2686        Value::Int(i) => set.numeric.push(i.to_f64()),
2687        Value::String(s) => set.text.push(s.clone()),
2688        Value::StringArray(array) => set.text.extend(array.data.iter().cloned()),
2689        Value::CharArray(array) => set.text.extend(char_rows(array)),
2690        Value::Tensor(tensor) => set.numeric.extend(tensor_utils::tensor_values_f64(tensor)),
2691        Value::Cell(cell) => {
2692            for item in &cell.data {
2693                collect_indicators(item, set)?;
2694            }
2695        }
2696        other => {
2697            return Err(unsupported_type(format!(
2698                "unsupported missing indicator {other:?}"
2699            )))
2700        }
2701    }
2702    Ok(())
2703}
2704
2705fn standardize_missing_value(value: Value, indicators: &IndicatorSet) -> BuiltinResult<Value> {
2706    match value {
2707        Value::Tensor(tensor) if tensor.integer_storage().is_some() => Ok(Value::Tensor(tensor)),
2708        Value::Tensor(tensor) => {
2709            let dtype = tensor.numeric_dtype();
2710            let shape = tensor.shape.clone();
2711            let mut values = tensor_utils::tensor_into_values_f64(tensor);
2712            for value in &mut values {
2713                if indicators
2714                    .numeric
2715                    .iter()
2716                    .any(|marker| numeric_indicator_matches(*value, *marker))
2717                {
2718                    *value = f64::NAN;
2719                }
2720            }
2721            Tensor::new_with_dtype(values, shape, dtype)
2722                .map(Value::Tensor)
2723                .map_err(internal_error)
2724        }
2725        Value::String(mut s) => {
2726            if indicators.text.iter().any(|marker| marker == &s) {
2727                s = MISSING_TEXT.to_string();
2728            }
2729            Ok(Value::String(s))
2730        }
2731        Value::StringArray(mut array) => {
2732            for text in &mut array.data {
2733                if indicators.text.iter().any(|marker| marker == text) {
2734                    *text = MISSING_TEXT.to_string();
2735                }
2736            }
2737            Ok(Value::StringArray(array))
2738        }
2739        Value::CharArray(array) => {
2740            let rows = char_rows(&array);
2741            let data: Vec<String> = rows
2742                .into_iter()
2743                .map(|text| {
2744                    if indicators.text.iter().any(|marker| marker == &text) {
2745                        MISSING_TEXT.to_string()
2746                    } else {
2747                        text
2748                    }
2749                })
2750                .collect();
2751            Ok(Value::StringArray(
2752                StringArray::new(data, vec![array.rows, 1]).map_err(internal_error)?,
2753            ))
2754        }
2755        Value::Object(mut object) if is_tabular_object(&object) => {
2756            let variables = table_variables(&object)?;
2757            let mut out = StructValue::new();
2758            for (name, field) in variables.fields {
2759                out.insert(name, standardize_missing_value(field, indicators)?);
2760            }
2761            object
2762                .properties
2763                .insert("__table_variables".to_string(), Value::Struct(out));
2764            Ok(Value::Object(object))
2765        }
2766        Value::Cell(cell) => {
2767            let mut out = Vec::with_capacity(cell.data.len());
2768            for item in cell.data {
2769                out.push(standardize_missing_value(item, indicators)?);
2770            }
2771            Ok(Value::Cell(
2772                CellArray::new(out, cell.rows, cell.cols).map_err(internal_error)?,
2773            ))
2774        }
2775        other => Ok(other),
2776    }
2777}
2778
2779fn numeric_indicator_matches(value: f64, marker: f64) -> bool {
2780    if marker.is_nan() {
2781        value.is_nan()
2782    } else {
2783        value == marker
2784    }
2785}
2786
2787fn numeric_tensor(value: Value, context: &str) -> BuiltinResult<Tensor> {
2788    match value {
2789        Value::Tensor(tensor) => Ok(tensor),
2790        Value::Num(n) => Tensor::new(vec![n], vec![1, 1]).map_err(internal_error),
2791        Value::Int(i) => Tensor::new(vec![i.to_f64()], vec![1, 1]).map_err(internal_error),
2792        Value::LogicalArray(array) => Tensor::new(
2793            array
2794                .data
2795                .iter()
2796                .map(|flag| f64::from(*flag != 0))
2797                .collect(),
2798            array.shape,
2799        )
2800        .map_err(internal_error),
2801        other => Err(unsupported_type(format!(
2802            "{context}: expected numeric input, got {other:?}"
2803        ))),
2804    }
2805}
2806
2807fn is_numeric_data_like(value: &Value) -> bool {
2808    matches!(
2809        value,
2810        Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::Tensor(_) | Value::LogicalArray(_)
2811    )
2812}
2813
2814fn pairwise_nan_min(left: Value, right: Value) -> BuiltinResult<Value> {
2815    if crate::builtins::math::reduction::integer_native::value_has_integer_storage(&left)
2816        || crate::builtins::math::reduction::integer_native::value_has_integer_storage(&right)
2817    {
2818        if let Ok(Some(evaluation)) =
2819            crate::builtins::math::reduction::integer_native::elementwise_value_extrema(
2820                &left,
2821                &right,
2822                crate::builtins::math::reduction::integer_native::ExtremaDirection::Min,
2823                crate::builtins::math::reduction::integer_native::ExtremaComparison::Natural,
2824                false,
2825            )
2826        {
2827            return Ok(evaluation.values);
2828        }
2829    }
2830    let left_scalar = numeric_scalar(&left, "nanmin left").ok();
2831    let right_scalar = numeric_scalar(&right, "nanmin right").ok();
2832    if let (Some(a), Some(b)) = (left_scalar, right_scalar) {
2833        return Ok(Value::Num(nan_min_pair(a, b)));
2834    }
2835    let left = numeric_tensor(left, "nanmin left")?;
2836    let right = numeric_tensor(right, "nanmin right")?;
2837    let (data, shape, dtype) = broadcast_pairwise_numeric(&left, &right, nan_min_pair)?;
2838    Tensor::new_with_dtype(data, shape, dtype)
2839        .map(Value::Tensor)
2840        .map_err(internal_error)
2841}
2842
2843fn broadcast_pairwise_numeric(
2844    left: &Tensor,
2845    right: &Tensor,
2846    op: impl Fn(f64, f64) -> f64,
2847) -> BuiltinResult<(Vec<f64>, Vec<usize>, runmat_value::NumericDType)> {
2848    let left_len = tensor_utils::tensor_element_len(left);
2849    let right_len = tensor_utils::tensor_element_len(right);
2850    if left_len == right_len && left.shape == right.shape {
2851        let left_values = tensor_utils::tensor_values_f64_cow(left);
2852        let right_values = tensor_utils::tensor_values_f64_cow(right);
2853        let data = left_values
2854            .iter()
2855            .zip(right_values.iter())
2856            .map(|(a, b)| op(*a, *b))
2857            .collect();
2858        return Ok((data, left.shape.clone(), left.numeric_dtype()));
2859    }
2860    if left_len == 1 {
2861        let left_values = tensor_utils::tensor_values_f64_cow(left);
2862        let right_values = tensor_utils::tensor_values_f64_cow(right);
2863        let data = right_values
2864            .iter()
2865            .map(|b| op(left_values[0], *b))
2866            .collect();
2867        return Ok((data, right.shape.clone(), right.numeric_dtype()));
2868    }
2869    if right_len == 1 {
2870        let left_values = tensor_utils::tensor_values_f64_cow(left);
2871        let right_values = tensor_utils::tensor_values_f64_cow(right);
2872        let data = left_values
2873            .iter()
2874            .map(|a| op(*a, right_values[0]))
2875            .collect();
2876        return Ok((data, left.shape.clone(), left.numeric_dtype()));
2877    }
2878    Err(invalid_argument(
2879        "nanmin: pairwise inputs must have the same shape or one scalar input",
2880    ))
2881}
2882
2883fn nan_min_pair(a: f64, b: f64) -> f64 {
2884    match (a.is_nan(), b.is_nan()) {
2885        (true, true) => f64::NAN,
2886        (true, false) => b,
2887        (false, true) => a,
2888        (false, false) => a.min(b),
2889    }
2890}
2891
2892fn numeric_scalar(value: &Value, context: &str) -> BuiltinResult<f64> {
2893    match value {
2894        Value::Num(n) => Ok(*n),
2895        Value::Int(i) => Ok(i.to_f64()),
2896        Value::Bool(b) => Ok(f64::from(*b)),
2897        Value::Tensor(tensor) if tensor_utils::is_scalar_tensor(tensor) => {
2898            Ok(tensor_utils::tensor_value_f64(tensor, 0))
2899        }
2900        other => Err(invalid_argument(format!(
2901            "{context}: expected numeric scalar, got {other:?}"
2902        ))),
2903    }
2904}
2905
2906fn first_nonsingleton_dim(rows: usize, cols: usize) -> usize {
2907    if rows > 1 {
2908        1
2909    } else if cols > 1 {
2910        2
2911    } else {
2912        1
2913    }
2914}
2915
2916fn validate_matrix_dim(dim: usize, context: &str) -> BuiltinResult<()> {
2917    if dim == 1 || dim == 2 {
2918        Ok(())
2919    } else {
2920        Err(invalid_argument(format!(
2921            "{context}: dimension must be 1 or 2"
2922        )))
2923    }
2924}
2925
2926fn scalar_usize(value: &Value, context: &str) -> BuiltinResult<usize> {
2927    match value {
2928        Value::Int(integer) => return integer_size_to_usize(integer, context),
2929        Value::Tensor(tensor) if tensor_utils::is_scalar_tensor(tensor) => {
2930            if let Some(storage) = tensor.integer_storage() {
2931                let integer = storage.value_at(0).ok_or_else(|| {
2932                    internal_error(format!("{context}: integer scalar storage length mismatch"))
2933                })?;
2934                return integer_size_to_usize(&integer, context);
2935            }
2936        }
2937        _ => {}
2938    }
2939    let n = numeric_scalar(value, context)?;
2940    numeric_size_to_usize(n, context)
2941}
2942
2943fn integer_size_to_usize(value: &IntValue, context: &str) -> BuiltinResult<usize> {
2944    value.try_to_usize().ok_or_else(|| {
2945        invalid_argument(format!("{context}: expected nonnegative platform integer"))
2946    })
2947}
2948
2949fn numeric_size_to_usize(n: f64, context: &str) -> BuiltinResult<usize> {
2950    if !n.is_finite() || n < 0.0 || n.fract() != 0.0 {
2951        return Err(invalid_argument(format!(
2952            "{context}: expected nonnegative integer"
2953        )));
2954    }
2955    if n > usize::MAX as f64 {
2956        return Err(invalid_argument(format!("{context}: integer too large")));
2957    }
2958    if usize::BITS == 64 && n == usize::MAX as f64 {
2959        return Err(invalid_argument(format!("{context}: integer too large")));
2960    }
2961    Ok(n as usize)
2962}
2963
2964fn scalar_text(value: &Value) -> Option<String> {
2965    match value {
2966        Value::String(text) => Some(text.clone()),
2967        Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
2968        Value::CharArray(array) if array.rows == 1 => Some(array.data.iter().collect()),
2969        _ => None,
2970    }
2971}
2972
2973fn char_rows(array: &CharArray) -> Vec<String> {
2974    let mut out = Vec::with_capacity(array.rows);
2975    for row in 0..array.rows {
2976        let start = row * array.cols;
2977        out.push(array.data[start..start + array.cols].iter().collect());
2978    }
2979    out
2980}
2981
2982fn is_missing_text(text: &str) -> bool {
2983    text.eq_ignore_ascii_case(MISSING_TEXT)
2984}
2985
2986#[cfg(test)]
2987mod tests {
2988    use super::*;
2989    use crate::builtins::common::test_support;
2990    use futures::executor::block_on;
2991    #[cfg(feature = "wgpu")]
2992    use runmat_accelerate_api::{HostIntegerDataView, HostIntegerTensorView};
2993    use runmat_value::IntegerStorage;
2994
2995    fn tensor(data: Vec<f64>, shape: Vec<usize>) -> Value {
2996        Value::Tensor(Tensor::new(data, shape).unwrap())
2997    }
2998
2999    fn first_unrepresentable_usize_double() -> f64 {
3000        if usize::BITS == 64 {
3001            usize::MAX as f64
3002        } else {
3003            (usize::MAX as f64) + 1.0
3004        }
3005    }
3006
3007    #[test]
3008    fn missing_constructs_scalar_and_arrays() {
3009        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3010        let scalar = block_on(missing_builtin(Vec::new())).unwrap();
3011        assert!(matches!(scalar, Value::StringArray(sa) if sa.data == vec![MISSING_TEXT]));
3012        let shaped = block_on(missing_builtin(vec![Value::Num(2.0), Value::Num(3.0)])).unwrap();
3013        assert!(
3014            matches!(shaped, Value::StringArray(sa) if sa.shape == vec![2, 3] && sa.data.len() == 6)
3015        );
3016    }
3017
3018    #[test]
3019    fn missing_runmat_shape_extension_reads_every_integer_class_exactly() {
3020        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3021        let cases = [
3022            IntValue::I8(2),
3023            IntValue::I16(2),
3024            IntValue::I32(2),
3025            IntValue::I64(2),
3026            IntValue::U8(2),
3027            IntValue::U16(2),
3028            IntValue::U32(2),
3029            IntValue::U64(2),
3030        ];
3031        for size in cases {
3032            let result =
3033                block_on(missing_builtin(vec![Value::Int(size)])).expect("RunMat shaped missing");
3034            assert!(
3035                matches!(result, Value::StringArray(array) if array.shape == vec![2, 2] && array.data.len() == 4)
3036            );
3037        }
3038    }
3039
3040    #[test]
3041    fn missing_shaped_extension_gates_before_provider_access() {
3042        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3043        let value = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
3044            shape: vec![1, 1],
3045            device_id: 0,
3046            buffer_id: 9_419_005,
3047            descriptor: Default::default(),
3048        });
3049        let error = block_on(missing_builtin(vec![value]))
3050            .expect_err("MATLAB-compatible mode must reject shaped missing");
3051        assert_eq!(
3052            error.identifier(),
3053            Some("RunMat:compatibility:MissingShapedArrayExtension")
3054        );
3055    }
3056
3057    #[test]
3058    fn missing_preserves_typed_integer_size_vectors_exactly() {
3059        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3060        let large = 9_007_199_254_740_993_u64;
3061        let dims = Tensor::new_integer(IntegerStorage::U64(vec![large, 0]), vec![1, 2]).unwrap();
3062
3063        let result = block_on(missing_builtin(vec![Value::Tensor(dims)])).unwrap();
3064
3065        match result {
3066            Value::StringArray(array) => {
3067                assert_eq!(array.shape, vec![large as usize, 0]);
3068                assert!(array.data.is_empty());
3069            }
3070            other => panic!("expected string array, got {other:?}"),
3071        }
3072    }
3073
3074    #[test]
3075    #[cfg(target_pointer_width = "64")]
3076    fn missing_parses_typed_integer_scalar_tensors_exactly() {
3077        let scalar = Value::Tensor(
3078            Tensor::new_integer(IntegerStorage::U64(vec![9_007_199_254_740_993]), vec![1, 1])
3079                .unwrap(),
3080        );
3081
3082        assert_eq!(
3083            scalar_usize(&scalar, "missing size").unwrap(),
3084            9_007_199_254_740_993
3085        );
3086    }
3087
3088    #[test]
3089    #[cfg(target_pointer_width = "32")]
3090    fn missing_rejects_typed_integer_scalar_tensors_outside_platform_range() {
3091        let scalar = Value::Tensor(
3092            Tensor::new_integer(IntegerStorage::U64(vec![9_007_199_254_740_993]), vec![1, 1])
3093                .unwrap(),
3094        );
3095
3096        let err = scalar_usize(&scalar, "missing size").expect_err("dimension must fit usize");
3097        assert!(err.message.contains("platform integer"), "{}", err.message);
3098    }
3099
3100    #[test]
3101    fn missing_numeric_scalar_reads_typed_integer_storage_exactly() {
3102        let tensor = Tensor::new_integer(IntegerStorage::U16(vec![2026]), vec![1, 1])
3103            .expect("typed numeric scalar");
3104
3105        assert_eq!(
3106            numeric_scalar(&Value::Tensor(tensor), "fillmissing constant").unwrap(),
3107            2026.0
3108        );
3109    }
3110
3111    #[test]
3112    fn missing_rejects_negative_typed_integer_sizes() {
3113        let scalar = Tensor::new_integer(IntegerStorage::I8(vec![-1]), vec![1, 1]).unwrap();
3114        assert!(scalar_usize(&Value::Tensor(scalar), "missing size").is_err());
3115
3116        let dims = Tensor::new_integer(IntegerStorage::I64(vec![2, -1]), vec![1, 2]).unwrap();
3117        assert!(tensor_shape_as_size(&dims).is_err());
3118    }
3119
3120    #[test]
3121    fn missing_rejects_unrepresentable_double_sizes_before_casting() {
3122        let err = scalar_usize(
3123            &Value::Num(first_unrepresentable_usize_double()),
3124            "missing size",
3125        )
3126        .unwrap_err();
3127        assert!(err.message.contains("integer too large"), "{}", err.message);
3128
3129        let dims =
3130            Tensor::new(vec![first_unrepresentable_usize_double(), 0.0], vec![1, 2]).unwrap();
3131        let err = tensor_shape_as_size(&dims).unwrap_err();
3132        assert!(err.message.contains("platform limits"), "{}", err.message);
3133    }
3134
3135    #[test]
3136    fn ismissing_detects_numeric_and_string_values() {
3137        let result = block_on(ismissing_builtin(tensor(
3138            vec![1.0, f64::NAN, 3.0],
3139            vec![1, 3],
3140        )))
3141        .unwrap();
3142        assert!(matches!(result, Value::LogicalArray(mask) if mask.data == vec![0, 1, 0]));
3143
3144        let strings = StringArray::new(vec!["a".into(), MISSING_TEXT.into()], vec![1, 2]).unwrap();
3145        let result = block_on(ismissing_builtin(Value::StringArray(strings))).unwrap();
3146        assert!(matches!(result, Value::LogicalArray(mask) if mask.data == vec![0, 1]));
3147    }
3148
3149    #[test]
3150    fn ismissing_typed_integer_tensor_ignores_f64_mirror() {
3151        let input = Tensor::new_integer(IntegerStorage::I16(vec![1, 2, 3]), vec![1, 3])
3152            .expect("integer tensor");
3153
3154        let result = block_on(ismissing_builtin(Value::Tensor(input))).unwrap();
3155
3156        assert!(matches!(
3157            result,
3158            Value::LogicalArray(mask) if mask.data == vec![0, 0, 0] && mask.shape == vec![1, 3]
3159        ));
3160    }
3161
3162    #[test]
3163    fn ismissing_returns_same_shaped_false_for_all_integer_classes() {
3164        let storages = [
3165            IntegerStorage::I8(vec![i8::MIN, i8::MAX]),
3166            IntegerStorage::I16(vec![i16::MIN, i16::MAX]),
3167            IntegerStorage::I32(vec![i32::MIN, i32::MAX]),
3168            IntegerStorage::I64(vec![i64::MIN, i64::MAX]),
3169            IntegerStorage::U8(vec![u8::MIN, u8::MAX]),
3170            IntegerStorage::U16(vec![u16::MIN, u16::MAX]),
3171            IntegerStorage::U32(vec![u32::MIN, u32::MAX]),
3172            IntegerStorage::U64(vec![u64::MIN, u64::MAX]),
3173        ];
3174        for storage in storages {
3175            let input = Tensor::new_integer(storage, vec![2, 1]).expect("integer tensor");
3176            let result = block_on(ismissing_builtin(Value::Tensor(input))).expect("ismissing");
3177            assert!(matches!(
3178                result,
3179                Value::LogicalArray(mask)
3180                    if mask.shape == vec![2, 1] && mask.data == vec![0, 0]
3181            ));
3182        }
3183    }
3184
3185    #[test]
3186    fn ismissing_resident_integer_uses_shape_metadata_and_returns_host_mask() {
3187        test_support::with_test_provider(|provider| {
3188            let input = Tensor::new_integer(IntegerStorage::U64(vec![0, u64::MAX]), vec![1, 2])
3189                .expect("integer tensor");
3190            let handle = gpu_helpers::upload_tensor(provider, &input).expect("upload integer");
3191            let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
3192            let result = block_on(ismissing_builtin(Value::GpuTensor(handle.clone())))
3193                .expect("resident ismissing extension");
3194            assert!(matches!(
3195                result,
3196                Value::LogicalArray(mask)
3197                    if mask.shape == vec![1, 2] && mask.data == vec![0, 0]
3198            ));
3199            assert!(gpu_helpers::exact_provider_for_handle(&handle).is_some());
3200            provider.free(&handle).ok();
3201        });
3202    }
3203
3204    #[test]
3205    fn ismissing_rejects_contradictory_resident_integer_metadata() {
3206        test_support::with_test_provider(|provider| {
3207            let input = Tensor::new_integer(IntegerStorage::I8(vec![1]), vec![1, 1])
3208                .expect("integer tensor");
3209            let handle = gpu_helpers::upload_tensor(provider, &input).expect("upload integer");
3210            runmat_accelerate_api::set_handle_logical(&handle, true);
3211            let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
3212            let error = block_on(ismissing_builtin(Value::GpuTensor(handle.clone())))
3213                .expect_err("integer/logical metadata contradiction must reject");
3214            assert!(error.message().contains("metadata is contradictory"));
3215            provider.free(&handle).ok();
3216        });
3217    }
3218
3219    #[test]
3220    fn ismissing_matlab_mode_only_gates_explicit_resident_input() {
3221        test_support::with_test_provider(|provider| {
3222            let input = Tensor::new_integer(IntegerStorage::U16(vec![1]), vec![1, 1])
3223                .expect("integer tensor");
3224            let handle = gpu_helpers::upload_tensor(provider, &input).expect("upload integer");
3225            {
3226                let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3227                let result = block_on(ismissing_builtin(Value::GpuTensor(handle.clone())))
3228                    .expect("automatic residency is transparent");
3229                assert!(matches!(
3230                    result,
3231                    Value::LogicalArray(mask)
3232                        if mask.shape == vec![1, 1] && mask.data == vec![0]
3233                ));
3234            }
3235            let handle =
3236                handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
3237            {
3238                let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3239                let error = block_on(ismissing_builtin(Value::GpuTensor(handle.clone())))
3240                    .expect_err("explicit resident input is a compatibility-gated extension");
3241                assert_eq!(
3242                    error.identifier(),
3243                    ISMISSING_RESIDENT_INPUT_EXTENSION.error_identifier
3244                );
3245            }
3246            provider.free(&handle).ok();
3247        });
3248    }
3249
3250    #[test]
3251    fn anymissing_returns_false_for_every_integer_storage_class() {
3252        let storages = [
3253            IntegerStorage::I8(vec![i8::MIN, i8::MAX]),
3254            IntegerStorage::I16(vec![i16::MIN, i16::MAX]),
3255            IntegerStorage::I32(vec![i32::MIN, i32::MAX]),
3256            IntegerStorage::I64(vec![i64::MIN, i64::MAX]),
3257            IntegerStorage::U8(vec![u8::MIN, u8::MAX]),
3258            IntegerStorage::U16(vec![u16::MIN, u16::MAX]),
3259            IntegerStorage::U32(vec![u32::MIN, u32::MAX]),
3260            IntegerStorage::U64(vec![u64::MIN, u64::MAX]),
3261        ];
3262        for storage in storages {
3263            let input = Tensor::new_integer(storage, vec![1, 2]).unwrap();
3264            assert_eq!(
3265                block_on(anymissing_builtin(Value::Tensor(input))).unwrap(),
3266                Value::Bool(false)
3267            );
3268        }
3269    }
3270
3271    #[test]
3272    fn rmmissing_removes_rows_and_columns() {
3273        let value = tensor(vec![1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0], vec![3, 2]);
3274        let result = block_on(rmmissing_builtin(value, Vec::new())).unwrap();
3275        assert!(
3276            matches!(result, Value::Tensor(t) if t.shape == vec![2, 2] && t.materialize_f64() == vec![1.0, 2.0, 4.0, 5.0])
3277        );
3278
3279        let value = tensor(vec![1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0], vec![3, 2]);
3280        let result = block_on(rmmissing_builtin(value, vec![Value::Num(2.0)])).unwrap();
3281        assert!(
3282            matches!(result, Value::Tensor(t) if t.shape == vec![3, 1] && t.materialize_f64() == vec![4.0, 5.0, 6.0])
3283        );
3284    }
3285
3286    #[test]
3287    fn rmmissing_typed_integer_tensor_preserves_storage_and_reports_no_missing() {
3288        let expected = IntegerStorage::U64(vec![1, u64::MAX, 3, 4]);
3289        let input = Tensor::new_integer(expected.clone(), vec![2, 2]).expect("integer tensor");
3290
3291        let result = remove_missing_tensor(
3292            input,
3293            RemoveOptions {
3294                dim: RemoveDim::Rows,
3295            },
3296        )
3297        .unwrap();
3298
3299        match result {
3300            (Value::Tensor(tensor), mask) => {
3301                assert_eq!(tensor.integer_storage(), Some(&expected));
3302                assert_eq!(mask.data, vec![0, 0]);
3303                assert_eq!(mask.shape, vec![2, 1]);
3304            }
3305            other => panic!("expected tensor and mask, got {other:?}"),
3306        }
3307    }
3308
3309    #[test]
3310    fn rmmissing_typed_integer_vector_mask_uses_storage_len_not_mirror() {
3311        let expected = IntegerStorage::I16(vec![1, 2, 3]);
3312        let input = Tensor::new_integer(expected.clone(), vec![1, 3]).expect("integer tensor");
3313
3314        let result = remove_missing_tensor(
3315            input,
3316            RemoveOptions {
3317                dim: RemoveDim::Rows,
3318            },
3319        )
3320        .unwrap();
3321
3322        match result {
3323            (Value::Tensor(tensor), mask) => {
3324                assert_eq!(tensor.integer_storage(), Some(&expected));
3325                assert_eq!(mask.data, vec![0, 0, 0]);
3326                assert_eq!(mask.shape, vec![1, 3]);
3327            }
3328            other => panic!("expected tensor and mask, got {other:?}"),
3329        }
3330    }
3331
3332    #[test]
3333    fn rmmissing_resident_integer_restores_value_and_mask_through_exact_owner() {
3334        test_support::with_test_provider(|provider| {
3335            let input = Tensor::new_integer(
3336                IntegerStorage::U64(vec![1, 9_007_199_254_740_993]),
3337                vec![1, 2],
3338            )
3339            .expect("integer tensor");
3340            let source = gpu_helpers::upload_tensor(provider, &input).expect("upload integer");
3341            let source =
3342                source.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
3343            let _outputs = crate::output_count::push_output_count(Some(2));
3344            let result = block_on(rmmissing_builtin(
3345                Value::GpuTensor(source.clone()),
3346                Vec::new(),
3347            ))
3348            .expect("resident rmmissing");
3349            let Value::OutputList(outputs) = result else {
3350                panic!("expected output list");
3351            };
3352            assert_eq!(outputs.len(), 2);
3353            for output in &outputs {
3354                assert!(
3355                    matches!(output, Value::GpuTensor(handle) if runmat_accelerate_api::handle_is_explicit(handle))
3356                );
3357            }
3358            assert!(
3359                matches!(&outputs[1], Value::GpuTensor(handle) if runmat_accelerate_api::handle_is_logical(handle))
3360            );
3361            let gathered_value = test_support::gather(outputs[0].clone()).expect("gather value");
3362            assert_eq!(
3363                gathered_value.integer_storage(),
3364                Some(&IntegerStorage::U64(vec![1, 9_007_199_254_740_993]))
3365            );
3366            let gathered_mask = test_support::gather(outputs[1].clone()).expect("gather mask");
3367            assert_eq!(gathered_mask.shape, vec![1, 2]);
3368            assert_eq!(gathered_mask.materialize_f64(), vec![0.0, 0.0]);
3369            assert!(gpu_helpers::exact_provider_for_handle(&source).is_some());
3370            for output in outputs {
3371                if let Value::GpuTensor(handle) = output {
3372                    provider.free(&handle).ok();
3373                }
3374            }
3375            provider.free(&source).ok();
3376        });
3377    }
3378
3379    #[test]
3380    fn rmmissing_cell_arrays_use_cell_row_major_order() {
3381        let value = Value::Cell(
3382            CellArray::new(
3383                vec![
3384                    Value::Num(1.0),
3385                    Value::StringArray(
3386                        StringArray::new(vec![MISSING_TEXT.into()], vec![1, 1]).unwrap(),
3387                    ),
3388                    Value::Num(2.0),
3389                    Value::Num(3.0),
3390                ],
3391                2,
3392                2,
3393            )
3394            .unwrap(),
3395        );
3396        let result = block_on(rmmissing_builtin(value.clone(), Vec::new())).unwrap();
3397        match result {
3398            Value::Cell(cell) => {
3399                assert_eq!((cell.rows, cell.cols), (1, 2));
3400                assert_eq!(cell.get(0, 0).unwrap(), Value::Num(2.0));
3401                assert_eq!(cell.get(0, 1).unwrap(), Value::Num(3.0));
3402            }
3403            other => panic!("expected cell result, got {other:?}"),
3404        }
3405
3406        let result = block_on(rmmissing_builtin(value, vec![Value::Num(2.0)])).unwrap();
3407        match result {
3408            Value::Cell(cell) => {
3409                assert_eq!((cell.rows, cell.cols), (2, 1));
3410                assert_eq!(cell.get(0, 0).unwrap(), Value::Num(1.0));
3411                assert_eq!(cell.get(1, 0).unwrap(), Value::Num(2.0));
3412            }
3413            other => panic!("expected cell result, got {other:?}"),
3414        }
3415    }
3416
3417    #[test]
3418    fn fillmissing_supports_constant_previous_and_linear() {
3419        let value = tensor(vec![1.0, f64::NAN, 3.0], vec![3, 1]);
3420        let result = block_on(fillmissing_builtin(value, vec![Value::from("linear")])).unwrap();
3421        assert!(matches!(result, Value::Tensor(t) if t.materialize_f64() == vec![1.0, 2.0, 3.0]));
3422
3423        let value = tensor(vec![1.0, f64::NAN, 3.0], vec![1, 3]);
3424        let result = block_on(fillmissing_builtin(value, vec![Value::from("linear")])).unwrap();
3425        assert!(matches!(result, Value::Tensor(t) if t.materialize_f64() == vec![1.0, 2.0, 3.0]));
3426
3427        let value = tensor(vec![1.0, f64::NAN, f64::NAN], vec![3, 1]);
3428        let result = block_on(fillmissing_builtin(
3429            value,
3430            vec![Value::from("constant"), Value::Num(9.0)],
3431        ))
3432        .unwrap();
3433        assert!(matches!(result, Value::Tensor(t) if t.materialize_f64() == vec![1.0, 9.0, 9.0]));
3434    }
3435
3436    #[test]
3437    fn fillmissing_typed_integer_tensor_preserves_storage_and_reports_no_missing() {
3438        let expected = IntegerStorage::I32(vec![10, 20, 30]);
3439        let input = Tensor::new_integer(expected.clone(), vec![3, 1]).expect("integer tensor");
3440
3441        let result = fill_missing_tensor(
3442            input,
3443            &FillOptions {
3444                method: FillMethod::Constant(Value::Num(0.0)),
3445                dim: None,
3446            },
3447        )
3448        .unwrap();
3449
3450        match result {
3451            (Value::Tensor(tensor), mask) => {
3452                assert_eq!(tensor.integer_storage(), Some(&expected));
3453                assert_eq!(mask.data, vec![0, 0, 0]);
3454                assert_eq!(mask.shape, vec![3, 1]);
3455            }
3456            other => panic!("expected tensor and mask, got {other:?}"),
3457        }
3458    }
3459
3460    #[test]
3461    fn fillmissing_integer_data_is_gated_and_all_classes_are_exact_noops() {
3462        let storages = [
3463            IntegerStorage::I8(vec![-1, 2]),
3464            IntegerStorage::I16(vec![-1, 2]),
3465            IntegerStorage::I32(vec![-1, 2]),
3466            IntegerStorage::I64(vec![-1, 2]),
3467            IntegerStorage::U8(vec![1, 2]),
3468            IntegerStorage::U16(vec![1, 2]),
3469            IntegerStorage::U32(vec![1, 2]),
3470            IntegerStorage::U64(vec![u64::MAX - 1, u64::MAX]),
3471        ];
3472        for storage in storages {
3473            let input = Value::Tensor(Tensor::new_integer(storage.clone(), vec![2, 1]).unwrap());
3474            {
3475                let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3476                let error = block_on(fillmissing_builtin(
3477                    input.clone(),
3478                    vec![Value::from("constant"), Value::Num(0.0)],
3479                ))
3480                .expect_err("strict integer fillmissing");
3481                assert_eq!(
3482                    error.identifier(),
3483                    Some("RunMat:compatibility:FillmissingIntegerDataExtension")
3484                );
3485            }
3486            let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3487            let _outputs = crate::output_count::push_output_count(Some(2));
3488            let output = block_on(fillmissing_builtin(
3489                input,
3490                vec![Value::from("constant"), Value::Num(0.0)],
3491            ))
3492            .expect("RunMat integer fillmissing");
3493            let Value::OutputList(values) = output else {
3494                panic!("expected output list");
3495            };
3496            assert!(
3497                matches!(&values[0], Value::Tensor(tensor) if tensor.integer_storage() == Some(&storage))
3498            );
3499            assert!(
3500                matches!(&values[1], Value::LogicalArray(mask) if mask.shape == vec![2, 1] && mask.data == vec![0, 0])
3501            );
3502        }
3503    }
3504
3505    #[test]
3506    fn fillmissing_integer_table_variable_and_nested_cell_are_aggregate_gated() {
3507        let integer = Value::Tensor(
3508            Tensor::new_integer(IntegerStorage::I16(vec![1, 2]), vec![2, 1]).unwrap(),
3509        );
3510        let table = crate::builtins::table::table_from_columns(
3511            vec!["I".to_string()],
3512            vec![integer.clone()],
3513        )
3514        .unwrap();
3515        let nested = Value::Cell(
3516            CellArray::new(
3517                vec![Value::Cell(CellArray::new(vec![integer], 1, 1).unwrap())],
3518                1,
3519                1,
3520            )
3521            .unwrap(),
3522        );
3523        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3524        for aggregate in [table, nested] {
3525            let error = block_on(fillmissing_builtin(
3526                aggregate,
3527                vec![Value::from("constant"), Value::Num(0.0)],
3528            ))
3529            .expect_err("strict aggregate integer fillmissing");
3530            assert_eq!(
3531                error.identifier(),
3532                Some("RunMat:compatibility:FillmissingAggregateIntegerDataExtension")
3533            );
3534        }
3535    }
3536
3537    #[cfg(feature = "wgpu")]
3538    #[test]
3539    fn fillmissing_nested_resident_integer_is_gated_before_provider_access() {
3540        test_support::with_test_provider(|provider| {
3541            let handle = provider
3542                .upload_integer(&HostIntegerTensorView {
3543                    data: HostIntegerDataView::U64(&[u64::MAX]),
3544                    shape: &[1, 1],
3545                })
3546                .expect("integer upload");
3547            let nested = Value::Cell(
3548                CellArray::new(
3549                    vec![Value::Cell(
3550                        CellArray::new(vec![Value::GpuTensor(handle.clone())], 1, 1).unwrap(),
3551                    )],
3552                    1,
3553                    1,
3554                )
3555                .unwrap(),
3556            );
3557            let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3558            let error = block_on(fillmissing_builtin(
3559                nested,
3560                vec![Value::from("constant"), Value::Num(0.0)],
3561            ))
3562            .expect_err("strict nested resident integer fillmissing");
3563            assert_eq!(
3564                error.identifier(),
3565                Some("RunMat:compatibility:FillmissingAggregateIntegerDataExtension")
3566            );
3567            provider.free(&handle).ok();
3568        });
3569    }
3570
3571    #[test]
3572    fn fillmissing_rejects_complex_and_sparse_arrays_instead_of_aggregate_replacement() {
3573        let complex = Value::ComplexTensor(
3574            runmat_value::ComplexTensor::new(vec![(f64::NAN, 0.0)], vec![1, 1]).unwrap(),
3575        );
3576        let error = fill_missing_value(
3577            complex,
3578            &FillOptions {
3579                method: FillMethod::Constant(Value::Num(0.0)),
3580                dim: None,
3581            },
3582        )
3583        .expect_err("complex arrays are unsupported");
3584        assert!(error.message().contains("complex arrays are not supported"));
3585
3586        let sparse = Value::SparseTensor(
3587            runmat_value::SparseTensor::new(2, 1, vec![0, 1], vec![0], vec![f64::NAN]).unwrap(),
3588        );
3589        let error = fill_missing_value(
3590            sparse,
3591            &FillOptions {
3592                method: FillMethod::Constant(Value::Num(0.0)),
3593                dim: None,
3594            },
3595        )
3596        .expect_err("sparse arrays are unsupported");
3597        assert!(error.message().contains("sparse arrays are not supported"));
3598    }
3599
3600    #[test]
3601    fn fillmissing_nearest_uses_original_neighbors() {
3602        let value = tensor(vec![1.0, f64::NAN, f64::NAN, 4.0], vec![1, 4]);
3603        let result = block_on(fillmissing_builtin(value, vec![Value::from("nearest")])).unwrap();
3604        assert!(
3605            matches!(result, Value::Tensor(t) if t.materialize_f64() == vec![1.0, 1.0, 4.0, 4.0])
3606        );
3607    }
3608
3609    #[test]
3610    fn fillmissing_mask_marks_only_entries_actually_filled() {
3611        let _outputs = crate::output_count::push_output_count(Some(2));
3612        let value = tensor(vec![f64::NAN, 2.0, 3.0], vec![3, 1]);
3613        let output = block_on(fillmissing_builtin(value, vec![Value::from("previous")])).unwrap();
3614        let Value::OutputList(values) = output else {
3615            panic!("expected output list");
3616        };
3617        assert!(matches!(&values[1], Value::LogicalArray(mask) if mask.data == vec![0, 0, 0]));
3618    }
3619
3620    #[test]
3621    fn fillmissing_rejects_unknown_options() {
3622        let value = tensor(vec![1.0, f64::NAN], vec![1, 2]);
3623        let result = block_on(fillmissing_builtin(
3624            value,
3625            vec![
3626                Value::from("constant"),
3627                Value::Num(0.0),
3628                Value::from("bogus"),
3629            ],
3630        ));
3631        assert!(result.is_err());
3632    }
3633
3634    #[test]
3635    fn standardize_missing_replaces_indicators() {
3636        let result = block_on(standardize_missing_builtin(
3637            tensor(vec![-99.0, 2.0], vec![1, 2]),
3638            vec![Value::Num(-99.0)],
3639        ))
3640        .unwrap();
3641        assert!(
3642            matches!(result, Value::Tensor(t) if t.materialize_f64()[0].is_nan() && t.materialize_f64()[1] == 2.0)
3643        );
3644    }
3645
3646    #[test]
3647    fn standardize_missing_reads_indicator_storage_and_does_not_nan_integer_targets() {
3648        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3649        let marker = Tensor::new_integer(IntegerStorage::I16(vec![-99]), vec![1, 1])
3650            .expect("integer marker");
3651        let expected = IntegerStorage::I16(vec![-99, 2]);
3652        let input = Tensor::new_integer(expected.clone(), vec![1, 2]).expect("integer input");
3653
3654        let result = block_on(standardize_missing_builtin(
3655            Value::Tensor(input),
3656            vec![Value::Tensor(marker)],
3657        ))
3658        .unwrap();
3659
3660        match result {
3661            Value::Tensor(tensor) => {
3662                assert_eq!(tensor.integer_storage(), Some(&expected));
3663                assert_eq!(tensor.materialize_f64(), vec![-99.0, 2.0]);
3664            }
3665            other => panic!("expected tensor, got {other:?}"),
3666        }
3667    }
3668
3669    #[test]
3670    fn standardize_missing_integer_data_is_mode_gated() {
3671        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3672        let input = Value::Tensor(
3673            Tensor::new_integer(IntegerStorage::I16(vec![-99, 2]), vec![1, 2]).unwrap(),
3674        );
3675        let error = block_on(standardize_missing_builtin(input, vec![Value::Num(-99.0)]))
3676            .expect_err("MATLAB-compatible mode must reject direct integer data");
3677        assert_eq!(
3678            error.identifier(),
3679            Some("RunMat:compatibility:StandardizeMissingIntegerDataExtension")
3680        );
3681    }
3682
3683    #[test]
3684    fn standardize_missing_documented_integer_indicator_needs_no_extension() {
3685        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3686        let marker =
3687            Value::Tensor(Tensor::new_integer(IntegerStorage::I16(vec![-99]), vec![1, 1]).unwrap());
3688        let result = block_on(standardize_missing_builtin(
3689            tensor(vec![-99.0, 2.0], vec![1, 2]),
3690            vec![marker],
3691        ))
3692        .expect("documented integer indicator");
3693        assert!(
3694            matches!(result, Value::Tensor(t) if t.materialize_f64()[0].is_nan() && t.materialize_f64()[1] == 2.0)
3695        );
3696    }
3697
3698    #[test]
3699    fn standardize_missing_explicit_gpu_indicator_is_gated_before_provider_access() {
3700        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3701        let handle = runmat_accelerate_api::GpuTensorHandle {
3702            shape: vec![1, 1],
3703            device_id: 0,
3704            buffer_id: 9_451_002,
3705            descriptor: Default::default(),
3706        };
3707        let handle = handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
3708        let error = block_on(standardize_missing_builtin(
3709            tensor(vec![-99.0, 2.0], vec![1, 2]),
3710            vec![Value::GpuTensor(handle)],
3711        ))
3712        .expect_err("explicit GPU indicator must be gated");
3713        assert_eq!(
3714            error.identifier(),
3715            Some("RunMat:compatibility:StandardizeMissingExplicitGpuIndicatorExtension")
3716        );
3717    }
3718
3719    #[test]
3720    fn nanmean_alias_uses_omitnan() {
3721        let result = block_on(nanmean_builtin(
3722            tensor(vec![1.0, f64::NAN, 3.0], vec![1, 3]),
3723            vec![Value::from("all")],
3724        ))
3725        .unwrap();
3726        assert!(matches!(result, Value::Num(n) if (n - 2.0).abs() < 1e-12));
3727    }
3728
3729    #[test]
3730    fn legacy_nan_reductions_gate_typed_integer_data_by_compatibility_mode() {
3731        type LegacyNanCase = (
3732            fn(Value, Vec<Value>) -> BuiltinResult<Value>,
3733            &'static str,
3734            &'static str,
3735        );
3736        let cases: [LegacyNanCase; 4] = [
3737            (
3738                |value, rest| block_on(nanmean_builtin(value, rest)),
3739                "nanmean",
3740                "RunMat:compatibility:NanmeanTypedIntegerInputExtension",
3741            ),
3742            (
3743                |value, rest| block_on(nansum_builtin(value, rest)),
3744                "nansum",
3745                "RunMat:compatibility:NansumTypedIntegerInputExtension",
3746            ),
3747            (
3748                |value, rest| block_on(nanmin_builtin(value, rest)),
3749                "nanmin",
3750                "RunMat:compatibility:NanminTypedIntegerInputExtension",
3751            ),
3752            (
3753                |value, rest| block_on(nanmedian_builtin(value, rest)),
3754                "nanmedian",
3755                "RunMat:compatibility:NanmedianTypedIntegerInputExtension",
3756            ),
3757        ];
3758
3759        for (invoke, name, identifier) in cases {
3760            {
3761                let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3762                let error = invoke(Value::Int(IntValue::U16(7)), Vec::new()).unwrap_err();
3763                assert_eq!(error.identifier(), Some(identifier), "{name}");
3764            }
3765            {
3766                let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3767                invoke(Value::Int(IntValue::U16(7)), Vec::new())
3768                    .unwrap_or_else(|error| panic!("{name} RunMat typed-integer input: {error}"));
3769            }
3770        }
3771    }
3772
3773    #[test]
3774    fn legacy_nan_reductions_gate_typed_integer_dimensions() {
3775        let floating = || tensor(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]);
3776        let dimension = || Value::Int(IntValue::U8(2));
3777        let placeholder = || {
3778            Value::Tensor(Tensor::new(Vec::<f64>::new(), vec![0, 0]).expect("empty placeholder"))
3779        };
3780
3781        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3782        let error = block_on(nanmean_builtin(floating(), vec![dimension()]))
3783            .expect_err("strict nanmean typed-integer dimension");
3784        assert_eq!(
3785            error.identifier(),
3786            Some("RunMat:compatibility:NanmeanTypedIntegerInputExtension")
3787        );
3788        let error = block_on(nansum_builtin(floating(), vec![dimension()]))
3789            .expect_err("strict nansum typed-integer dimension");
3790        assert_eq!(
3791            error.identifier(),
3792            Some("RunMat:compatibility:NansumTypedIntegerInputExtension")
3793        );
3794        let error = block_on(nanmedian_builtin(floating(), vec![dimension()]))
3795            .expect_err("strict nanmedian typed-integer dimension");
3796        assert_eq!(
3797            error.identifier(),
3798            Some("RunMat:compatibility:NanmedianTypedIntegerInputExtension")
3799        );
3800        let error = block_on(nanmin_builtin(floating(), vec![placeholder(), dimension()]))
3801            .expect_err("strict nanmin typed-integer dimension");
3802        assert_eq!(
3803            error.identifier(),
3804            Some("RunMat:compatibility:NanminTypedIntegerInputExtension")
3805        );
3806    }
3807
3808    #[test]
3809    fn legacy_nan_std_var_reject_integer_data_and_gate_integer_controls() {
3810        for enabled in [false, true] {
3811            let _compat = crate::compatibility::push_runmat_extensions_enabled(enabled);
3812            let error = block_on(nanstd_builtin(
3813                Value::Int(IntValue::I16(7)),
3814                vec![Value::Int(IntValue::U8(1))],
3815            ))
3816            .expect_err("nanstd integer data");
3817            assert!(error.message().contains("integer data inputs"));
3818            let error = block_on(nanvar_builtin(
3819                Value::Int(IntValue::I16(7)),
3820                vec![Value::Int(IntValue::U8(1))],
3821            ))
3822            .expect_err("nanvar integer data");
3823            assert!(error.message().contains("integer data inputs"));
3824        }
3825
3826        let floating = || tensor(vec![1.0, 2.0, 3.0], vec![3, 1]);
3827        {
3828            let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3829            let error = block_on(nanstd_builtin(
3830                floating(),
3831                vec![Value::Int(IntValue::U8(1))],
3832            ))
3833            .expect_err("nanstd strict typed-integer control");
3834            assert_eq!(
3835                error.identifier(),
3836                Some("RunMat:compatibility:NanstdTypedIntegerControlExtension")
3837            );
3838            let error = block_on(nanvar_builtin(
3839                floating(),
3840                vec![Value::Int(IntValue::U8(1))],
3841            ))
3842            .expect_err("nanvar strict typed-integer control");
3843            assert_eq!(
3844                error.identifier(),
3845                Some("RunMat:compatibility:NanvarTypedIntegerControlExtension")
3846            );
3847        }
3848        {
3849            let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3850            block_on(nanstd_builtin(
3851                floating(),
3852                vec![Value::Int(IntValue::U8(1))],
3853            ))
3854            .expect("nanstd RunMat typed-integer control");
3855            block_on(nanvar_builtin(
3856                floating(),
3857                vec![Value::Int(IntValue::U8(1))],
3858            ))
3859            .expect("nanvar RunMat typed-integer control");
3860        }
3861    }
3862
3863    #[cfg(feature = "wgpu")]
3864    #[test]
3865    fn legacy_nan_integer_policy_inspects_resident_dtype_before_dispatch() {
3866        test_support::with_test_provider(|provider| {
3867            let handle = provider
3868                .upload_integer(&HostIntegerTensorView {
3869                    data: HostIntegerDataView::U64(&[1, u64::MAX]),
3870                    shape: &[2, 1],
3871                })
3872                .expect("integer upload");
3873
3874            {
3875                let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3876                let error = block_on(nanmean_builtin(
3877                    Value::GpuTensor(handle.clone()),
3878                    Vec::new(),
3879                ))
3880                .expect_err("strict resident nanmean");
3881                assert_eq!(
3882                    error.identifier(),
3883                    Some("RunMat:compatibility:NanmeanTypedIntegerInputExtension")
3884                );
3885
3886                let error = block_on(nanstd_builtin(Value::GpuTensor(handle.clone()), Vec::new()))
3887                    .expect_err("resident nanstd integer data");
3888                assert!(error.message().contains("integer data inputs"));
3889            }
3890
3891            provider.free(&handle).ok();
3892        });
3893    }
3894
3895    #[cfg(feature = "wgpu")]
3896    #[test]
3897    fn legacy_nan_integer_extensions_preserve_declared_residency() {
3898        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3899        test_support::with_test_provider(|provider| {
3900            type ResidentNanCase = (
3901                &'static str,
3902                fn(Value, Vec<Value>) -> BuiltinResult<Value>,
3903                Vec<f64>,
3904            );
3905            let cases: [ResidentNanCase; 2] = [
3906                (
3907                    "nanmean",
3908                    |value, rest| block_on(nanmean_builtin(value, rest)),
3909                    vec![2.0, 6.0],
3910                ),
3911                (
3912                    "nansum",
3913                    |value, rest| block_on(nansum_builtin(value, rest)),
3914                    vec![4.0, 12.0],
3915                ),
3916            ];
3917            for (name, invoke, expected) in cases {
3918                let handle = provider
3919                    .upload_integer(&HostIntegerTensorView {
3920                        data: HostIntegerDataView::U64(&[1, 3, 5, 7]),
3921                        shape: &[2, 2],
3922                    })
3923                    .expect("integer upload");
3924                let result = invoke(Value::GpuTensor(handle), Vec::new())
3925                    .expect("resident integer extension");
3926                assert!(
3927                    matches!(result, Value::GpuTensor(_)),
3928                    "{name} returned {result:?}"
3929                );
3930                let gathered = test_support::gather(result).expect("gather result");
3931                assert_eq!(gathered.materialize_f64(), expected);
3932            }
3933
3934            let handle = provider
3935                .upload_integer(&HostIntegerTensorView {
3936                    data: HostIntegerDataView::U64(&[1, 3, 5, 7]),
3937                    shape: &[2, 2],
3938                })
3939                .expect("integer upload");
3940            let result = block_on(nanmedian_builtin(Value::GpuTensor(handle), Vec::new()))
3941                .expect("resident integer nanmedian");
3942            assert!(matches!(result, Value::GpuTensor(_)));
3943            let gathered = test_support::gather(result).expect("gather median");
3944            assert_eq!(
3945                gathered.integer_storage(),
3946                Some(&IntegerStorage::U64(vec![2, 6]))
3947            );
3948        });
3949    }
3950
3951    #[test]
3952    fn nanmin_supports_pairwise_form() {
3953        let result = block_on(nanmin_builtin(
3954            tensor(vec![f64::NAN, 4.0, 3.0], vec![1, 3]),
3955            vec![tensor(vec![2.0, f64::NAN, 5.0], vec![1, 3])],
3956        ))
3957        .unwrap();
3958        assert!(matches!(result, Value::Tensor(t) if t.materialize_f64() == vec![2.0, 4.0, 3.0]));
3959    }
3960
3961    #[test]
3962    fn nanmin_pairwise_reads_typed_integer_storage_exactly() {
3963        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3964        let left = Tensor::new_integer(IntegerStorage::U16(vec![9, 4, 3]), vec![1, 3]).unwrap();
3965        let right = tensor(vec![2.0, f64::NAN, 5.0], vec![1, 3]);
3966
3967        let result = block_on(nanmin_builtin(Value::Tensor(left), vec![right])).unwrap();
3968
3969        assert!(
3970            matches!(result, Value::Tensor(tensor) if tensor.materialize_f64() == vec![2.0, 4.0, 3.0])
3971        );
3972
3973        let left = tensor(vec![9.0, 4.0, 3.0], vec![1, 3]);
3974        let right = Tensor::new_integer(IntegerStorage::U8(vec![5]), vec![1, 1]).unwrap();
3975
3976        let result = block_on(nanmin_builtin(left, vec![Value::Tensor(right)])).unwrap();
3977
3978        assert!(
3979            matches!(result, Value::Tensor(tensor) if tensor.materialize_f64() == vec![5.0, 4.0, 3.0])
3980        );
3981    }
3982
3983    #[test]
3984    fn nanmin_pairwise_preserves_exact_wide_same_class_integers() {
3985        let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3986        let left = Tensor::new_integer(
3987            IntegerStorage::U64(vec![(1_u64 << 53) + 1, u64::MAX]),
3988            vec![1, 2],
3989        )
3990        .expect("left uint64");
3991        let right = Tensor::new_integer(
3992            IntegerStorage::U64(vec![1_u64 << 53, u64::MAX - 1]),
3993            vec![1, 2],
3994        )
3995        .expect("right uint64");
3996
3997        let result = block_on(nanmin_builtin(
3998            Value::Tensor(left),
3999            vec![Value::Tensor(right)],
4000        ))
4001        .unwrap();
4002        let Value::Tensor(tensor) = result else {
4003            panic!("expected uint64 tensor");
4004        };
4005        assert_eq!(
4006            tensor.integer_storage(),
4007            Some(&IntegerStorage::U64(vec![1_u64 << 53, u64::MAX - 1]))
4008        );
4009    }
4010
4011    #[test]
4012    fn movmad_computes_centered_median_absolute_deviation() {
4013        let result = block_on(movmad_builtin(
4014            tensor(vec![1.0, 2.0, 100.0, 4.0, 5.0], vec![5, 1]),
4015            Value::Num(3.0),
4016            Vec::new(),
4017        ))
4018        .unwrap();
4019        assert!(matches!(result, Value::Tensor(t) if t.materialize_f64()[2] == 2.0));
4020    }
4021
4022    #[test]
4023    fn movmad_reads_typed_integer_storage_and_returns_double_output() {
4024        let input = Tensor::new_integer(IntegerStorage::I16(vec![1, 2, 100, 4, 5]), vec![5, 1])
4025            .expect("integer movmad input");
4026
4027        let result = block_on(movmad_builtin(
4028            Value::Tensor(input),
4029            Value::Num(3.0),
4030            Vec::new(),
4031        ))
4032        .unwrap();
4033
4034        match result {
4035            Value::Tensor(tensor) => {
4036                assert!(tensor.integer_storage().is_none());
4037                assert_eq!(tensor.materialize_f64(), vec![0.5, 1.0, 2.0, 1.0, 0.5]);
4038            }
4039            other => panic!("expected tensor, got {other:?}"),
4040        }
4041    }
4042
4043    #[test]
4044    fn movmad_accepts_all_integer_classes_into_double_output() {
4045        let storages = vec![
4046            IntegerStorage::I8(vec![1, 2, 5]),
4047            IntegerStorage::I16(vec![1, 2, 5]),
4048            IntegerStorage::I32(vec![1, 2, 5]),
4049            IntegerStorage::I64(vec![1, 2, 5]),
4050            IntegerStorage::U8(vec![1, 2, 5]),
4051            IntegerStorage::U16(vec![1, 2, 5]),
4052            IntegerStorage::U32(vec![1, 2, 5]),
4053            IntegerStorage::U64(vec![1, 2, 5]),
4054        ];
4055        for storage in storages {
4056            let input = Tensor::new_integer(storage, vec![3, 1]).unwrap();
4057            let result = block_on(movmad_builtin(
4058                Value::Tensor(input),
4059                Value::Num(3.0),
4060                Vec::new(),
4061            ))
4062            .unwrap();
4063            assert!(
4064                matches!(result, Value::Tensor(tensor) if tensor.integer_storage().is_none() && tensor.materialize_f64() == vec![0.5, 1.0, 1.5])
4065            );
4066        }
4067    }
4068
4069    #[test]
4070    #[cfg(feature = "wgpu")]
4071    fn movmad_gpu_large_window_follows_compatibility_mode() {
4072        test_support::with_test_provider(|provider| {
4073            let input = Tensor::new((1..=32).map(f64::from).collect(), vec![32, 1]).unwrap();
4074            let handle =
4075                crate::builtins::common::gpu_helpers::upload_tensor(provider, &input).unwrap();
4076            {
4077                let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
4078                let error = block_on(movmad_builtin(
4079                    Value::GpuTensor(handle.clone()),
4080                    Value::Num(32.0),
4081                    Vec::new(),
4082                ))
4083                .unwrap_err();
4084                assert_eq!(
4085                    error.identifier(),
4086                    Some("RunMat:compatibility:MovmadGpuLargeWindowExtension")
4087                );
4088            }
4089            {
4090                let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
4091                let result = block_on(movmad_builtin(
4092                    Value::GpuTensor(handle.clone()),
4093                    Value::Num(32.0),
4094                    Vec::new(),
4095                ))
4096                .unwrap();
4097                assert!(matches!(result, Value::GpuTensor(_)));
4098            }
4099            let _ = provider.free(&handle);
4100        });
4101    }
4102}