1use 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}