Skip to main content

runmat_runtime/builtins/common/
validation.rs

1//! Shared MATLAB argument-validation helpers and callable `mustBe*` builtins.
2
3use runmat_builtins::{
4    BuiltinExtensionDescriptor, BuiltinExtensionMode, BuiltinIntegerAuditDescriptor,
5    BuiltinIntegerAuditKind, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
6    BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
7    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
8    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
9};
10use std::cmp::Ordering;
11use std::path::Path;
12
13use runmat_accelerate_api::{
14    handle_integer_type, handle_is_logical, handle_storage, GpuTensorStorage,
15};
16use runmat_builtins::{
17    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
18    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
19};
20use runmat_macros::runtime_builtin;
21use runmat_value::{
22    CellArray, CharArray, ComplexTensor, IntValue, IntegerStorage, NumericDType, SparseTensor,
23    Value,
24};
25
26use crate::builtins::common::identifiers::is_valid_varname;
27use crate::builtins::common::tensor;
28use crate::builtins::introspection::class::class_name_for_value;
29use crate::builtins::introspection::underlying_type::underlying_type_matches;
30use crate::builtins::logical::rel::integer_comparison::integer_f64_order;
31use crate::builtins::logical::rel::integer_comparison::{
32    try_complex_ordering_comparison, try_real_ordering_comparison, IntegerComparisonError,
33    IntegerComparisonOp,
34};
35use crate::{build_runtime_error, BuiltinResult, RuntimeError};
36
37/// MATLAB stores complex integer arrays, but does not support arithmetic on
38/// them. Arithmetic builtins use this before selecting floating or provider
39/// execution paths so exact integer components are never coerced to `f64`.
40pub fn is_typed_complex_integer(value: &Value) -> bool {
41    matches!(value, Value::ComplexTensor(tensor) if tensor.integer_storage().is_some())
42        || matches!(value, Value::GpuTensor(handle)
43            if handle_integer_type(handle).is_some()
44                && handle_storage(handle) == GpuTensorStorage::ComplexInterleaved)
45}
46
47/// Reject a value that would otherwise enter a floating complex operation.
48pub fn reject_typed_complex_integer(value: &Value, builtin: &str) -> BuiltinResult<()> {
49    if is_typed_complex_integer(value) {
50        return Err(build_runtime_error(format!(
51            "{builtin}: operations involving complex numbers with integer types are not supported"
52        ))
53        .build());
54    }
55    Ok(())
56}
57
58/// Reject operations that would consume the lossy `f64` compatibility view of
59/// a typed complex integer tensor. MATLAB permits storage/inspection of these
60/// values, but not operations on them.
61pub fn reject_typed_complex_integer_tensor(
62    tensor: &ComplexTensor,
63    builtin: &str,
64) -> BuiltinResult<()> {
65    if tensor.integer_storage().is_some() {
66        return Err(build_runtime_error(format!(
67            "{builtin}: operations involving complex numbers with integer types are not supported"
68        ))
69        .build());
70    }
71    Ok(())
72}
73
74#[derive(Debug, Clone, PartialEq)]
75pub enum ValidationAtom {
76    Number(f64),
77    Integer(IntValue),
78    ComplexNumber(f64, f64),
79    ComplexInteger(IntValue, IntValue),
80    Text(String),
81    Bool(bool),
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
85pub struct RangeInclusivity {
86    pub lower: bool,
87    pub upper: bool,
88}
89
90impl RangeInclusivity {
91    pub const CLOSED: Self = Self {
92        lower: true,
93        upper: true,
94    };
95
96    pub const OPEN: Self = Self {
97        lower: false,
98        upper: false,
99    };
100
101    pub const OPEN_LEFT: Self = Self {
102        lower: false,
103        upper: true,
104    };
105
106    pub const OPEN_RIGHT: Self = Self {
107        lower: true,
108        upper: false,
109    };
110}
111
112const VALUE_INPUT: BuiltinParamDescriptor = BuiltinParamDescriptor {
113    name: "A",
114    ty: BuiltinParamType::Any,
115    arity: BuiltinParamArity::Required,
116    default: None,
117    description: "Value to validate.",
118};
119
120const EXTRA_INPUT: BuiltinParamDescriptor = BuiltinParamDescriptor {
121    name: "B",
122    ty: BuiltinParamType::Any,
123    arity: BuiltinParamArity::Variadic,
124    default: None,
125    description: "Additional validator-specific argument.",
126};
127
128const PREDICATE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
129    name: "tf",
130    ty: BuiltinParamType::LogicalArray,
131    arity: BuiltinParamArity::Required,
132    default: None,
133    description: "Validation result.",
134}];
135
136const VALIDATOR_INPUTS: [BuiltinParamDescriptor; 2] = [VALUE_INPUT, EXTRA_INPUT];
137const VALIDATOR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
138    label: "mustBe*(A, ...)",
139    inputs: &VALIDATOR_INPUTS,
140    outputs: &[],
141}];
142
143const PREDICATE_INPUTS: [BuiltinParamDescriptor; 1] = [VALUE_INPUT];
144const ISVARNAME_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
145    label: "tf = isvarname(S)",
146    inputs: &PREDICATE_INPUTS,
147    outputs: &PREDICATE_OUTPUT,
148}];
149
150const NAMEDARGS_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
151    label: "C = namedargs2cell(S)",
152    inputs: &PREDICATE_INPUTS,
153    outputs: &[BuiltinParamDescriptor {
154        name: "C",
155        ty: BuiltinParamType::Any,
156        arity: BuiltinParamArity::Required,
157        default: None,
158        description: "Cell row vector of alternating field names and values.",
159    }],
160}];
161
162const VALIDATION_ERROR_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
163    code: "RM.ARGUMENT_VALIDATION.FAILED",
164    identifier: Some("RunMat:validators:ValidationFailed"),
165    when: "A value does not satisfy the requested validator.",
166    message: "argument validation failed",
167};
168
169const VALIDATION_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
170    code: "RM.ARGUMENT_VALIDATION.INVALID_ARGUMENT",
171    identifier: Some("RunMat:validators:InvalidArgument"),
172    when: "A validator receives an unsupported argument count or argument type.",
173    message: "invalid argument validation input",
174};
175
176const VALIDATION_ERROR_PROVIDER_OWNERSHIP: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
177    code: "RM.ARGUMENT_VALIDATION.PROVIDER_OWNERSHIP_MISMATCH",
178    identifier: Some("RunMat:validators:ProviderOwnershipMismatch"),
179    when: "A resident value has no exact owning provider.",
180    message: "argument validation: no acceleration provider owns the input",
181};
182
183const VALIDATION_ERROR_PROVIDER_PAYLOAD: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
184    code: "RM.ARGUMENT_VALIDATION.PROVIDER_PAYLOAD_MISMATCH",
185    identifier: Some("RunMat:validators:ProviderPayloadMismatch"),
186    when: "A resident value carries contradictory physical class metadata.",
187    message: "argument validation: resident input has contradictory physical class metadata",
188};
189
190const VALIDATION_ERRORS: [BuiltinErrorDescriptor; 4] = [
191    VALIDATION_ERROR_FAILED,
192    VALIDATION_ERROR_INVALID_ARGUMENT,
193    VALIDATION_ERROR_PROVIDER_OWNERSHIP,
194    VALIDATION_ERROR_PROVIDER_PAYLOAD,
195];
196
197pub const VALIDATOR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
198    signatures: &VALIDATOR_SIGNATURES,
199    output_mode: BuiltinOutputMode::Fixed,
200    completion_policy: BuiltinCompletionPolicy::Public,
201    errors: &VALIDATION_ERRORS,
202};
203
204const ALL_INTEGER_CLASSES: &[runmat_builtins::BuiltinIntegerClass] =
205    &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES;
206
207const VALIDATOR_INTEGER_VALUE: [BuiltinIntegerInputCapability; 1] =
208    [BuiltinIntegerInputCapability {
209        name: "A",
210        classes: ALL_INTEGER_CLASSES,
211        availability: BuiltinIntegerInputAvailability::Documented,
212        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
213        notes: "All eight integer classes are validated directly from authoritative class, shape, or exact element storage.",
214    }];
215
216const VALIDATOR_INTEGER_VALUE_AND_BOUND: [BuiltinIntegerInputCapability; 2] = [
217    BuiltinIntegerInputCapability {
218        name: "A",
219        classes: ALL_INTEGER_CLASSES,
220        availability: BuiltinIntegerInputAvailability::Documented,
221        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
222        notes: "All eight integer classes participate in exact mixed numeric comparisons without conversion through binary64.",
223    },
224    BuiltinIntegerInputCapability {
225        name: "bound",
226        classes: ALL_INTEGER_CLASSES,
227        availability: BuiltinIntegerInputAvailability::Documented,
228        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
229        notes: "Integer, logical, single, and double compatible-size bounds retain their native values during comparison.",
230    },
231];
232
233const VALIDATOR_INTEGER_RANGE_INPUTS: [BuiltinIntegerInputCapability; 3] = [
234    BuiltinIntegerInputCapability {
235        name: "A",
236        classes: ALL_INTEGER_CLASSES,
237        availability: BuiltinIntegerInputAvailability::Documented,
238        scalar_double: BuiltinIntegerScalarDoubleRule::Rejected,
239        notes: "All eight integer classes are accepted, and the documented form requires both range bounds to use the same class as A.",
240    },
241    BuiltinIntegerInputCapability {
242        name: "lower",
243        classes: ALL_INTEGER_CLASSES,
244        availability: BuiltinIntegerInputAvailability::Documented,
245        scalar_double: BuiltinIntegerScalarDoubleRule::Rejected,
246        notes: "The lower bound must share A's integer class and is compared exactly.",
247    },
248    BuiltinIntegerInputCapability {
249        name: "upper",
250        classes: ALL_INTEGER_CLASSES,
251        availability: BuiltinIntegerInputAvailability::Documented,
252        scalar_double: BuiltinIntegerScalarDoubleRule::Rejected,
253        notes: "The upper bound must share A's integer class and is compared exactly.",
254    },
255];
256
257const VALIDATOR_INTEGER_MEMBER_INPUTS: [BuiltinIntegerInputCapability; 2] = [
258    BuiltinIntegerInputCapability {
259        name: "A",
260        classes: ALL_INTEGER_CLASSES,
261        availability: BuiltinIntegerInputAvailability::Documented,
262        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
263        notes: "Membership reads authoritative integer elements exactly.",
264    },
265    BuiltinIntegerInputCapability {
266        name: "S",
267        classes: ALL_INTEGER_CLASSES,
268        availability: BuiltinIntegerInputAvailability::Documented,
269        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
270        notes: "Unlike nondouble numeric inputs normally share a class; double retains the documented cross-class exception.",
271    },
272];
273
274macro_rules! validator_integer_capability {
275    ($constant:ident, $form:literal, $inputs:expr, $backend:expr, $overload:expr, $notes:literal) => {
276        pub const $constant: [BuiltinIntegerCapabilityDescriptor; 1] =
277            [BuiltinIntegerCapabilityDescriptor {
278                form: $form,
279                inputs: $inputs,
280                computation_domain: BuiltinIntegerComputationDomain::Predicate,
281                output_class: BuiltinIntegerOutputClassRule::NotApplicable,
282                overflow: BuiltinIntegerOverflowRule::NotApplicable,
283                backend: $backend,
284                overload: $overload,
285                notes: $notes,
286            }];
287    };
288}
289
290validator_integer_capability!(MUST_BE_A_INTEGER_CAPABILITIES, "mustBeA(integer_A, class_names)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::FunctionSpecific, "Class validation uses wrapper identity for explicit gpuArray values and the underlying integer class for ordinary host or internally automatic-resident values; payload data is never converted.");
291validator_integer_capability!(MUST_BE_COLUMN_INTEGER_CAPABILITIES, "mustBeColumn(integer_A)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::StructuralParameter, "Column validation reads shape metadata only and supports resident integer arrays without gathering.");
292validator_integer_capability!(MUST_BE_FINITE_INTEGER_CAPABILITIES, "mustBeFinite(integer_A)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::ElementwiseShapePreserving, "Every native integer value is finite; compatibility is decided from exact class metadata without floating conversion.");
293validator_integer_capability!(MUST_BE_FLOAT_INTEGER_CAPABILITIES, "mustBeFloat(integer_A)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::FunctionSpecific, "Every native integer class fails isfloat-style validation from metadata without gathering or conversion.");
294validator_integer_capability!(MUST_BE_GREATER_THAN_INTEGER_CAPABILITIES, "mustBeGreaterThan(integer_A, numeric_B)", &VALIDATOR_INTEGER_VALUE_AND_BOUND, BuiltinIntegerBackendRule::GatherFallback, BuiltinIntegerOverloadKind::BroadcastCompatible, "The compatibility target's compatible-size comparison is exact across mixed integer and floating classes; resident values use an owner-preserving predicate path.");
295validator_integer_capability!(MUST_BE_GREATER_THAN_OR_EQUAL_INTEGER_CAPABILITIES, "mustBeGreaterThanOrEqual(integer_A, numeric_B)", &VALIDATOR_INTEGER_VALUE_AND_BOUND, BuiltinIntegerBackendRule::GatherFallback, BuiltinIntegerOverloadKind::BroadcastCompatible, "The compatibility target's compatible-size comparison is exact across mixed integer and floating classes; resident values use an owner-preserving predicate path.");
296validator_integer_capability!(MUST_BE_IN_RANGE_INTEGER_CAPABILITIES, "mustBeInRange(integer_A, integer_lower, integer_upper, flags...)", &VALIDATOR_INTEGER_RANGE_INPUTS, BuiltinIntegerBackendRule::GatherFallback, BuiltinIntegerOverloadKind::BroadcastCompatible, "Documented same-class bounds and selectable open/closed endpoints compare exactly without binary64 materialization.");
297validator_integer_capability!(MUST_BE_INTEGER_INTEGER_CAPABILITIES, "mustBeInteger(integer_A)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::ElementwiseShapePreserving, "Every native integer class is integral by construction; this validator checks value integrality rather than integer storage class.");
298validator_integer_capability!(
299    MUST_BE_LESS_THAN_INTEGER_CAPABILITIES,
300    "mustBeLessThan(integer_A, numeric_B)",
301    &VALIDATOR_INTEGER_VALUE_AND_BOUND,
302    BuiltinIntegerBackendRule::GatherFallback,
303    BuiltinIntegerOverloadKind::BroadcastCompatible,
304    "The compatibility target's compatible-size comparison is exact across mixed integer and floating classes."
305);
306validator_integer_capability!(
307    MUST_BE_LESS_THAN_OR_EQUAL_INTEGER_CAPABILITIES,
308    "mustBeLessThanOrEqual(integer_A, numeric_B)",
309    &VALIDATOR_INTEGER_VALUE_AND_BOUND,
310    BuiltinIntegerBackendRule::GatherFallback,
311    BuiltinIntegerOverloadKind::BroadcastCompatible,
312    "The compatibility target's compatible-size comparison is exact across mixed integer and floating classes."
313);
314validator_integer_capability!(MUST_BE_MEMBER_INTEGER_CAPABILITIES, "mustBeMember(integer_A, integer_or_double_S)", &VALIDATOR_INTEGER_MEMBER_INPUTS, BuiltinIntegerBackendRule::GatherFallback, BuiltinIntegerOverloadKind::Multiple, "Exact membership preserves the documented same-class rule and double cross-class exception without rounding wide integers.");
315validator_integer_capability!(MUST_BE_NEGATIVE_INTEGER_CAPABILITIES, "mustBeNegative(integer_A)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::GatherFallback, BuiltinIntegerOverloadKind::ElementwiseShapePreserving, "All elements compare exactly against zero; unsigned nonempty arrays fail and empty arrays pass.");
316validator_integer_capability!(
317    MUST_BE_NON_NAN_INTEGER_CAPABILITIES,
318    "mustBeNonNan(integer_A)",
319    &VALIDATOR_INTEGER_VALUE,
320    BuiltinIntegerBackendRule::HostAndGpu,
321    BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
322    "Native integer storage cannot contain NaN and passes from metadata without conversion."
323);
324validator_integer_capability!(
325    MUST_BE_NONEMPTY_INTEGER_CAPABILITIES,
326    "mustBeNonempty(integer_A)",
327    &VALIDATOR_INTEGER_VALUE,
328    BuiltinIntegerBackendRule::HostAndGpu,
329    BuiltinIntegerOverloadKind::StructuralParameter,
330    "Emptiness is decided from shape metadata without payload access."
331);
332validator_integer_capability!(MUST_BE_NONMISSING_INTEGER_CAPABILITIES, "mustBeNonmissing(integer_A)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::ElementwiseShapePreserving, "Native integer classes have no standard missing representation and therefore pass without conversion; public validator and delegated anymissing type lists are not fully synchronized.");
333validator_integer_capability!(
334    MUST_BE_NONNEGATIVE_INTEGER_CAPABILITIES,
335    "mustBeNonnegative(integer_A)",
336    &VALIDATOR_INTEGER_VALUE,
337    BuiltinIntegerBackendRule::GatherFallback,
338    BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
339    "All elements compare exactly against zero."
340);
341validator_integer_capability!(
342    MUST_BE_NONPOSITIVE_INTEGER_CAPABILITIES,
343    "mustBeNonpositive(integer_A)",
344    &VALIDATOR_INTEGER_VALUE,
345    BuiltinIntegerBackendRule::GatherFallback,
346    BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
347    "All elements compare exactly against zero."
348);
349validator_integer_capability!(MUST_BE_NONSPARSE_INTEGER_CAPABILITIES, "mustBeNonsparse(integer_A)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::StructuralParameter, "Dense host and resident integer arrays pass from storage metadata; sparse integer arrays fail.");
350validator_integer_capability!(
351    MUST_BE_NONZERO_INTEGER_CAPABILITIES,
352    "mustBeNonzero(integer_A)",
353    &VALIDATOR_INTEGER_VALUE,
354    BuiltinIntegerBackendRule::GatherFallback,
355    BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
356    "All integer elements compare exactly against zero without a floating compatibility view."
357);
358validator_integer_capability!(
359    MUST_BE_NUMERIC_INTEGER_CAPABILITIES,
360    "mustBeNumeric(integer_A)",
361    &VALIDATOR_INTEGER_VALUE,
362    BuiltinIntegerBackendRule::HostAndGpu,
363    BuiltinIntegerOverloadKind::FunctionSpecific,
364    "All native integer classes satisfy isnumeric-style validation from class metadata."
365);
366validator_integer_capability!(
367    MUST_BE_NUMERIC_OR_LOGICAL_INTEGER_CAPABILITIES,
368    "mustBeNumericOrLogical(integer_A)",
369    &VALIDATOR_INTEGER_VALUE,
370    BuiltinIntegerBackendRule::HostAndGpu,
371    BuiltinIntegerOverloadKind::FunctionSpecific,
372    "All native integer classes satisfy the numeric branch from class metadata."
373);
374validator_integer_capability!(
375    MUST_BE_POSITIVE_INTEGER_CAPABILITIES,
376    "mustBePositive(integer_A)",
377    &VALIDATOR_INTEGER_VALUE,
378    BuiltinIntegerBackendRule::GatherFallback,
379    BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
380    "All elements compare exactly against zero; empty arrays pass."
381);
382validator_integer_capability!(MUST_BE_REAL_INTEGER_CAPABILITIES, "mustBeReal(integer_A)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::FunctionSpecific, "All eight native integer classes are real by construction; host and resident values pass from authoritative class/storage metadata without floating conversion or payload access.");
383validator_integer_capability!(MUST_BE_SCALAR_OR_EMPTY_INTEGER_CAPABILITIES, "mustBeScalarOrEmpty(integer_A)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::StructuralParameter, "Scalar-or-empty validation reads shape metadata only, including all documented empty integer shapes, without gathering resident payloads.");
384validator_integer_capability!(MUST_BE_SPARSE_INTEGER_CAPABILITIES, "mustBeSparse(integer_A)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::StructuralParameter, "Every empty integer array passes as documented; nonempty dense or resident integer arrays fail from storage metadata, while RunMat-native sparse integer values pass without materialization.");
385validator_integer_capability!(MUST_BE_UNDERLYING_TYPE_INTEGER_CAPABILITIES, "mustBeUnderlyingType(integer_A, typenames)", &VALIDATOR_INTEGER_VALUE, BuiltinIntegerBackendRule::HostAndGpu, BuiltinIntegerOverloadKind::FunctionSpecific, "One or more requested type names are compared with the authoritative signedness and width of host or resident integer storage without reading payload values.");
386
387pub const MUST_BE_VECTOR_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
388    BuiltinIntegerCapabilityDescriptor {
389        form: "mustBeVector(integer_A)",
390        inputs: &VALIDATOR_INTEGER_VALUE,
391        computation_domain: BuiltinIntegerComputationDomain::Predicate,
392        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
393        overflow: BuiltinIntegerOverflowRule::NotApplicable,
394        backend: BuiltinIntegerBackendRule::HostAndGpu,
395        overload: BuiltinIntegerOverloadKind::StructuralParameter,
396        notes: "The documented 1-by-N or N-by-1 rule, including only vector-shaped empties, is decided from host or resident shape metadata without payload access.",
397    },
398    BuiltinIntegerCapabilityDescriptor {
399        form: "mustBeVector(integer_A, \"allow-all-empties\")",
400        inputs: &VALIDATOR_INTEGER_VALUE,
401        computation_domain: BuiltinIntegerComputationDomain::Predicate,
402        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
403        overflow: BuiltinIntegerOverflowRule::NotApplicable,
404        backend: BuiltinIntegerBackendRule::HostAndGpu,
405        overload: BuiltinIntegerOverloadKind::StructuralParameter,
406        notes: "The compatibility target's option additionally accepts every empty integer shape while preserving the ordinary vector rule for nonempty values; callable and arguments-block paths share the same metadata-only predicate.",
407    },
408];
409
410pub const MUST_BE_FILE_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor { kind: BuiltinIntegerAuditKind::NotApplicable, canonical_builtin: None, notes: "mustBeFile is a text/path validator; integer host or resident values reject without numeric conversion or provider access." };
411pub const MUST_BE_FOLDER_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor { kind: BuiltinIntegerAuditKind::NotApplicable, canonical_builtin: None, notes: "mustBeFolder is a text/path validator; integer host or resident values reject without numeric conversion or provider access." };
412pub const MUST_BE_NONZERO_LENGTH_TEXT_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor { kind: BuiltinIntegerAuditKind::NotApplicable, canonical_builtin: None, notes: "mustBeNonzeroLengthText is a text validator; integer host or resident values fail without numeric conversion or provider access." };
413pub const MUST_BE_TEXT_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor { kind: BuiltinIntegerAuditKind::NotApplicable, canonical_builtin: None, notes: "mustBeText is a text validator; integer host or resident values fail before numeric conversion, payload access, or provider lookup." };
414pub const MUST_BE_TEXT_SCALAR_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor { kind: BuiltinIntegerAuditKind::NotApplicable, canonical_builtin: None, notes: "mustBeTextScalar is a text-shape validator; integer host or resident values fail before numeric conversion, payload access, or provider lookup." };
415pub const MUST_BE_VALID_VARIABLE_NAME_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor { kind: BuiltinIntegerAuditKind::NotApplicable, canonical_builtin: None, notes: "mustBeValidVariableName accepts text names only; integer host or resident values fail before numeric conversion, payload access, or provider lookup." };
416pub const NAMEDARGS2CELL_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor { kind: BuiltinIntegerAuditKind::NotApplicable, canonical_builtin: None, notes: "namedargs2cell accepts only a scalar structure. A top-level integer host or resident value rejects without conversion or provider access, while integer values stored in valid structure fields are preserved exactly as ordinary payloads." };
417pub const VALIDATE_FUNCTION_SIGNATURES_JSON_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor { kind: BuiltinIntegerAuditKind::NotApplicable, canonical_builtin: None, notes: "validateFunctionSignaturesJSON accepts text paths in the compatibility target and a text JSON payload in the current RunMat implementation; integer and resident numeric values are not valid in either form and reject without conversion or provider access." };
418
419const MUST_BE_INTEGER_RESIDENT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
420    id: "mustBeInteger.resident-input",
421    mode: BuiltinExtensionMode::RunMatOnly,
422    description: "explicit gpuArray validation is not documented for mustBeInteger",
423    error_identifier: Some("RunMat:compatibility:mustBeIntegerResidentInput"),
424};
425const MUST_BE_FINITE_RESIDENT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
426    id: "mustBeFinite.resident-input",
427    mode: BuiltinExtensionMode::RunMatOnly,
428    description: "explicit gpuArray validation is not documented for mustBeFinite",
429    error_identifier: Some("RunMat:compatibility:mustBeFiniteResidentInput"),
430};
431const MUST_BE_NON_NAN_RESIDENT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
432    id: "mustBeNonNan.resident-input",
433    mode: BuiltinExtensionMode::RunMatOnly,
434    description: "explicit gpuArray validation is not documented for mustBeNonNan",
435    error_identifier: Some("RunMat:compatibility:mustBeNonNanResidentInput"),
436};
437const MUST_BE_NONZERO_RESIDENT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
438    id: "mustBeNonzero.resident-input",
439    mode: BuiltinExtensionMode::RunMatOnly,
440    description: "explicit gpuArray validation is not documented for mustBeNonzero",
441    error_identifier: Some("RunMat:compatibility:mustBeNonzeroResidentInput"),
442};
443pub const MUST_BE_INTEGER_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
444    [MUST_BE_INTEGER_RESIDENT_EXTENSION];
445pub const MUST_BE_FINITE_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
446    [MUST_BE_FINITE_RESIDENT_EXTENSION];
447pub const MUST_BE_NON_NAN_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
448    [MUST_BE_NON_NAN_RESIDENT_EXTENSION];
449pub const MUST_BE_NONZERO_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
450    [MUST_BE_NONZERO_RESIDENT_EXTENSION];
451
452pub const ISVARNAME_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
453    signatures: &ISVARNAME_SIGNATURES,
454    output_mode: BuiltinOutputMode::Fixed,
455    completion_policy: BuiltinCompletionPolicy::Public,
456    errors: &[],
457};
458pub const ISVARNAME_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor =
459    BuiltinIntegerAuditDescriptor {
460        kind: BuiltinIntegerAuditKind::NotApplicable,
461        canonical_builtin: None,
462        notes: "isvarname is a text predicate; integer host or resident values return scalar false without numeric conversion or provider access.",
463    };
464
465pub const NAMEDARGS2CELL_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
466    signatures: &NAMEDARGS_SIGNATURES,
467    output_mode: BuiltinOutputMode::Fixed,
468    completion_policy: BuiltinCompletionPolicy::Public,
469    errors: &VALIDATION_ERRORS,
470};
471
472pub fn validation_error(builtin: &str, detail: impl AsRef<str>) -> RuntimeError {
473    let detail = detail.as_ref();
474    let message = if detail.is_empty() {
475        format!("{builtin}: validation failed")
476    } else {
477        format!("{builtin}: {detail}")
478    };
479    build_runtime_error(message)
480        .with_builtin(builtin)
481        .with_identifier(format!("RunMat:{builtin}:ValidationFailed"))
482        .build()
483}
484
485fn invalid_argument_error(builtin: &str, detail: impl AsRef<str>) -> RuntimeError {
486    let detail = detail.as_ref();
487    let message = if detail.is_empty() {
488        format!("{builtin}: invalid argument")
489    } else {
490        format!("{builtin}: {detail}")
491    };
492    build_runtime_error(message)
493        .with_builtin(builtin)
494        .with_identifier(format!("RunMat:{builtin}:InvalidArgument"))
495        .build()
496}
497
498fn pass() -> BuiltinResult<Value> {
499    Ok(Value::Num(0.0))
500}
501
502fn require_args<'a>(
503    builtin: &str,
504    args: &'a [Value],
505    min: usize,
506    max: usize,
507) -> BuiltinResult<&'a Value> {
508    if args.len() < min || args.len() > max {
509        return Err(invalid_argument_error(builtin, "invalid number of inputs").into());
510    }
511    args.first()
512        .ok_or_else(|| invalid_argument_error(builtin, "missing value").into())
513}
514
515fn require_arg_count(builtin: &str, args: &[Value], min: usize, max: usize) -> BuiltinResult<()> {
516    if args.len() < min || args.len() > max {
517        return Err(invalid_argument_error(builtin, "invalid number of inputs").into());
518    }
519    Ok(())
520}
521
522fn require_exact_arg_count(builtin: &str, args: &[Value], expected: usize) -> BuiltinResult<()> {
523    require_arg_count(builtin, args, expected, expected)
524}
525
526fn check_validator(builtin: &str, ok: bool) -> BuiltinResult<Value> {
527    if ok {
528        pass()
529    } else {
530        Err(validation_error(builtin, "value does not satisfy validator").into())
531    }
532}
533
534pub fn dispatch_validator(builtin: &str, args: Vec<Value>) -> BuiltinResult<Value> {
535    futures::executor::block_on(dispatch_validator_async(builtin, args))
536}
537
538pub async fn dispatch_validator_async(builtin: &str, args: Vec<Value>) -> BuiltinResult<Value> {
539    let value = require_args(builtin, &args, 1, usize::MAX)?;
540    if matches!(value, Value::GpuTensor(_))
541        && matches!(
542            builtin,
543            "mustBeText"
544                | "mustBeTextScalar"
545                | "mustBeValidVariableName"
546                | "validateFunctionSignaturesJSON"
547        )
548    {
549        require_exact_arg_count(builtin, &args, 1)?;
550        return Err(
551            build_runtime_error(format!("{builtin}: value does not satisfy validator"))
552                .with_builtin(builtin)
553                .with_identifier(format!("RunMat:{builtin}:ValidationFailed"))
554                .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
555                .build()
556                .into(),
557        );
558    }
559    validate_resident_metadata(value)?;
560    match builtin {
561        "mustBeA" => {
562            require_exact_arg_count(builtin, &args, 2)?;
563            check_validator(builtin, must_be_a(value, type_names_arg(&args, 1)?)?)
564        }
565        "mustBeColumn" => {
566            require_exact_arg_count(builtin, &args, 1)?;
567            check_validator(builtin, value_is_column(value))
568        }
569        "mustBeFile" => check_validator(builtin, {
570            require_exact_arg_count(builtin, &args, 1)?;
571            value_texts(value)?.iter().all(|p| Path::new(p).is_file())
572        }),
573        "mustBeFinite" => {
574            require_exact_arg_count(builtin, &args, 1)?;
575            ensure_resident_extension(value, builtin)?;
576            check_validator(builtin, value_is_finite_async(value).await?)
577        }
578        "mustBeFloat" => {
579            require_exact_arg_count(builtin, &args, 1)?;
580            check_validator(builtin, value_is_float(value))
581        }
582        "mustBeFolder" => check_validator(builtin, {
583            require_exact_arg_count(builtin, &args, 1)?;
584            value_texts(value)?.iter().all(|p| Path::new(p).is_dir())
585        }),
586        "mustBeGreaterThan" => {
587            require_exact_arg_count(builtin, &args, 2)?;
588            check_validator(
589                builtin,
590                value_is_greater_than_values_async(value, &args[1]).await?,
591            )
592        }
593        "mustBeGreaterThanOrEqual" => {
594            require_exact_arg_count(builtin, &args, 2)?;
595            check_validator(
596                builtin,
597                value_is_greater_than_or_equal_values_async(value, &args[1]).await?,
598            )
599        }
600        "mustBeInRange" => {
601            require_arg_count(builtin, &args, 3, 5)?;
602            let inclusivity = range_inclusivity_arg(builtin, &args[3..])?;
603            check_validator(
604                builtin,
605                value_is_in_range_documented_async(value, &args[1], &args[2], inclusivity).await?,
606            )
607        }
608        "mustBeInteger" => {
609            require_exact_arg_count(builtin, &args, 1)?;
610            ensure_resident_extension(value, builtin)?;
611            check_validator(builtin, value_is_integer_async(value).await?)
612        }
613        "mustBeLessThan" => {
614            require_exact_arg_count(builtin, &args, 2)?;
615            check_validator(
616                builtin,
617                value_is_less_than_values_async(value, &args[1]).await?,
618            )
619        }
620        "mustBeLessThanOrEqual" => {
621            require_exact_arg_count(builtin, &args, 2)?;
622            check_validator(
623                builtin,
624                value_is_less_than_or_equal_values_async(value, &args[1]).await?,
625            )
626        }
627        "mustBeMember" => {
628            require_exact_arg_count(builtin, &args, 2)?;
629            check_validator(builtin, value_is_member_async(value, &args[1]).await?)
630        }
631        "mustBeNegative" => {
632            require_exact_arg_count(builtin, &args, 1)?;
633            check_validator(builtin, value_is_negative_async(value).await?)
634        }
635        "mustBeNonempty" => {
636            require_exact_arg_count(builtin, &args, 1)?;
637            check_validator(builtin, !value_is_empty(value))
638        }
639        "mustBeNonmissing" => {
640            require_exact_arg_count(builtin, &args, 1)?;
641            check_validator(builtin, value_is_nonmissing_async(value).await?)
642        }
643        "mustBeNonNan" => {
644            require_exact_arg_count(builtin, &args, 1)?;
645            ensure_resident_extension(value, builtin)?;
646            check_validator(builtin, value_is_non_nan_async(value).await?)
647        }
648        "mustBeNonnegative" => {
649            require_exact_arg_count(builtin, &args, 1)?;
650            check_validator(builtin, value_is_nonnegative_async(value).await?)
651        }
652        "mustBeNonpositive" => {
653            require_exact_arg_count(builtin, &args, 1)?;
654            check_validator(builtin, value_is_nonpositive_async(value).await?)
655        }
656        "mustBeNonsparse" => {
657            require_exact_arg_count(builtin, &args, 1)?;
658            check_validator(builtin, !matches!(value, Value::SparseTensor(_)))
659        }
660        "mustBeNonzero" => {
661            require_exact_arg_count(builtin, &args, 1)?;
662            ensure_resident_extension(value, builtin)?;
663            check_validator(builtin, value_is_nonzero_async(value).await?)
664        }
665        "mustBeNonzeroLengthText" => {
666            require_exact_arg_count(builtin, &args, 1)?;
667            check_validator(builtin, value_is_nonzero_length_text(value))
668        }
669        "mustBeNumeric" => {
670            require_exact_arg_count(builtin, &args, 1)?;
671            check_validator(builtin, value_is_numeric(value))
672        }
673        "mustBeNumericOrLogical" => {
674            require_exact_arg_count(builtin, &args, 1)?;
675            check_validator(builtin, value_is_numeric_or_logical(value))
676        }
677        "mustBePositive" => {
678            require_exact_arg_count(builtin, &args, 1)?;
679            check_validator(builtin, value_is_positive_async(value).await?)
680        }
681        "mustBeReal" => {
682            require_exact_arg_count(builtin, &args, 1)?;
683            check_validator(builtin, value_is_real_async(value).await?)
684        }
685        "mustBeScalarOrEmpty" => {
686            require_exact_arg_count(builtin, &args, 1)?;
687            check_validator(builtin, value_is_scalar_or_empty(value))
688        }
689        "mustBeSparse" => {
690            require_exact_arg_count(builtin, &args, 1)?;
691            check_validator(
692                builtin,
693                value_is_empty(value) || matches!(value, Value::SparseTensor(_)),
694            )
695        }
696        "mustBeText" => {
697            require_exact_arg_count(builtin, &args, 1)?;
698            check_validator(builtin, value_is_text(value))
699        }
700        "mustBeTextScalar" => {
701            require_exact_arg_count(builtin, &args, 1)?;
702            check_validator(builtin, value_is_text_scalar(value))
703        }
704        "mustBeUnderlyingType" => {
705            require_exact_arg_count(builtin, &args, 2)?;
706            check_validator(
707                builtin,
708                value_underlying_type_matches(value, type_names_arg(&args, 1)?)?,
709            )
710        }
711        "mustBeValidVariableName" => {
712            require_exact_arg_count(builtin, &args, 1)?;
713            check_validator(
714                builtin,
715                value_texts(value)?
716                    .iter()
717                    .all(|name| is_valid_varname(name)),
718            )
719        }
720        "mustBeVector" => {
721            require_arg_count(builtin, &args, 1, 2)?;
722            let allow_all_empties = match args.get(1) {
723                None => false,
724                Some(flag)
725                    if text_scalar_arg(builtin, flag)?
726                        .eq_ignore_ascii_case("allow-all-empties") =>
727                {
728                    true
729                }
730                Some(_) => {
731                    return Err(invalid_argument_error(
732                        builtin,
733                        "option must be 'allow-all-empties'",
734                    )
735                    .into())
736                }
737            };
738            check_validator(
739                builtin,
740                value_satisfies_vector_validator(value, allow_all_empties)?,
741            )
742        }
743        "validateFunctionSignaturesJSON" => {
744            require_exact_arg_count(builtin, &args, 1)?;
745            validate_function_signatures_json(value)?;
746            pass()
747        }
748        _ => Err(invalid_argument_error(builtin, "unknown validator").into()),
749    }
750}
751
752pub fn value_shape_2d(value: &Value) -> (usize, usize) {
753    match value {
754        Value::Tensor(t) => (t.rows, t.cols),
755        Value::SparseTensor(t) => (t.rows, t.cols),
756        Value::ComplexTensor(t) => (t.rows, t.cols),
757        Value::LogicalArray(a) => {
758            let rows = a.shape.first().copied().unwrap_or(0);
759            let cols = a.shape.get(1).copied().unwrap_or(1);
760            (rows, cols)
761        }
762        Value::Cell(c) => (c.rows, c.cols),
763        Value::CharArray(c) => (c.rows, c.cols),
764        Value::StringArray(s) => (s.rows, s.cols),
765        Value::GpuTensor(handle) => {
766            let rows = handle.shape.first().copied().unwrap_or(1);
767            let cols = handle.shape.get(1).copied().unwrap_or(1);
768            (rows, cols)
769        }
770        _ => (1, 1),
771    }
772}
773
774pub fn value_is_empty(value: &Value) -> bool {
775    match value {
776        Value::Tensor(t) => t.is_empty(),
777        Value::SparseTensor(t) => t.rows == 0 || t.cols == 0,
778        Value::ComplexTensor(t) => tensor::complex_tensor_element_len(t) == 0,
779        Value::LogicalArray(a) => a.data.is_empty(),
780        Value::StringArray(s) => s.data.is_empty(),
781        Value::CharArray(c) => c.rows == 0 || c.cols == 0,
782        Value::Cell(c) => c.data.is_empty(),
783        Value::GpuTensor(handle) => handle.shape.contains(&0),
784        _ => false,
785    }
786}
787
788pub fn value_is_finite(value: &Value) -> bool {
789    match value {
790        Value::Num(v) => v.is_finite(),
791        Value::Int(_) | Value::Bool(_) => true,
792        Value::Complex(re, im) => re.is_finite() && im.is_finite(),
793        Value::Tensor(t) if t.integer_storage().is_some() => true,
794        Value::Tensor(t) => tensor::tensor_values_f64_cow(t)
795            .iter()
796            .all(|v| v.is_finite()),
797        Value::SparseTensor(t) if t.integer_storage().is_some() => true,
798        Value::SparseTensor(t) => t.materialize_f64().iter().all(|v| v.is_finite()),
799        Value::ComplexTensor(t) if t.integer_storage().is_some() => true,
800        Value::ComplexTensor(t) => t
801            .materialize_f64()
802            .iter()
803            .all(|(re, im)| re.is_finite() && im.is_finite()),
804        Value::LogicalArray(_) | Value::CharArray(_) => true,
805        Value::GpuTensor(_) => true,
806        _ => false,
807    }
808}
809
810pub fn value_is_numeric(value: &Value) -> bool {
811    match value {
812        Value::Num(_)
813        | Value::Int(_)
814        | Value::Complex(_, _)
815        | Value::Tensor(_)
816        | Value::SparseTensor(_)
817        | Value::ComplexTensor(_) => true,
818        Value::GpuTensor(handle) => !handle_is_logical(handle),
819        _ => false,
820    }
821}
822
823pub fn value_is_float(value: &Value) -> bool {
824    match value {
825        Value::Num(_) | Value::Complex(_, _) => true,
826        Value::ComplexTensor(tensor) => tensor.integer_storage().is_none(),
827        Value::Tensor(t) => matches!(t.numeric_dtype(), NumericDType::F64 | NumericDType::F32),
828        Value::SparseTensor(tensor) => tensor.integer_storage().is_none(),
829        Value::GpuTensor(handle) => {
830            !handle_is_logical(handle) && handle_integer_type(handle).is_none()
831        }
832        _ => false,
833    }
834}
835
836pub fn value_is_numeric_or_logical(value: &Value) -> bool {
837    value_is_numeric(value) || value_has_logical_class(value)
838}
839
840pub fn value_is_text(value: &Value) -> bool {
841    match value {
842        Value::String(_) | Value::StringArray(_) => true,
843        Value::CharArray(chars) => chars.rows == 1,
844        Value::Cell(cell) => cell.data.iter().all(value_is_text),
845        _ => false,
846    }
847}
848
849pub fn value_is_text_scalar(value: &Value) -> bool {
850    match value {
851        Value::String(_) => true,
852        Value::StringArray(strings) => {
853            strings.data.len() == 1 && strings.rows == 1 && strings.cols == 1
854        }
855        Value::CharArray(chars) => chars.rows == 1,
856        _ => false,
857    }
858}
859
860pub fn value_is_nonzero_length_text(value: &Value) -> bool {
861    if !value_is_text(value) {
862        return false;
863    }
864    match value {
865        Value::String(s) => !s.is_empty(),
866        Value::StringArray(s) => s.data.iter().all(|value| !value.is_empty()),
867        Value::CharArray(c) => c.rows == 1 && c.cols > 0,
868        Value::Cell(c) => c.data.iter().all(value_is_nonzero_length_text),
869        _ => false,
870    }
871}
872
873pub fn value_is_scalar_or_empty(value: &Value) -> bool {
874    let (rows, cols) = value_shape_2d(value);
875    (rows == 1 && cols == 1) || rows == 0 || cols == 0
876}
877
878pub fn value_is_real(value: &Value) -> bool {
879    if value_is_empty(value) {
880        return true;
881    }
882    match value {
883        Value::Complex(_, im) => *im == 0.0,
884        Value::ComplexTensor(t) if t.integer_storage().is_some() => t
885            .integer_storage()
886            .as_ref()
887            .expect("checked integer complex storage")
888            .imag
889            .exact_values()
890            .iter()
891            .all(IntValue::is_zero),
892        Value::ComplexTensor(t) => t.materialize_f64().iter().all(|(_, im)| *im == 0.0),
893        Value::GpuTensor(handle) => handle_storage(handle) == GpuTensorStorage::Real,
894        Value::Num(_)
895        | Value::Int(_)
896        | Value::Bool(_)
897        | Value::Tensor(_)
898        | Value::SparseTensor(_)
899        | Value::LogicalArray(_)
900        | Value::CharArray(_) => true,
901        _ => false,
902    }
903}
904
905pub async fn value_is_real_async(value: &Value) -> BuiltinResult<bool> {
906    if matches!(value, Value::GpuTensor(handle) if handle_storage(handle) == GpuTensorStorage::ComplexInterleaved)
907    {
908        return Ok(value_is_real(&host_value(value).await?));
909    }
910    Ok(value_is_real(value))
911}
912
913pub fn value_is_integer(value: &Value) -> bool {
914    match value {
915        Value::Int(_) => true,
916        Value::Bool(_) | Value::LogicalArray(_) | Value::CharArray(_) => true,
917        Value::Num(v) => v.is_finite() && v.fract() == 0.0,
918        Value::Tensor(t) if t.integer_storage().is_some() => true,
919        Value::Tensor(t) => tensor::tensor_values_f64_cow(t)
920            .iter()
921            .all(|v| v.is_finite() && v.fract() == 0.0),
922        Value::SparseTensor(t) if t.integer_storage().is_some() => true,
923        Value::SparseTensor(t) => t
924            .materialize_f64()
925            .iter()
926            .all(|v| v.is_finite() && v.fract() == 0.0),
927        Value::Complex(re, im) => {
928            re.is_finite() && re.fract() == 0.0 && im.is_finite() && im.fract() == 0.0
929        }
930        Value::ComplexTensor(t) if t.integer_storage().is_some() => true,
931        Value::ComplexTensor(t) => t.materialize_f64().iter().all(|(re, im)| {
932            re.is_finite() && re.fract() == 0.0 && im.is_finite() && im.fract() == 0.0
933        }),
934        Value::GpuTensor(handle) => {
935            handle_is_logical(handle) || handle_integer_type(handle).is_some()
936        }
937        _ => false,
938    }
939}
940
941pub fn value_is_non_nan(value: &Value) -> bool {
942    match value {
943        Value::Num(v) => !v.is_nan(),
944        Value::Complex(re, im) => !re.is_nan() && !im.is_nan(),
945        Value::Tensor(t) if t.integer_storage().is_some() => true,
946        Value::Tensor(t) => tensor::tensor_values_f64_cow(t).iter().all(|v| !v.is_nan()),
947        Value::SparseTensor(t) if t.integer_storage().is_some() => true,
948        Value::SparseTensor(t) => t.materialize_f64().iter().all(|v| !v.is_nan()),
949        Value::ComplexTensor(t) if t.integer_storage().is_some() => true,
950        Value::ComplexTensor(t) => t
951            .materialize_f64()
952            .iter()
953            .all(|(re, im)| !re.is_nan() && !im.is_nan()),
954        Value::Cell(c) => c.data.iter().all(value_is_non_nan),
955        _ => true,
956    }
957}
958
959pub fn value_is_nonmissing(value: &Value) -> bool {
960    value_is_non_nan(value)
961}
962
963pub fn value_is_positive(value: &Value) -> bool {
964    if let Some(result) = complex_real_values_all(value, |v| v > 0.0, int_is_positive) {
965        return result;
966    }
967    if let Some(result) = exact_integer_values_all(value, int_is_positive) {
968        return result;
969    }
970    numeric_values_all(value, |v| v > 0.0)
971}
972
973pub fn value_is_negative(value: &Value) -> bool {
974    if let Some(result) = complex_real_values_all(value, |v| v < 0.0, int_is_negative) {
975        return result;
976    }
977    if let Some(result) = exact_integer_values_all(value, int_is_negative) {
978        return result;
979    }
980    numeric_values_all(value, |v| v < 0.0)
981}
982
983pub fn value_is_nonnegative(value: &Value) -> bool {
984    if let Some(result) = complex_real_values_all(value, |v| v >= 0.0, int_is_nonnegative) {
985        return result;
986    }
987    if let Some(result) = exact_integer_values_all(value, int_is_nonnegative) {
988        return result;
989    }
990    numeric_values_all(value, |v| v >= 0.0)
991}
992
993pub fn value_is_nonpositive(value: &Value) -> bool {
994    if let Some(result) = complex_real_values_all(value, |v| v <= 0.0, int_is_nonpositive) {
995        return result;
996    }
997    if let Some(result) = exact_integer_values_all(value, int_is_nonpositive) {
998        return result;
999    }
1000    numeric_values_all(value, |v| v <= 0.0)
1001}
1002
1003pub fn value_is_nonzero(value: &Value) -> bool {
1004    match value {
1005        Value::Complex(re, im) => *re != 0.0 || *im != 0.0,
1006        Value::ComplexTensor(t) if t.integer_storage().is_some() => {
1007            let integer_data = t.integer_storage().expect("checked integer data");
1008            (0..integer_data.len()).all(|index| integer_data.is_nonzero_at(index).unwrap_or(false))
1009        }
1010        Value::ComplexTensor(t) => t
1011            .materialize_f64()
1012            .iter()
1013            .all(|(re, im)| *re != 0.0 || *im != 0.0),
1014        _ => {
1015            if let Some(result) = exact_integer_values_all(value, |integer| !integer.is_zero()) {
1016                return result;
1017            }
1018            numeric_values_all(value, |v| v != 0.0)
1019        }
1020    }
1021}
1022
1023pub fn value_is_greater_than_or_equal(value: &Value, threshold: f64) -> bool {
1024    if let Some(result) = exact_integer_values_all(value, |integer| {
1025        int_f64_matches(integer, threshold, |ordering| ordering >= Ordering::Equal)
1026    }) {
1027        return result;
1028    }
1029    numeric_values_all(value, |v| v.is_finite() && v >= threshold)
1030}
1031
1032pub fn value_is_less_than_or_equal(value: &Value, threshold: f64) -> bool {
1033    if let Some(result) = exact_integer_values_all(value, |integer| {
1034        int_f64_matches(integer, threshold, |ordering| ordering <= Ordering::Equal)
1035    }) {
1036        return result;
1037    }
1038    numeric_values_all(value, |v| v.is_finite() && v <= threshold)
1039}
1040
1041pub fn value_is_greater_than(value: &Value, threshold: f64) -> bool {
1042    if let Some(result) = exact_integer_values_all(value, |integer| {
1043        int_f64_matches(integer, threshold, |ordering| ordering == Ordering::Greater)
1044    }) {
1045        return result;
1046    }
1047    numeric_values_all(value, |v| v.is_finite() && v > threshold)
1048}
1049
1050pub fn value_is_less_than(value: &Value, threshold: f64) -> bool {
1051    if let Some(result) = exact_integer_values_all(value, |integer| {
1052        int_f64_matches(integer, threshold, |ordering| ordering == Ordering::Less)
1053    }) {
1054        return result;
1055    }
1056    numeric_values_all(value, |v| v.is_finite() && v < threshold)
1057}
1058
1059pub fn value_is_in_range(
1060    value: &Value,
1061    lower: f64,
1062    upper: f64,
1063    inclusivity: RangeInclusivity,
1064) -> bool {
1065    if let Some(result) = exact_integer_values_all(value, |integer| {
1066        let lower_ok = int_f64_matches(integer, lower, |ordering| {
1067            if inclusivity.lower {
1068                ordering >= Ordering::Equal
1069            } else {
1070                ordering == Ordering::Greater
1071            }
1072        });
1073        let upper_ok = int_f64_matches(integer, upper, |ordering| {
1074            if inclusivity.upper {
1075                ordering <= Ordering::Equal
1076            } else {
1077                ordering == Ordering::Less
1078            }
1079        });
1080        lower_ok && upper_ok
1081    }) {
1082        return result;
1083    }
1084    numeric_values_all(value, |v| {
1085        v.is_finite()
1086            && if inclusivity.lower {
1087                v >= lower
1088            } else {
1089                v > lower
1090            }
1091            && if inclusivity.upper {
1092                v <= upper
1093            } else {
1094                v < upper
1095            }
1096    })
1097}
1098
1099async fn resident_host_value(value: &Value) -> BuiltinResult<Option<Value>> {
1100    let Value::GpuTensor(handle) = value else {
1101        return Ok(None);
1102    };
1103    let owner = crate::builtins::common::gpu_helpers::exact_provider_for_handle(handle)
1104        .ok_or_else(|| {
1105            build_runtime_error(VALIDATION_ERROR_PROVIDER_OWNERSHIP.message)
1106                .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
1107                .build()
1108        })?;
1109    crate::builtins::common::gpu_helpers::download_value_preserving_residency_async(owner, handle)
1110        .await
1111        .map(Some)
1112}
1113
1114async fn host_value(value: &Value) -> BuiltinResult<Value> {
1115    validate_resident_metadata(value)?;
1116    Ok(match resident_host_value(value).await? {
1117        Some(value) => value,
1118        None => value.clone(),
1119    })
1120}
1121
1122fn explicit_resident(value: &Value) -> bool {
1123    matches!(value, Value::GpuTensor(handle) if runmat_accelerate_api::handle_provenance(handle) == Some(runmat_accelerate_api::GpuHandleProvenance::Explicit))
1124}
1125
1126pub fn ensure_resident_extension(value: &Value, builtin: &str) -> BuiltinResult<()> {
1127    if !explicit_resident(value) {
1128        return Ok(());
1129    }
1130    let extension = match builtin {
1131        "mustBeFinite" => &MUST_BE_FINITE_RESIDENT_EXTENSION,
1132        "mustBeInteger" => &MUST_BE_INTEGER_RESIDENT_EXTENSION,
1133        "mustBeNonNan" => &MUST_BE_NON_NAN_RESIDENT_EXTENSION,
1134        "mustBeNonzero" => &MUST_BE_NONZERO_RESIDENT_EXTENSION,
1135        _ => return Ok(()),
1136    };
1137    crate::compatibility::ensure_builtin_extension_enabled(extension, builtin)
1138}
1139
1140pub fn validate_resident_metadata(value: &Value) -> BuiltinResult<()> {
1141    let Value::GpuTensor(handle) = value else {
1142        return Ok(());
1143    };
1144    crate::builtins::common::gpu_helpers::exact_provider_for_handle(handle).ok_or_else(|| {
1145        build_runtime_error(VALIDATION_ERROR_PROVIDER_OWNERSHIP.message)
1146            .with_identifier(
1147                VALIDATION_ERROR_PROVIDER_OWNERSHIP
1148                    .identifier
1149                    .expect("validator provider-ownership descriptor identifier"),
1150            )
1151            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
1152            .build()
1153    })?;
1154    let storage = handle_storage(handle);
1155    let integer = handle_integer_type(handle);
1156    let logical = handle_is_logical(handle);
1157    let precision = runmat_accelerate_api::handle_precision(handle);
1158    let expected_class =
1159        crate::builtins::common::gpu_helpers::expected_gpu_class_name(precision, integer, logical);
1160    let class_valid = runmat_accelerate_api::handle_class_name(handle)
1161        .as_deref()
1162        .is_none_or(|class_name| Some(class_name) == expected_class);
1163    let physical_valid = if integer.is_some() {
1164        storage == GpuTensorStorage::Real && precision.is_none() && !logical
1165    } else if logical {
1166        storage == GpuTensorStorage::Real && precision.is_some()
1167    } else {
1168        precision.is_some()
1169    };
1170    if !physical_valid || !class_valid {
1171        return Err(
1172            build_runtime_error(VALIDATION_ERROR_PROVIDER_PAYLOAD.message)
1173                .with_identifier(
1174                    VALIDATION_ERROR_PROVIDER_PAYLOAD
1175                        .identifier
1176                        .expect("validator provider-payload descriptor identifier"),
1177                )
1178                .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
1179                .build()
1180                .into(),
1181        );
1182    }
1183    Ok(())
1184}
1185
1186pub async fn value_is_finite_async(value: &Value) -> BuiltinResult<bool> {
1187    if matches!(value, Value::GpuTensor(handle) if handle_integer_type(handle).is_some() || handle_is_logical(handle))
1188    {
1189        return Ok(true);
1190    }
1191    let value = host_value(value).await?;
1192    Ok(value_is_finite(&value))
1193}
1194
1195pub async fn value_is_integer_async(value: &Value) -> BuiltinResult<bool> {
1196    if matches!(value, Value::GpuTensor(handle) if handle_integer_type(handle).is_some() || handle_is_logical(handle))
1197    {
1198        return Ok(true);
1199    }
1200    let value = host_value(value).await?;
1201    Ok(value_is_integer(&value))
1202}
1203
1204pub async fn value_is_non_nan_async(value: &Value) -> BuiltinResult<bool> {
1205    if matches!(value, Value::GpuTensor(handle) if handle_integer_type(handle).is_some() || handle_is_logical(handle))
1206    {
1207        return Ok(true);
1208    }
1209    let value = host_value(value).await?;
1210    Ok(value_is_non_nan(&value))
1211}
1212
1213pub async fn value_is_nonmissing_async(value: &Value) -> BuiltinResult<bool> {
1214    if matches!(value, Value::GpuTensor(handle) if handle_integer_type(handle).is_some() || handle_is_logical(handle))
1215    {
1216        return Ok(true);
1217    }
1218    let value = host_value(value).await?;
1219    Ok(value_is_nonmissing(&value))
1220}
1221
1222pub async fn value_is_positive_async(value: &Value) -> BuiltinResult<bool> {
1223    value_is_ordered_against_zero_async(value, IntegerComparisonOp::Gt).await
1224}
1225
1226pub async fn value_is_negative_async(value: &Value) -> BuiltinResult<bool> {
1227    value_is_ordered_against_zero_async(value, IntegerComparisonOp::Lt).await
1228}
1229
1230pub async fn value_is_nonnegative_async(value: &Value) -> BuiltinResult<bool> {
1231    value_is_ordered_against_zero_async(value, IntegerComparisonOp::Ge).await
1232}
1233
1234pub async fn value_is_nonpositive_async(value: &Value) -> BuiltinResult<bool> {
1235    value_is_ordered_against_zero_async(value, IntegerComparisonOp::Le).await
1236}
1237
1238pub async fn value_is_nonzero_async(value: &Value) -> BuiltinResult<bool> {
1239    let value = host_value(value).await?;
1240    Ok(value_is_nonzero(&value))
1241}
1242
1243async fn value_is_ordered_against_zero_async(
1244    value: &Value,
1245    operation: IntegerComparisonOp,
1246) -> BuiltinResult<bool> {
1247    let value = host_value(value).await?;
1248    comparison_all_normalized(&value, &Value::Num(0.0), operation)
1249}
1250
1251pub async fn value_is_greater_than_values_async(
1252    value: &Value,
1253    bound: &Value,
1254) -> BuiltinResult<bool> {
1255    compare_values_async(value, bound, IntegerComparisonOp::Gt).await
1256}
1257
1258pub async fn value_is_greater_than_or_equal_values_async(
1259    value: &Value,
1260    bound: &Value,
1261) -> BuiltinResult<bool> {
1262    compare_values_async(value, bound, IntegerComparisonOp::Ge).await
1263}
1264
1265pub async fn value_is_less_than_values_async(value: &Value, bound: &Value) -> BuiltinResult<bool> {
1266    compare_values_async(value, bound, IntegerComparisonOp::Lt).await
1267}
1268
1269pub async fn value_is_less_than_or_equal_values_async(
1270    value: &Value,
1271    bound: &Value,
1272) -> BuiltinResult<bool> {
1273    compare_values_async(value, bound, IntegerComparisonOp::Le).await
1274}
1275
1276async fn compare_values_async(
1277    value: &Value,
1278    bound: &Value,
1279    operation: IntegerComparisonOp,
1280) -> BuiltinResult<bool> {
1281    let value = host_value(value).await?;
1282    let bound = host_value(bound).await?;
1283    comparison_all_normalized(&value, &bound, operation)
1284}
1285
1286pub async fn value_is_in_range_values_async(
1287    value: &Value,
1288    lower: &Value,
1289    upper: &Value,
1290    inclusivity: RangeInclusivity,
1291) -> BuiltinResult<bool> {
1292    let value = host_value(value).await?;
1293    let lower = host_value(lower).await?;
1294    let upper = host_value(upper).await?;
1295    let lower_op = if inclusivity.lower {
1296        IntegerComparisonOp::Ge
1297    } else {
1298        IntegerComparisonOp::Gt
1299    };
1300    let upper_op = if inclusivity.upper {
1301        IntegerComparisonOp::Le
1302    } else {
1303        IntegerComparisonOp::Lt
1304    };
1305    Ok(comparison_all_normalized(&value, &lower, lower_op)?
1306        && comparison_all_normalized(&value, &upper, upper_op)?)
1307}
1308
1309pub async fn value_is_in_range_documented_async(
1310    value: &Value,
1311    lower: &Value,
1312    upper: &Value,
1313    inclusivity: RangeInclusivity,
1314) -> BuiltinResult<bool> {
1315    ensure_same_numeric_class("mustBeInRange", value, lower)?;
1316    ensure_same_numeric_class("mustBeInRange", value, upper)?;
1317    value_is_in_range_values_async(value, lower, upper, inclusivity).await
1318}
1319
1320fn comparison_all_normalized(
1321    lhs: &Value,
1322    rhs: &Value,
1323    operation: IntegerComparisonOp,
1324) -> BuiltinResult<bool> {
1325    if let Value::SparseTensor(sparse) = lhs {
1326        if let Some(rhs) = scalar_numeric_value(rhs) {
1327            return sparse_scalar_comparison_all(sparse, &rhs, true, operation);
1328        }
1329    }
1330    if let Value::SparseTensor(sparse) = rhs {
1331        if let Some(lhs) = scalar_numeric_value(lhs) {
1332            return sparse_scalar_comparison_all(sparse, &lhs, false, operation);
1333        }
1334    }
1335    let lhs_dense;
1336    let rhs_dense;
1337    let lhs = if let Value::SparseTensor(sparse) = lhs {
1338        lhs_dense = Value::Tensor(
1339            sparse
1340                .to_dense()
1341                .map_err(|error| invalid_argument_error("argumentValidation", error))?,
1342        );
1343        &lhs_dense
1344    } else {
1345        lhs
1346    };
1347    let rhs = if let Value::SparseTensor(sparse) = rhs {
1348        rhs_dense = Value::Tensor(
1349            sparse
1350                .to_dense()
1351                .map_err(|error| invalid_argument_error("argumentValidation", error))?,
1352        );
1353        &rhs_dense
1354    } else {
1355        rhs
1356    };
1357    comparison_all(lhs, rhs, operation)
1358}
1359
1360fn comparison_all(lhs: &Value, rhs: &Value, operation: IntegerComparisonOp) -> BuiltinResult<bool> {
1361    let map_error = |error| match error {
1362        IntegerComparisonError::SizeMismatch => invalid_argument_error(
1363            "argumentValidation",
1364            "numeric inputs are not compatible for implicit expansion",
1365        ),
1366        IntegerComparisonError::Internal => {
1367            invalid_argument_error("argumentValidation", "numeric comparison failed")
1368        }
1369    };
1370    let result = if let Some(result) =
1371        try_real_ordering_comparison(lhs, rhs, operation).map_err(map_error)?
1372    {
1373        result
1374    } else {
1375        try_complex_ordering_comparison(lhs, rhs, operation)
1376            .map_err(map_error)?
1377            .ok_or_else(|| {
1378                invalid_argument_error("argumentValidation", "expected comparable numeric inputs")
1379            })?
1380    };
1381    logical_value_all_true(&result)
1382}
1383
1384fn scalar_numeric_value(value: &Value) -> Option<Value> {
1385    match value {
1386        Value::Num(_) | Value::Int(_) | Value::Bool(_) => Some(value.clone()),
1387        Value::Complex(real, _) => Some(Value::Num(*real)),
1388        Value::Tensor(tensor) if tensor::tensor_element_len(tensor) == 1 => tensor
1389            .numeric_value_at(0)
1390            .map(|value| match value.into_int_value() {
1391                Some(value) => Value::Int(value),
1392                None => Value::Num(floating_numeric_scalar_to_f64(value)),
1393            }),
1394        Value::LogicalArray(array) if array.data.len() == 1 => {
1395            Some(Value::Bool(array.data[0] != 0))
1396        }
1397        Value::ComplexTensor(tensor) if tensor::complex_tensor_element_len(tensor) == 1 => {
1398            tensor.numeric_value_at(0).map(|(real, _)| {
1399                if let Some(real) = real.into_int_value() {
1400                    Value::Int(real)
1401                } else {
1402                    Value::Num(floating_numeric_scalar_to_f64(real))
1403                }
1404            })
1405        }
1406        _ => None,
1407    }
1408}
1409
1410fn sparse_scalar_comparison_all(
1411    sparse: &SparseTensor,
1412    scalar: &Value,
1413    sparse_is_left: bool,
1414    operation: IntegerComparisonOp,
1415) -> BuiltinResult<bool> {
1416    for index in 0..sparse.nnz() {
1417        let value = sparse.numeric_value_at(index).ok_or_else(|| {
1418            invalid_argument_error("argumentValidation", "invalid sparse storage")
1419        })?;
1420        let value = match value.into_int_value() {
1421            Some(value) => Value::Int(value),
1422            None => Value::Num(floating_numeric_scalar_to_f64(value)),
1423        };
1424        let matches = if sparse_is_left {
1425            comparison_all(&value, scalar, operation)?
1426        } else {
1427            comparison_all(scalar, &value, operation)?
1428        };
1429        if !matches {
1430            return Ok(false);
1431        }
1432    }
1433    if sparse.nnz() < sparse.rows.saturating_mul(sparse.cols) {
1434        let zero = Value::Num(0.0);
1435        return if sparse_is_left {
1436            comparison_all(&zero, scalar, operation)
1437        } else {
1438            comparison_all(scalar, &zero, operation)
1439        };
1440    }
1441    Ok(true)
1442}
1443
1444fn floating_numeric_scalar_to_f64(value: runmat_value::NumericScalar) -> f64 {
1445    match value {
1446        runmat_value::NumericScalar::F64(value) => value,
1447        runmat_value::NumericScalar::F32(value) => f64::from(value),
1448        _ => unreachable!("integer numeric scalar was handled before floating conversion"),
1449    }
1450}
1451
1452fn logical_value_all_true(value: &Value) -> BuiltinResult<bool> {
1453    match value {
1454        Value::Bool(value) => Ok(*value),
1455        Value::LogicalArray(array) => Ok(array.data.iter().all(|value| *value != 0)),
1456        _ => Err(invalid_argument_error(
1457            "argumentValidation",
1458            "comparison did not return logical data",
1459        )
1460        .into()),
1461    }
1462}
1463
1464fn ensure_same_numeric_class(builtin: &str, lhs: &Value, rhs: &Value) -> BuiltinResult<()> {
1465    let lhs = numeric_class_name(lhs)
1466        .ok_or_else(|| invalid_argument_error(builtin, "expected numeric input"))?;
1467    let rhs = numeric_class_name(rhs)
1468        .ok_or_else(|| invalid_argument_error(builtin, "expected numeric bound"))?;
1469    if lhs != rhs {
1470        return Err(invalid_argument_error(
1471            builtin,
1472            "bounds must have the same numeric class as the value",
1473        )
1474        .into());
1475    }
1476    Ok(())
1477}
1478
1479fn numeric_class_name(value: &Value) -> Option<String> {
1480    match value {
1481        Value::GpuTensor(handle) if handle_is_logical(handle) => Some("logical".into()),
1482        Value::GpuTensor(handle) if handle_integer_type(handle).is_some() => {
1483            crate::builtins::common::gpu_helpers::expected_gpu_class_name(
1484                None,
1485                handle_integer_type(handle),
1486                false,
1487            )
1488            .map(str::to_owned)
1489        }
1490        Value::GpuTensor(handle) => crate::builtins::common::gpu_helpers::expected_gpu_class_name(
1491            runmat_accelerate_api::handle_precision(handle),
1492            None,
1493            false,
1494        )
1495        .map(str::to_owned),
1496        _ if value_is_numeric_or_logical(value) => {
1497            Some(class_name_for_value(value).to_ascii_lowercase())
1498        }
1499        _ => None,
1500    }
1501}
1502
1503pub fn value_is_column(value: &Value) -> bool {
1504    let shape: &[usize] = match value {
1505        Value::Tensor(value) => &value.shape,
1506        Value::ComplexTensor(value) => &value.shape,
1507        Value::LogicalArray(value) => &value.shape,
1508        Value::StringArray(value) => &value.shape,
1509        Value::Cell(value) => &value.shape,
1510        Value::CharArray(value) => &value.shape,
1511        Value::GpuTensor(handle) => &handle.shape,
1512        _ => {
1513            let (_, cols) = value_shape_2d(value);
1514            return cols == 1;
1515        }
1516    };
1517    shape.get(1).copied().unwrap_or(1) == 1 && shape.iter().skip(2).all(|extent| *extent == 1)
1518}
1519
1520pub fn value_is_vector(value: &Value) -> Result<bool, RuntimeError> {
1521    fn shape_is_vector(shape: &[usize]) -> bool {
1522        let mut dimensions = shape.len();
1523        while dimensions > 2 && shape[dimensions - 1] == 1 {
1524            dimensions -= 1;
1525        }
1526        if dimensions > 2 {
1527            return false;
1528        }
1529        let rows = shape.first().copied().unwrap_or(1);
1530        let cols = shape.get(1).copied().unwrap_or(1);
1531        rows == 1 || cols == 1
1532    }
1533
1534    Ok(match value {
1535        Value::Tensor(value) => shape_is_vector(&value.shape),
1536        Value::ComplexTensor(value) => shape_is_vector(&value.shape),
1537        Value::LogicalArray(value) => shape_is_vector(&value.shape),
1538        Value::StringArray(value) => shape_is_vector(&value.shape),
1539        Value::Cell(value) => shape_is_vector(&value.shape),
1540        Value::CharArray(value) => shape_is_vector(&value.shape),
1541        Value::GpuTensor(handle) => shape_is_vector(&handle.shape),
1542        Value::SparseTensor(value) => value.rows == 1 || value.cols == 1,
1543        _ => true,
1544    })
1545}
1546
1547pub fn value_satisfies_vector_validator(
1548    value: &Value,
1549    allow_all_empties: bool,
1550) -> Result<bool, RuntimeError> {
1551    Ok(value_is_vector(value)? || (allow_all_empties && value_is_empty(value)))
1552}
1553
1554pub fn value_matches_class(value: &Value, class_name: &str) -> bool {
1555    let requested = class_name.trim();
1556    if requested.is_empty() {
1557        return false;
1558    }
1559    if matches!(value, Value::GpuTensor(handle) if runmat_accelerate_api::handle_is_explicit(handle))
1560    {
1561        return requested.eq_ignore_ascii_case("gpuarray");
1562    }
1563    match requested.to_ascii_lowercase().as_str() {
1564        "numeric" => value_is_numeric(value),
1565        "float" => value_is_float(value),
1566        "integer" => value_has_native_integer_class(value),
1567        "logical" => value_has_logical_class(value),
1568        "char" => matches!(value, Value::CharArray(_)),
1569        "string" => matches!(value, Value::String(_) | Value::StringArray(_)),
1570        "cell" => matches!(value, Value::Cell(_)),
1571        "struct" => matches!(value, Value::Struct(_)),
1572        "sparse" => matches!(value, Value::SparseTensor(_)),
1573        "double" => {
1574            matches!(value, Value::Num(_) | Value::Complex(_, _))
1575                || matches!(value, Value::Tensor(t) if t.numeric_dtype() == NumericDType::F64)
1576                || matches!(value, Value::SparseTensor(t) if t.integer_storage().is_none())
1577                || matches!(value, Value::ComplexTensor(t) if t.numeric_dtype() == NumericDType::F64)
1578                || matches!(value, Value::GpuTensor(handle) if !handle_is_logical(handle) && handle_integer_type(handle).is_none() && runmat_accelerate_api::handle_precision(handle) == Some(runmat_accelerate_api::ProviderPrecision::F64))
1579        }
1580        "single" => {
1581            matches!(value, Value::Tensor(t) if t.numeric_dtype() == NumericDType::F32)
1582                || matches!(value, Value::ComplexTensor(t) if t.numeric_dtype() == NumericDType::F32)
1583                || matches!(value, Value::GpuTensor(handle) if !handle_is_logical(handle) && handle_integer_type(handle).is_none() && runmat_accelerate_api::handle_precision(handle) == Some(runmat_accelerate_api::ProviderPrecision::F32))
1584        }
1585        "gpuarray" => {
1586            matches!(value, Value::GpuTensor(handle) if runmat_accelerate_api::handle_is_explicit(handle))
1587        }
1588        _ => class_name_for_value(value).eq_ignore_ascii_case(requested),
1589    }
1590}
1591
1592pub fn value_has_native_integer_class(value: &Value) -> bool {
1593    match value {
1594        Value::Int(_) => true,
1595        Value::Tensor(tensor) => tensor.integer_storage().is_some(),
1596        Value::SparseTensor(tensor) => tensor.integer_storage().is_some(),
1597        Value::ComplexTensor(tensor) => tensor.integer_storage().is_some(),
1598        Value::GpuTensor(handle) => handle_integer_type(handle).is_some(),
1599        _ => false,
1600    }
1601}
1602
1603/// Returns whether `value`, including aggregate payloads, contains a native
1604/// integer class. Use this at compatibility boundaries before a recursive
1605/// gather can erase resident class metadata.
1606pub fn value_contains_native_integer_class(value: &Value) -> bool {
1607    match value {
1608        Value::Cell(cell) => cell.data.iter().any(value_contains_native_integer_class),
1609        Value::Struct(value) => value
1610            .fields
1611            .values()
1612            .any(value_contains_native_integer_class),
1613        Value::Object(value) => value
1614            .properties
1615            .values()
1616            .any(value_contains_native_integer_class),
1617        Value::Closure(value) => value
1618            .captures
1619            .iter()
1620            .any(value_contains_native_integer_class),
1621        Value::OutputList(values) => values.iter().any(value_contains_native_integer_class),
1622        _ => value_has_native_integer_class(value),
1623    }
1624}
1625
1626/// Returns whether `value`, including aggregate payloads, contains a handle
1627/// created through explicit `gpuArray` intent rather than automatic residency.
1628pub fn value_contains_explicit_gpu(value: &Value) -> bool {
1629    match value {
1630        Value::GpuTensor(handle) => runmat_accelerate_api::handle_is_explicit(handle),
1631        Value::Cell(cell) => cell.data.iter().any(value_contains_explicit_gpu),
1632        Value::Struct(value) => value.fields.values().any(value_contains_explicit_gpu),
1633        Value::Object(value) => value.properties.values().any(value_contains_explicit_gpu),
1634        Value::Closure(value) => value.captures.iter().any(value_contains_explicit_gpu),
1635        Value::OutputList(values) => values.iter().any(value_contains_explicit_gpu),
1636        _ => false,
1637    }
1638}
1639
1640pub fn value_has_logical_class(value: &Value) -> bool {
1641    matches!(value, Value::Bool(_) | Value::LogicalArray(_))
1642        || matches!(value, Value::GpuTensor(handle) if handle_is_logical(handle))
1643}
1644
1645pub fn native_integer_value_is_exact_f64(value: &Value) -> bool {
1646    let exact = crate::builtins::math::trigonometry::cos::integer_is_exact_f64;
1647    match value {
1648        Value::Int(value) => exact(value),
1649        Value::Tensor(tensor) => tensor
1650            .integer_storage()
1651            .is_none_or(|storage| storage.exact_values().iter().all(exact)),
1652        Value::SparseTensor(tensor) => tensor
1653            .integer_storage()
1654            .is_none_or(|storage| storage.exact_values().iter().all(exact)),
1655        Value::ComplexTensor(tensor) => tensor.integer_storage().is_none_or(|storage| {
1656            storage.real.exact_values().iter().all(exact)
1657                && storage.imag.exact_values().iter().all(exact)
1658        }),
1659        Value::Cell(cell) => cell.data.iter().all(native_integer_value_is_exact_f64),
1660        Value::Struct(value) => value.fields.values().all(native_integer_value_is_exact_f64),
1661        Value::OutputList(values) => values.iter().all(native_integer_value_is_exact_f64),
1662        Value::GpuTensor(handle) => runmat_accelerate_api::handle_integer_type(handle)
1663            .is_none_or(|element_type| element_type.element_size() <= 4),
1664        _ => true,
1665    }
1666}
1667
1668/// Check a native integer value against a binary64 boundary without rejecting a
1669/// resident 64-bit value solely because its contents are not visible in handle
1670/// metadata. Compatibility admission must run before calling this helper.
1671pub async fn native_integer_value_is_exact_f64_async(value: &Value) -> Result<bool, RuntimeError> {
1672    if native_integer_value_is_exact_f64(value) {
1673        return Ok(true);
1674    }
1675    if matches!(value, Value::GpuTensor(handle) if handle_integer_type(handle).is_some()) {
1676        let Value::GpuTensor(handle) = value else {
1677            unreachable!();
1678        };
1679        let provider = crate::builtins::common::gpu_helpers::exact_provider_for_handle(handle)
1680            .ok_or_else(|| {
1681                build_runtime_error(
1682                    "integer exactness check: no acceleration provider owns the input handle",
1683                )
1684                .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
1685                .build()
1686            })?;
1687        let expected_type = handle_integer_type(handle).expect("integer type checked above");
1688        if runmat_accelerate_api::handle_storage(handle)
1689            != runmat_accelerate_api::GpuTensorStorage::Real
1690            || handle_is_logical(handle)
1691            || runmat_accelerate_api::handle_precision(handle).is_some()
1692            || !crate::builtins::common::gpu_helpers::gpu_class_metadata_matches(
1693                handle,
1694                None,
1695                Some(expected_type),
1696                false,
1697            )
1698        {
1699            return Err(build_runtime_error(
1700                "integer exactness check: input handle has contradictory integer metadata",
1701            )
1702            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
1703            .build());
1704        }
1705        let snapshot = crate::builtins::common::gpu_helpers::snapshot_handle_metadata(handle);
1706        let gathered = provider.download_integer(handle).await.map_err(|error| {
1707            build_runtime_error(format!(
1708                "integer exactness check: owner-preserving download failed: {error}"
1709            ))
1710            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
1711            .build()
1712        });
1713        crate::builtins::common::gpu_helpers::restore_handle_metadata(handle, &snapshot);
1714        let gathered = gathered?;
1715        if gathered.shape != handle.shape || gathered.data.element_type() != expected_type {
1716            return Err(build_runtime_error(
1717                "integer exactness check: provider returned contradictory integer payload metadata",
1718            )
1719            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
1720            .build());
1721        }
1722        let exact = crate::builtins::math::trigonometry::cos::integer_is_exact_f64;
1723        let values_exact = match &gathered.data {
1724            runmat_accelerate_api::HostIntegerDataOwned::I8(values) => values
1725                .iter()
1726                .all(|value| exact(&IntValue::I64(i64::from(*value)))),
1727            runmat_accelerate_api::HostIntegerDataOwned::I16(values) => values
1728                .iter()
1729                .all(|value| exact(&IntValue::I64(i64::from(*value)))),
1730            runmat_accelerate_api::HostIntegerDataOwned::I32(values) => values
1731                .iter()
1732                .all(|value| exact(&IntValue::I64(i64::from(*value)))),
1733            runmat_accelerate_api::HostIntegerDataOwned::I64(values) => {
1734                values.iter().all(|value| exact(&IntValue::I64(*value)))
1735            }
1736            runmat_accelerate_api::HostIntegerDataOwned::U8(values) => values
1737                .iter()
1738                .all(|value| exact(&IntValue::U64(u64::from(*value)))),
1739            runmat_accelerate_api::HostIntegerDataOwned::U16(values) => values
1740                .iter()
1741                .all(|value| exact(&IntValue::U64(u64::from(*value)))),
1742            runmat_accelerate_api::HostIntegerDataOwned::U32(values) => values
1743                .iter()
1744                .all(|value| exact(&IntValue::U64(u64::from(*value)))),
1745            runmat_accelerate_api::HostIntegerDataOwned::U64(values) => {
1746                values.iter().all(|value| exact(&IntValue::U64(*value)))
1747            }
1748        };
1749        return Ok(values_exact);
1750    }
1751    Ok(false)
1752}
1753
1754/// Gate a native-integer RunMat extension before provider lookup or gathering,
1755/// then prove that its authoritative values can cross a binary64 computation
1756/// boundary without rounding.
1757pub async fn ensure_runmat_integer_f64_boundary(
1758    value: &Value,
1759    extension: &'static BuiltinExtensionDescriptor,
1760    builtin: &'static str,
1761    role: &str,
1762) -> BuiltinResult<()> {
1763    if !value_has_native_integer_class(value) {
1764        return Ok(());
1765    }
1766    crate::compatibility::ensure_builtin_extension_enabled(extension, builtin)?;
1767    if !native_integer_value_is_exact_f64_async(value).await? {
1768        return Err(build_runtime_error(format!(
1769            "{builtin}: integer {role} values must be exactly representable as double"
1770        ))
1771        .with_builtin(builtin)
1772        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
1773        .build());
1774    }
1775    Ok(())
1776}
1777
1778pub fn must_be_a(value: &Value, class_names: Vec<String>) -> Result<bool, RuntimeError> {
1779    Ok(class_names
1780        .iter()
1781        .any(|class_name| value_matches_class(value, class_name)))
1782}
1783
1784pub fn value_underlying_type_matches(
1785    value: &Value,
1786    class_names: Vec<String>,
1787) -> Result<bool, RuntimeError> {
1788    Ok(class_names
1789        .iter()
1790        .any(|class_name| underlying_type_matches(value, class_name)))
1791}
1792
1793pub fn value_is_member(value: &Value, set: &Value) -> Result<bool, RuntimeError> {
1794    let values = atoms(value)?;
1795    let allowed = atoms(set)?;
1796    value_is_member_atoms_inner(&values, &allowed)
1797}
1798
1799pub async fn value_is_member_async(value: &Value, set: &Value) -> BuiltinResult<bool> {
1800    let value = host_value(value).await?;
1801    let set = host_value(set).await?;
1802    ensure_member_class_compatibility(&value, &set)?;
1803    value_is_member(&value, &set).map_err(Into::into)
1804}
1805
1806fn ensure_member_class_compatibility(value: &Value, set: &Value) -> BuiltinResult<()> {
1807    let Some(value_class) = numeric_class_name(value) else {
1808        return Ok(());
1809    };
1810    let Some(set_class) = numeric_class_name(set) else {
1811        return Ok(());
1812    };
1813    if value_class == set_class || value_class == "double" || set_class == "double" {
1814        return Ok(());
1815    }
1816    Err(invalid_argument_error(
1817        "mustBeMember",
1818        "unlike nondouble numeric inputs must have the same class",
1819    )
1820    .into())
1821}
1822
1823pub fn value_is_member_atoms(
1824    value: &Value,
1825    allowed: &[ValidationAtom],
1826) -> Result<bool, RuntimeError> {
1827    let values = atoms(value)?;
1828    value_is_member_atoms_inner(&values, allowed)
1829}
1830
1831pub async fn value_is_member_atoms_async(
1832    value: &Value,
1833    allowed: &[ValidationAtom],
1834) -> BuiltinResult<bool> {
1835    let value = host_value(value).await?;
1836    value_is_member_atoms(&value, allowed).map_err(Into::into)
1837}
1838
1839fn value_is_member_atoms_inner(
1840    values: &[ValidationAtom],
1841    allowed: &[ValidationAtom],
1842) -> Result<bool, RuntimeError> {
1843    Ok(values
1844        .iter()
1845        .all(|value| allowed.iter().any(|allowed| atom_eq(value, allowed))))
1846}
1847
1848pub fn atoms(value: &Value) -> Result<Vec<ValidationAtom>, RuntimeError> {
1849    match value {
1850        Value::Num(v) => Ok(vec![ValidationAtom::Number(*v)]),
1851        Value::Int(v) => Ok(vec![ValidationAtom::Integer(v.clone())]),
1852        Value::Bool(v) => Ok(vec![ValidationAtom::Bool(*v)]),
1853        Value::Complex(re, im) => Ok(vec![ValidationAtom::ComplexNumber(*re, *im)]),
1854        Value::String(s) => Ok(vec![ValidationAtom::Text(s.clone())]),
1855        Value::CharArray(c) if c.rows == 1 => Ok(vec![ValidationAtom::Text(chars_to_string(c))]),
1856        Value::Tensor(t) => {
1857            if let Some(storage) = t.integer_storage() {
1858                return Ok(storage
1859                    .exact_values()
1860                    .into_iter()
1861                    .map(ValidationAtom::Integer)
1862                    .collect());
1863            }
1864            Ok(tensor::tensor_values_f64_cow(t)
1865                .iter()
1866                .copied()
1867                .map(ValidationAtom::Number)
1868                .collect())
1869        }
1870        Value::ComplexTensor(t) => {
1871            if let Some(storage) = t.integer_storage() {
1872                return Ok((0..storage.len())
1873                    .map(|index| {
1874                        ValidationAtom::ComplexInteger(
1875                            storage
1876                                .real
1877                                .value_at(index)
1878                                .expect("validated real storage"),
1879                            storage
1880                                .imag
1881                                .value_at(index)
1882                                .expect("validated imaginary storage"),
1883                        )
1884                    })
1885                    .collect());
1886            }
1887            Ok(t.materialize_f64()
1888                .iter()
1889                .map(|(re, im)| ValidationAtom::ComplexNumber(*re, *im))
1890                .collect())
1891        }
1892        Value::SparseTensor(t) => sparse_atoms(t),
1893        Value::LogicalArray(a) => Ok(a
1894            .data
1895            .iter()
1896            .map(|v| ValidationAtom::Bool(*v != 0))
1897            .collect()),
1898        Value::StringArray(s) => Ok(s.data.iter().cloned().map(ValidationAtom::Text).collect()),
1899        Value::Cell(c) => {
1900            let mut out = Vec::new();
1901            for entry in &c.data {
1902                out.extend(atoms(entry)?);
1903            }
1904            Ok(out)
1905        }
1906        _ => Err(invalid_argument_error(
1907            "mustBeMember",
1908            "unsupported member value type",
1909        )),
1910    }
1911}
1912
1913fn sparse_atoms(t: &SparseTensor) -> Result<Vec<ValidationAtom>, RuntimeError> {
1914    let numel = t.rows.saturating_mul(t.cols);
1915    let mut out = Vec::with_capacity(numel.min(t.nnz().saturating_add(1)));
1916    if let Some(storage) = t.integer_storage() {
1917        out.extend(
1918            storage
1919                .exact_values()
1920                .into_iter()
1921                .map(ValidationAtom::Integer),
1922        );
1923        if storage.len() < numel {
1924            let zero = storage
1925                .zeros_like(1)
1926                .value_at(0)
1927                .expect("one-element zero storage");
1928            out.push(ValidationAtom::Integer(zero));
1929        }
1930        return Ok(out);
1931    }
1932    let values = t.materialize_f64();
1933    out.extend(values.iter().copied().map(ValidationAtom::Number));
1934    if values.len() < numel {
1935        out.push(ValidationAtom::Number(0.0));
1936    }
1937    Ok(out)
1938}
1939
1940fn atom_eq(left: &ValidationAtom, right: &ValidationAtom) -> bool {
1941    match (left, right) {
1942        (ValidationAtom::Number(a), ValidationAtom::Number(b)) => a == b,
1943        (ValidationAtom::Integer(a), ValidationAtom::Integer(b)) => a == b,
1944        (ValidationAtom::Integer(a), ValidationAtom::Number(b))
1945        | (ValidationAtom::Number(b), ValidationAtom::Integer(a)) => {
1946            integer_f64_order(a.clone(), *b) == Some(Ordering::Equal)
1947        }
1948        (ValidationAtom::ComplexNumber(ar, ai), ValidationAtom::ComplexNumber(br, bi)) => {
1949            ar == br && ai == bi
1950        }
1951        (ValidationAtom::ComplexInteger(ar, ai), ValidationAtom::ComplexInteger(br, bi)) => {
1952            ar == br && ai == bi
1953        }
1954        (ValidationAtom::ComplexInteger(ar, ai), ValidationAtom::ComplexNumber(br, bi))
1955        | (ValidationAtom::ComplexNumber(br, bi), ValidationAtom::ComplexInteger(ar, ai)) => {
1956            integer_f64_order(ar.clone(), *br) == Some(Ordering::Equal)
1957                && integer_f64_order(ai.clone(), *bi) == Some(Ordering::Equal)
1958        }
1959        (ValidationAtom::ComplexNumber(re, im), ValidationAtom::Number(value))
1960        | (ValidationAtom::Number(value), ValidationAtom::ComplexNumber(re, im)) => {
1961            *im == 0.0 && re == value
1962        }
1963        (ValidationAtom::ComplexInteger(re, im), ValidationAtom::Integer(value))
1964        | (ValidationAtom::Integer(value), ValidationAtom::ComplexInteger(re, im)) => {
1965            im.is_zero() && re == value
1966        }
1967        (ValidationAtom::ComplexInteger(re, im), ValidationAtom::Number(value))
1968        | (ValidationAtom::Number(value), ValidationAtom::ComplexInteger(re, im)) => {
1969            im.is_zero() && integer_f64_order(re.clone(), *value) == Some(Ordering::Equal)
1970        }
1971        (ValidationAtom::ComplexNumber(re, im), ValidationAtom::Integer(value))
1972        | (ValidationAtom::Integer(value), ValidationAtom::ComplexNumber(re, im)) => {
1973            *im == 0.0 && integer_f64_order(value.clone(), *re) == Some(Ordering::Equal)
1974        }
1975        (ValidationAtom::Text(a), ValidationAtom::Text(b)) => a == b,
1976        (ValidationAtom::Bool(a), ValidationAtom::Bool(b)) => a == b,
1977        (ValidationAtom::Bool(value), ValidationAtom::Number(number))
1978        | (ValidationAtom::Number(number), ValidationAtom::Bool(value)) => {
1979            *number == f64::from(*value)
1980        }
1981        (ValidationAtom::Bool(value), ValidationAtom::Integer(integer))
1982        | (ValidationAtom::Integer(integer), ValidationAtom::Bool(value)) => {
1983            integer_f64_order(integer.clone(), f64::from(*value)) == Some(Ordering::Equal)
1984        }
1985        _ => false,
1986    }
1987}
1988
1989fn complex_real_values_all(
1990    value: &Value,
1991    float_pred: impl Fn(f64) -> bool + Copy,
1992    integer_pred: impl Fn(&IntValue) -> bool + Copy,
1993) -> Option<bool> {
1994    match value {
1995        Value::Complex(real, _) => Some(float_pred(*real)),
1996        Value::ComplexTensor(tensor) => {
1997            if let Some(storage) = tensor.integer_storage() {
1998                Some(integer_storage_all(&storage.real, integer_pred))
1999            } else {
2000                Some(
2001                    tensor
2002                        .materialize_f64()
2003                        .iter()
2004                        .all(|(real, _)| float_pred(*real)),
2005                )
2006            }
2007        }
2008        _ => None,
2009    }
2010}
2011
2012fn exact_integer_values_all(
2013    value: &Value,
2014    pred: impl Fn(&IntValue) -> bool + Copy,
2015) -> Option<bool> {
2016    match value {
2017        Value::Int(value) => Some(pred(value)),
2018        Value::Tensor(tensor) => tensor
2019            .integer_storage()
2020            .map(|storage| integer_storage_all(storage, pred)),
2021        Value::SparseTensor(tensor) => tensor.integer_storage().map(|storage| {
2022            let numel = tensor.rows.saturating_mul(tensor.cols);
2023            integer_storage_all(storage, pred)
2024                && (storage.len() >= numel || integer_storage_zero_satisfies(storage, pred))
2025        }),
2026        Value::ComplexTensor(tensor) => tensor.integer_storage().map(|storage| {
2027            (0..storage.len()).all(|index| {
2028                let Some(real) = storage.real.value_at(index) else {
2029                    return false;
2030                };
2031                let Some(imag) = storage.imag.value_at(index) else {
2032                    return false;
2033                };
2034                imag.is_zero() && pred(&real)
2035            })
2036        }),
2037        _ => None,
2038    }
2039}
2040
2041fn integer_storage_all(storage: &IntegerStorage, pred: impl Fn(&IntValue) -> bool + Copy) -> bool {
2042    (0..storage.len()).all(|index| {
2043        storage
2044            .value_at(index)
2045            .as_ref()
2046            .is_some_and(|value| pred(value))
2047    })
2048}
2049
2050fn integer_storage_zero_satisfies(
2051    storage: &IntegerStorage,
2052    pred: impl Fn(&IntValue) -> bool,
2053) -> bool {
2054    storage.zeros_like(1).value_at(0).as_ref().is_some_and(pred)
2055}
2056
2057fn int_f64_matches(integer: &IntValue, threshold: f64, pred: impl Fn(Ordering) -> bool) -> bool {
2058    integer_f64_order(integer.clone(), threshold).is_some_and(pred)
2059}
2060
2061fn int_is_positive(value: &IntValue) -> bool {
2062    match value {
2063        IntValue::I8(value) => *value > 0,
2064        IntValue::I16(value) => *value > 0,
2065        IntValue::I32(value) => *value > 0,
2066        IntValue::I64(value) => *value > 0,
2067        IntValue::U8(value) => *value > 0,
2068        IntValue::U16(value) => *value > 0,
2069        IntValue::U32(value) => *value > 0,
2070        IntValue::U64(value) => *value > 0,
2071    }
2072}
2073
2074fn int_is_negative(value: &IntValue) -> bool {
2075    match value {
2076        IntValue::I8(value) => *value < 0,
2077        IntValue::I16(value) => *value < 0,
2078        IntValue::I32(value) => *value < 0,
2079        IntValue::I64(value) => *value < 0,
2080        IntValue::U8(_) | IntValue::U16(_) | IntValue::U32(_) | IntValue::U64(_) => false,
2081    }
2082}
2083
2084fn int_is_nonnegative(value: &IntValue) -> bool {
2085    !int_is_negative(value)
2086}
2087
2088fn int_is_nonpositive(value: &IntValue) -> bool {
2089    !int_is_positive(value)
2090}
2091
2092fn numeric_values_all(value: &Value, pred: impl Fn(f64) -> bool) -> bool {
2093    match value {
2094        Value::Num(v) => pred(*v),
2095        Value::Int(v) => pred(v.to_f64()),
2096        Value::Bool(v) => pred(if *v { 1.0 } else { 0.0 }),
2097        Value::LogicalArray(a) => a.data.iter().map(|v| f64::from(*v != 0)).all(pred),
2098        Value::Tensor(t) => tensor::tensor_values_f64(t).into_iter().all(pred),
2099        Value::SparseTensor(t) if t.integer_storage().is_some() => {
2100            let storage = t
2101                .integer_storage()
2102                .expect("integer storage was checked above");
2103            let numel = t.rows.saturating_mul(t.cols);
2104            (0..storage.len()).all(|index| {
2105                pred(
2106                    storage
2107                        .value_at(index)
2108                        .expect("sparse integer storage length is consistent")
2109                        .to_f64(),
2110                )
2111            }) && (storage.len() >= numel || pred(0.0))
2112        }
2113        Value::SparseTensor(t) => {
2114            let numel = t.rows.saturating_mul(t.cols);
2115            let values = t.materialize_f64();
2116            values.iter().copied().all(&pred) && (values.len() >= numel || pred(0.0))
2117        }
2118        Value::Complex(re, im) => *im == 0.0 && pred(*re),
2119        Value::ComplexTensor(t) if t.integer_storage().is_some() => {
2120            let storage = t
2121                .integer_storage()
2122                .expect("integer storage was checked above");
2123            (0..storage.len()).all(|index| {
2124                let real = storage
2125                    .real
2126                    .value_at(index)
2127                    .expect("complex integer real storage length is consistent");
2128                let imag = storage
2129                    .imag
2130                    .value_at(index)
2131                    .expect("complex integer imaginary storage length is consistent");
2132                imag.is_zero() && pred(real.to_f64())
2133            })
2134        }
2135        Value::ComplexTensor(t) => t
2136            .materialize_f64()
2137            .iter()
2138            .all(|(re, im)| *im == 0.0 && pred(*re)),
2139        _ => false,
2140    }
2141}
2142
2143fn type_names_arg(args: &[Value], index: usize) -> Result<Vec<String>, RuntimeError> {
2144    match args.get(index) {
2145        Some(value) => value_texts(value),
2146        None => Err(invalid_argument_error(
2147            "argumentValidation",
2148            "missing type name argument",
2149        )),
2150    }
2151}
2152
2153fn range_inclusivity_arg(builtin: &str, args: &[Value]) -> Result<RangeInclusivity, RuntimeError> {
2154    match args {
2155        [] => Ok(RangeInclusivity::CLOSED),
2156        [flag] => range_inclusivity_single_flag(builtin, text_scalar_arg(builtin, flag)?.as_str()),
2157        [lower, upper] => {
2158            let lower =
2159                range_bound_inclusive_flag(builtin, text_scalar_arg(builtin, lower)?.as_str())?;
2160            let upper =
2161                range_bound_inclusive_flag(builtin, text_scalar_arg(builtin, upper)?.as_str())?;
2162            Ok(RangeInclusivity { lower, upper })
2163        }
2164        _ => Err(invalid_argument_error(
2165            builtin,
2166            "invalid range inclusivity flags",
2167        )),
2168    }
2169}
2170
2171fn range_inclusivity_single_flag(
2172    builtin: &str,
2173    flag: &str,
2174) -> Result<RangeInclusivity, RuntimeError> {
2175    match flag.trim().to_ascii_lowercase().as_str() {
2176        "inclusive" => Ok(RangeInclusivity::CLOSED),
2177        "exclusive" => Ok(RangeInclusivity::OPEN),
2178        "exclude-lower" | "openleft" | "open-left" => Ok(RangeInclusivity::OPEN_LEFT),
2179        "exclude-upper" | "openright" | "open-right" => Ok(RangeInclusivity::OPEN_RIGHT),
2180        _ => Err(invalid_argument_error(
2181            builtin,
2182            "range flag must be 'inclusive', 'exclusive', 'exclude-lower', or 'exclude-upper'",
2183        )),
2184    }
2185}
2186
2187fn range_bound_inclusive_flag(builtin: &str, flag: &str) -> Result<bool, RuntimeError> {
2188    match flag.trim().to_ascii_lowercase().as_str() {
2189        "inclusive" => Ok(true),
2190        "exclusive" => Ok(false),
2191        _ => Err(invalid_argument_error(
2192            builtin,
2193            "range bound flag must be 'inclusive' or 'exclusive'",
2194        )),
2195    }
2196}
2197
2198fn text_scalar_arg(builtin: &str, value: &Value) -> Result<String, RuntimeError> {
2199    let texts = value_texts(value)?;
2200    match texts.as_slice() {
2201        [text] => Ok(text.clone()),
2202        _ => Err(invalid_argument_error(builtin, "expected text scalar")),
2203    }
2204}
2205
2206fn value_texts(value: &Value) -> Result<Vec<String>, RuntimeError> {
2207    match value {
2208        Value::String(s) => Ok(vec![s.clone()]),
2209        Value::StringArray(s) => Ok(s.data.clone()),
2210        Value::CharArray(c) if c.rows == 1 => Ok(vec![chars_to_string(c)]),
2211        Value::Cell(c) => {
2212            let mut out = Vec::with_capacity(c.data.len());
2213            for entry in &c.data {
2214                out.extend(value_texts(entry)?);
2215            }
2216            Ok(out)
2217        }
2218        other => Err(invalid_argument_error(
2219            "argumentValidation",
2220            format!("expected text, got {}", class_name_for_value(other)),
2221        )),
2222    }
2223}
2224
2225fn chars_to_string(chars: &CharArray) -> String {
2226    chars.data.iter().collect()
2227}
2228
2229pub fn isvarname_value(value: &Value) -> bool {
2230    value_texts(value)
2231        .map(|names| names.iter().all(|name| is_valid_varname(name)))
2232        .unwrap_or(false)
2233}
2234
2235pub fn namedargs2cell_value(value: Value) -> BuiltinResult<Value> {
2236    let Value::Struct(struct_value) = value else {
2237        return Err(
2238            invalid_argument_error("namedargs2cell", "input must be a scalar struct").into(),
2239        );
2240    };
2241    let mut data = Vec::with_capacity(struct_value.fields.len().saturating_mul(2));
2242    for (field, value) in struct_value.fields {
2243        data.push(Value::String(field));
2244        data.push(value);
2245    }
2246    let cols = data.len();
2247    let cell = CellArray::new(data, 1, cols)
2248        .map_err(|err| invalid_argument_error("namedargs2cell", err))?;
2249    Ok(Value::Cell(cell))
2250}
2251
2252pub fn validate_function_signatures_json(value: &Value) -> BuiltinResult<()> {
2253    for text in value_texts(value)? {
2254        serde_json::from_str::<serde_json::Value>(&text).map_err(|err| {
2255            invalid_argument_error(
2256                "validateFunctionSignaturesJSON",
2257                format!("invalid JSON signature payload: {err}"),
2258            )
2259        })?;
2260    }
2261    Ok(())
2262}
2263
2264fn bool_type(
2265    _: &[runmat_builtins::Type],
2266    _: &runmat_builtins::ResolveContext,
2267) -> runmat_builtins::Type {
2268    runmat_builtins::Type::Bool
2269}
2270
2271fn any_type(
2272    _: &[runmat_builtins::Type],
2273    _: &runmat_builtins::ResolveContext,
2274) -> runmat_builtins::Type {
2275    runmat_builtins::Type::Unknown
2276}
2277
2278#[runtime_builtin(
2279    name = "isvarname",
2280    category = "argument-validation",
2281    summary = "Return true when text is a valid MATLAB variable name.",
2282    type_resolver(bool_type),
2283    descriptor(self::ISVARNAME_DESCRIPTOR),
2284    integer_audit(self::ISVARNAME_INTEGER_AUDIT),
2285    builtin_path = "crate::builtins::common::validation"
2286)]
2287fn isvarname_builtin(value: Value) -> BuiltinResult<Value> {
2288    Ok(Value::Bool(isvarname_value(&value)))
2289}
2290
2291#[runtime_builtin(
2292    name = "namedargs2cell",
2293    category = "argument-validation",
2294    summary = "Convert a scalar name-value struct to an alternating name/value cell row.",
2295    type_resolver(any_type),
2296    descriptor(self::NAMEDARGS2CELL_DESCRIPTOR),
2297    integer_audit(self::NAMEDARGS2CELL_INTEGER_AUDIT),
2298    builtin_path = "crate::builtins::common::validation"
2299)]
2300fn namedargs2cell_builtin(value: Value) -> BuiltinResult<Value> {
2301    namedargs2cell_value(value)
2302}
2303
2304macro_rules! validator_builtin {
2305    ($func:ident, $name:literal, capabilities = $capabilities:path) => {
2306        #[runtime_builtin(
2307            name = $name,
2308            category = "argument-validation",
2309            summary = "Validate an input argument and throw if the constraint is not satisfied.",
2310            sink = true,
2311            suppress_auto_output = true,
2312            descriptor(self::VALIDATOR_DESCRIPTOR),
2313            integer_capabilities($capabilities),
2314            builtin_path = "crate::builtins::common::validation"
2315        )]
2316        async fn $func(args: Vec<Value>) -> BuiltinResult<Value> {
2317            dispatch_validator_async($name, args).await
2318        }
2319    };
2320    ($func:ident, $name:literal, capabilities = $capabilities:path, extensions = $extensions:path) => {
2321        #[runtime_builtin(
2322            name = $name,
2323            category = "argument-validation",
2324            summary = "Validate an input argument and throw if the constraint is not satisfied.",
2325            sink = true,
2326            suppress_auto_output = true,
2327            descriptor(self::VALIDATOR_DESCRIPTOR),
2328            extensions($extensions),
2329            integer_capabilities($capabilities),
2330            builtin_path = "crate::builtins::common::validation"
2331        )]
2332        async fn $func(args: Vec<Value>) -> BuiltinResult<Value> {
2333            dispatch_validator_async($name, args).await
2334        }
2335    };
2336    ($func:ident, $name:literal, audit = $audit:path) => {
2337        #[runtime_builtin(
2338            name = $name,
2339            category = "argument-validation",
2340            summary = "Validate an input argument and throw if the constraint is not satisfied.",
2341            sink = true,
2342            suppress_auto_output = true,
2343            descriptor(self::VALIDATOR_DESCRIPTOR),
2344            integer_audit($audit),
2345            builtin_path = "crate::builtins::common::validation"
2346        )]
2347        async fn $func(args: Vec<Value>) -> BuiltinResult<Value> {
2348            dispatch_validator_async($name, args).await
2349        }
2350    };
2351    ($func:ident, $name:literal) => {
2352        #[runtime_builtin(
2353            name = $name,
2354            category = "argument-validation",
2355            summary = "Validate an input argument and throw if the constraint is not satisfied.",
2356            sink = true,
2357            suppress_auto_output = true,
2358            descriptor(self::VALIDATOR_DESCRIPTOR),
2359            builtin_path = "crate::builtins::common::validation"
2360        )]
2361        fn $func(args: Vec<Value>) -> BuiltinResult<Value> {
2362            dispatch_validator($name, args)
2363        }
2364    };
2365}
2366
2367validator_builtin!(
2368    must_be_a_builtin,
2369    "mustBeA",
2370    capabilities = self::MUST_BE_A_INTEGER_CAPABILITIES
2371);
2372validator_builtin!(
2373    must_be_column_builtin,
2374    "mustBeColumn",
2375    capabilities = self::MUST_BE_COLUMN_INTEGER_CAPABILITIES
2376);
2377validator_builtin!(
2378    must_be_file_builtin,
2379    "mustBeFile",
2380    audit = self::MUST_BE_FILE_INTEGER_AUDIT
2381);
2382validator_builtin!(
2383    must_be_finite_builtin,
2384    "mustBeFinite",
2385    capabilities = self::MUST_BE_FINITE_INTEGER_CAPABILITIES,
2386    extensions = self::MUST_BE_FINITE_EXTENSIONS
2387);
2388validator_builtin!(
2389    must_be_float_builtin,
2390    "mustBeFloat",
2391    capabilities = self::MUST_BE_FLOAT_INTEGER_CAPABILITIES
2392);
2393validator_builtin!(
2394    must_be_folder_builtin,
2395    "mustBeFolder",
2396    audit = self::MUST_BE_FOLDER_INTEGER_AUDIT
2397);
2398validator_builtin!(
2399    must_be_greater_than_builtin,
2400    "mustBeGreaterThan",
2401    capabilities = self::MUST_BE_GREATER_THAN_INTEGER_CAPABILITIES
2402);
2403validator_builtin!(
2404    must_be_greater_than_or_equal_builtin,
2405    "mustBeGreaterThanOrEqual",
2406    capabilities = self::MUST_BE_GREATER_THAN_OR_EQUAL_INTEGER_CAPABILITIES
2407);
2408validator_builtin!(
2409    must_be_in_range_builtin,
2410    "mustBeInRange",
2411    capabilities = self::MUST_BE_IN_RANGE_INTEGER_CAPABILITIES
2412);
2413validator_builtin!(
2414    must_be_integer_builtin,
2415    "mustBeInteger",
2416    capabilities = self::MUST_BE_INTEGER_INTEGER_CAPABILITIES,
2417    extensions = self::MUST_BE_INTEGER_EXTENSIONS
2418);
2419validator_builtin!(
2420    must_be_less_than_builtin,
2421    "mustBeLessThan",
2422    capabilities = self::MUST_BE_LESS_THAN_INTEGER_CAPABILITIES
2423);
2424validator_builtin!(
2425    must_be_less_than_or_equal_builtin,
2426    "mustBeLessThanOrEqual",
2427    capabilities = self::MUST_BE_LESS_THAN_OR_EQUAL_INTEGER_CAPABILITIES
2428);
2429validator_builtin!(
2430    must_be_member_builtin,
2431    "mustBeMember",
2432    capabilities = self::MUST_BE_MEMBER_INTEGER_CAPABILITIES
2433);
2434validator_builtin!(
2435    must_be_negative_builtin,
2436    "mustBeNegative",
2437    capabilities = self::MUST_BE_NEGATIVE_INTEGER_CAPABILITIES
2438);
2439validator_builtin!(
2440    must_be_nonempty_builtin,
2441    "mustBeNonempty",
2442    capabilities = self::MUST_BE_NONEMPTY_INTEGER_CAPABILITIES
2443);
2444validator_builtin!(
2445    must_be_nonmissing_builtin,
2446    "mustBeNonmissing",
2447    capabilities = self::MUST_BE_NONMISSING_INTEGER_CAPABILITIES
2448);
2449validator_builtin!(
2450    must_be_non_nan_builtin,
2451    "mustBeNonNan",
2452    capabilities = self::MUST_BE_NON_NAN_INTEGER_CAPABILITIES,
2453    extensions = self::MUST_BE_NON_NAN_EXTENSIONS
2454);
2455validator_builtin!(
2456    must_be_nonnegative_builtin,
2457    "mustBeNonnegative",
2458    capabilities = self::MUST_BE_NONNEGATIVE_INTEGER_CAPABILITIES
2459);
2460validator_builtin!(
2461    must_be_nonpositive_builtin,
2462    "mustBeNonpositive",
2463    capabilities = self::MUST_BE_NONPOSITIVE_INTEGER_CAPABILITIES
2464);
2465validator_builtin!(
2466    must_be_nonsparse_builtin,
2467    "mustBeNonsparse",
2468    capabilities = self::MUST_BE_NONSPARSE_INTEGER_CAPABILITIES
2469);
2470validator_builtin!(
2471    must_be_nonzero_builtin,
2472    "mustBeNonzero",
2473    capabilities = self::MUST_BE_NONZERO_INTEGER_CAPABILITIES,
2474    extensions = self::MUST_BE_NONZERO_EXTENSIONS
2475);
2476validator_builtin!(
2477    must_be_nonzero_length_text_builtin,
2478    "mustBeNonzeroLengthText",
2479    audit = self::MUST_BE_NONZERO_LENGTH_TEXT_INTEGER_AUDIT
2480);
2481validator_builtin!(
2482    must_be_numeric_builtin,
2483    "mustBeNumeric",
2484    capabilities = self::MUST_BE_NUMERIC_INTEGER_CAPABILITIES
2485);
2486validator_builtin!(
2487    must_be_numeric_or_logical_builtin,
2488    "mustBeNumericOrLogical",
2489    capabilities = self::MUST_BE_NUMERIC_OR_LOGICAL_INTEGER_CAPABILITIES
2490);
2491validator_builtin!(
2492    must_be_positive_builtin,
2493    "mustBePositive",
2494    capabilities = self::MUST_BE_POSITIVE_INTEGER_CAPABILITIES
2495);
2496validator_builtin!(
2497    must_be_real_builtin,
2498    "mustBeReal",
2499    capabilities = self::MUST_BE_REAL_INTEGER_CAPABILITIES
2500);
2501validator_builtin!(
2502    must_be_scalar_or_empty_builtin,
2503    "mustBeScalarOrEmpty",
2504    capabilities = self::MUST_BE_SCALAR_OR_EMPTY_INTEGER_CAPABILITIES
2505);
2506validator_builtin!(
2507    must_be_sparse_builtin,
2508    "mustBeSparse",
2509    capabilities = self::MUST_BE_SPARSE_INTEGER_CAPABILITIES
2510);
2511validator_builtin!(
2512    must_be_text_builtin,
2513    "mustBeText",
2514    audit = self::MUST_BE_TEXT_INTEGER_AUDIT
2515);
2516validator_builtin!(
2517    must_be_text_scalar_builtin,
2518    "mustBeTextScalar",
2519    audit = self::MUST_BE_TEXT_SCALAR_INTEGER_AUDIT
2520);
2521validator_builtin!(
2522    must_be_underlying_type_builtin,
2523    "mustBeUnderlyingType",
2524    capabilities = self::MUST_BE_UNDERLYING_TYPE_INTEGER_CAPABILITIES
2525);
2526validator_builtin!(
2527    must_be_valid_variable_name_builtin,
2528    "mustBeValidVariableName",
2529    audit = self::MUST_BE_VALID_VARIABLE_NAME_INTEGER_AUDIT
2530);
2531validator_builtin!(
2532    must_be_vector_builtin,
2533    "mustBeVector",
2534    capabilities = self::MUST_BE_VECTOR_INTEGER_CAPABILITIES
2535);
2536validator_builtin!(
2537    validate_function_signatures_json_builtin,
2538    "validateFunctionSignaturesJSON",
2539    audit = self::VALIDATE_FUNCTION_SIGNATURES_JSON_INTEGER_AUDIT
2540);
2541
2542#[cfg(test)]
2543mod tests {
2544    use super::*;
2545    use crate::builtins::common::identifiers::MATLAB_NAME_LENGTH_MAX;
2546    use crate::builtins::common::test_support;
2547    use runmat_value::{
2548        ComplexTensor, IntValue, IntegerComplexStorage, IntegerStorage, LogicalArray, StringArray,
2549        StructValue, Tensor,
2550    };
2551
2552    fn ok(builtin: &str, args: Vec<Value>) {
2553        dispatch_validator(builtin, args).unwrap_or_else(|err| {
2554            panic!("{builtin} unexpectedly failed: {err}");
2555        });
2556    }
2557
2558    fn err(builtin: &str, args: Vec<Value>) {
2559        assert!(
2560            dispatch_validator(builtin, args).is_err(),
2561            "{builtin} unexpectedly passed"
2562        );
2563    }
2564
2565    fn tensor(data: Vec<f64>, rows: usize, cols: usize) -> Value {
2566        Value::Tensor(Tensor::new_2d(data, rows, cols).unwrap())
2567    }
2568
2569    fn sparse(values: Vec<f64>) -> Value {
2570        Value::SparseTensor(SparseTensor::new(2, 2, vec![0, 1, 1], vec![0], values).unwrap())
2571    }
2572
2573    #[test]
2574    fn resident_integer_exactness_is_class_conservative() {
2575        use runmat_accelerate_api::{GpuTensorHandle, IntegerElementType};
2576
2577        let narrow = GpuTensorHandle {
2578            shape: vec![1, 1],
2579            device_id: u32::MAX,
2580            buffer_id: u64::MAX - 1,
2581            descriptor: Default::default(),
2582        }
2583        .with_numeric_descriptor(
2584            IntegerElementType::U32.into(),
2585            runmat_accelerate_api::GpuTensorStorage::Real,
2586        );
2587        assert!(native_integer_value_is_exact_f64(&Value::GpuTensor(narrow)));
2588        let wide = GpuTensorHandle {
2589            shape: vec![1, 1],
2590            device_id: u32::MAX,
2591            buffer_id: u64::MAX - 2,
2592            descriptor: Default::default(),
2593        }
2594        .with_numeric_descriptor(
2595            IntegerElementType::I64.into(),
2596            runmat_accelerate_api::GpuTensorStorage::Real,
2597        );
2598        assert!(!native_integer_value_is_exact_f64(&Value::GpuTensor(wide)));
2599    }
2600
2601    #[test]
2602    fn resident_wide_integer_exactness_is_decided_from_gathered_values() {
2603        use crate::builtins::common::test_support;
2604        use futures::executor::block_on;
2605        use runmat_accelerate_api::{HostIntegerDataView, HostIntegerTensorView};
2606
2607        test_support::with_test_provider(|provider| {
2608            for (value, expected) in [
2609                (9_007_199_254_740_992_u64, true),
2610                (9_007_199_254_740_993_u64, false),
2611            ] {
2612                let handle = provider
2613                    .upload_integer(&HostIntegerTensorView {
2614                        data: HostIntegerDataView::U64(std::slice::from_ref(&value)),
2615                        shape: &[1, 1],
2616                    })
2617                    .expect("upload resident uint64");
2618                assert_eq!(
2619                    block_on(native_integer_value_is_exact_f64_async(&Value::GpuTensor(
2620                        handle.clone()
2621                    )))
2622                    .expect("exactness check"),
2623                    expected
2624                );
2625                provider.free(&handle).expect("free resident uint64");
2626                runmat_accelerate_api::clear_handle_metadata(&handle);
2627            }
2628        });
2629    }
2630
2631    fn integer_tensor(storage: IntegerStorage, shape: Vec<usize>) -> Value {
2632        Value::Tensor(Tensor::new_integer(storage, shape).unwrap())
2633    }
2634
2635    #[test]
2636    fn structural_validators_cover_all_native_integer_classes_without_conversion() {
2637        let cases = [
2638            (IntegerStorage::I8(vec![1]), "int8"),
2639            (IntegerStorage::I16(vec![1]), "int16"),
2640            (IntegerStorage::I32(vec![1]), "int32"),
2641            (IntegerStorage::I64(vec![1]), "int64"),
2642            (IntegerStorage::U8(vec![1]), "uint8"),
2643            (IntegerStorage::U16(vec![1]), "uint16"),
2644            (IntegerStorage::U32(vec![1]), "uint32"),
2645            (IntegerStorage::U64(vec![1]), "uint64"),
2646        ];
2647
2648        for (storage, class_name) in cases {
2649            let value = integer_tensor(storage, vec![1, 1]);
2650            ok("mustBeReal", vec![value.clone()]);
2651            ok("mustBeScalarOrEmpty", vec![value.clone()]);
2652            err("mustBeSparse", vec![value.clone()]);
2653            ok(
2654                "mustBeUnderlyingType",
2655                vec![value.clone(), Value::String(class_name.into())],
2656            );
2657            ok("mustBeVector", vec![value.clone()]);
2658            err("mustBeText", vec![value.clone()]);
2659            err("mustBeTextScalar", vec![value.clone()]);
2660            err("mustBeValidVariableName", vec![value]);
2661        }
2662    }
2663
2664    #[test]
2665    fn sparse_and_vector_validators_apply_documented_empty_shape_rules() {
2666        let empty = integer_tensor(IntegerStorage::U16(vec![]), vec![0, 3]);
2667        ok("mustBeScalarOrEmpty", vec![empty.clone()]);
2668        ok("mustBeSparse", vec![empty.clone()]);
2669        err("mustBeVector", vec![empty.clone()]);
2670        ok(
2671            "mustBeVector",
2672            vec![empty, Value::String("allow-all-empties".into())],
2673        );
2674
2675        let empty_vector = integer_tensor(IntegerStorage::I8(vec![]), vec![0, 1]);
2676        ok("mustBeVector", vec![empty_vector]);
2677
2678        let multidimensional = integer_tensor(IntegerStorage::U32(vec![1, 2]), vec![1, 1, 2]);
2679        err("mustBeVector", vec![multidimensional]);
2680        let trailing_singleton = integer_tensor(IntegerStorage::U32(vec![1, 2]), vec![1, 2, 1]);
2681        ok("mustBeVector", vec![trailing_singleton]);
2682
2683        err(
2684            "mustBeVector",
2685            vec![
2686                Value::Int(IntValue::U8(1)),
2687                Value::String("unsupported".into()),
2688            ],
2689        );
2690    }
2691
2692    #[test]
2693    fn sparse_integer_storage_satisfies_sparse_validation_without_materialization() {
2694        let sparse = SparseTensor::new_integer(
2695            2,
2696            2,
2697            vec![0, 1, 1],
2698            vec![0],
2699            IntegerStorage::I64(vec![9_007_199_254_740_993]),
2700        )
2701        .expect("sparse integer");
2702        ok("mustBeSparse", vec![Value::SparseTensor(sparse)]);
2703    }
2704
2705    #[test]
2706    fn resident_text_validators_reject_before_provider_lookup() {
2707        use runmat_accelerate_api::{GpuTensorHandle, IntegerElementType};
2708
2709        let handle = GpuTensorHandle {
2710            shape: vec![1, 1],
2711            device_id: u32::MAX - 1,
2712            buffer_id: u64::MAX - 2,
2713            descriptor: Default::default(),
2714        }
2715        .with_numeric_descriptor(
2716            IntegerElementType::I32.into(),
2717            runmat_accelerate_api::GpuTensorStorage::Real,
2718        );
2719        for builtin in ["mustBeText", "mustBeTextScalar", "mustBeValidVariableName"] {
2720            let error = dispatch_validator(builtin, vec![Value::GpuTensor(handle.clone())])
2721                .expect_err("resident integer must fail text validation");
2722            let expected_identifier = format!("RunMat:{builtin}:ValidationFailed");
2723            assert_eq!(error.identifier(), Some(expected_identifier.as_str()));
2724            assert_eq!(error.gpu_gather_retry(), crate::GpuGatherRetry::Never);
2725        }
2726        runmat_accelerate_api::clear_handle_metadata(&handle);
2727    }
2728
2729    #[test]
2730    fn resident_integer_structural_validators_do_not_read_freed_payloads() {
2731        test_support::with_test_provider(|provider| {
2732            let tensor = Tensor::new_integer(IntegerStorage::U64(vec![1, u64::MAX]), vec![1, 2])
2733                .expect("resident integer");
2734            let handle = crate::builtins::common::gpu_helpers::upload_tensor(provider, &tensor)
2735                .expect("upload resident integer");
2736            provider
2737                .free(&handle)
2738                .expect("free payload before predicates");
2739            runmat_accelerate_api::set_handle_logical(&handle, false);
2740            let value = Value::GpuTensor(handle.clone());
2741
2742            ok("mustBeReal", vec![value.clone()]);
2743            err("mustBeScalarOrEmpty", vec![value.clone()]);
2744            err("mustBeSparse", vec![value.clone()]);
2745            ok(
2746                "mustBeUnderlyingType",
2747                vec![value.clone(), Value::String("uint64".into())],
2748            );
2749            ok("mustBeVector", vec![value]);
2750            runmat_accelerate_api::clear_handle_metadata(&handle);
2751        });
2752    }
2753
2754    #[test]
2755    fn numeric_validators_check_all_elements() {
2756        let ok = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
2757        assert!(dispatch_validator("mustBePositive", vec![Value::Tensor(ok)]).is_ok());
2758
2759        let bad = Tensor::new(vec![1.0, 0.0], vec![1, 2]).unwrap();
2760        assert!(dispatch_validator("mustBePositive", vec![Value::Tensor(bad)]).is_err());
2761    }
2762
2763    #[test]
2764    fn numeric_validators_read_typed_integer_storage_exactly() {
2765        let positive =
2766            Tensor::new_integer(IntegerStorage::U16(vec![1, 2]), vec![1, 2]).expect("positive");
2767        ok("mustBePositive", vec![Value::Tensor(positive)]);
2768
2769        let negative =
2770            Tensor::new_integer(IntegerStorage::I16(vec![-1, -2]), vec![1, 2]).expect("negative");
2771        ok("mustBeNegative", vec![Value::Tensor(negative)]);
2772
2773        let zero = Tensor::new_integer(IntegerStorage::I16(vec![0]), vec![1, 1]).expect("zero");
2774        err("mustBeNonzero", vec![Value::Tensor(zero)]);
2775
2776        let wide = 9_007_199_254_740_993_u64;
2777        let adjacent = wide - 1;
2778        assert_eq!(wide as f64, adjacent as f64);
2779        let wide_nonzero =
2780            Tensor::new_integer(IntegerStorage::U64(vec![wide]), vec![1, 1]).expect("wide");
2781        ok("mustBeNonzero", vec![Value::Tensor(wide_nonzero)]);
2782
2783        let complex_nonzero = ComplexTensor::new_integer(
2784            IntegerComplexStorage::new(
2785                IntegerStorage::U64(vec![0]),
2786                IntegerStorage::U64(vec![wide]),
2787            )
2788            .expect("complex integer storage"),
2789            vec![1, 1],
2790        )
2791        .expect("complex integer tensor");
2792        ok("mustBeNonzero", vec![Value::ComplexTensor(complex_nonzero)]);
2793    }
2794
2795    #[test]
2796    fn value_is_empty_uses_typed_integer_storage_length() {
2797        let scalar = Tensor::new_integer(IntegerStorage::U16(vec![7]), vec![1, 1]).expect("scalar");
2798        assert!(!value_is_empty(&Value::Tensor(scalar)));
2799
2800        let empty =
2801            Tensor::new_integer(IntegerStorage::U16(Vec::new()), vec![0, 0]).expect("empty tensor");
2802        assert!(value_is_empty(&Value::Tensor(empty)));
2803
2804        let complex = ComplexTensor::new_integer(
2805            IntegerComplexStorage::new(IntegerStorage::I16(vec![1]), IntegerStorage::I16(vec![0]))
2806                .expect("complex integer storage"),
2807            vec![1, 1],
2808        )
2809        .expect("complex integer tensor");
2810        assert!(!value_is_empty(&Value::ComplexTensor(complex)));
2811
2812        let empty_complex = ComplexTensor::new_integer(
2813            IntegerComplexStorage::new(IntegerStorage::I16(vec![]), IntegerStorage::I16(vec![]))
2814                .expect("empty complex integer storage"),
2815            vec![0, 0],
2816        )
2817        .expect("empty complex integer tensor");
2818        assert!(value_is_empty(&Value::ComplexTensor(empty_complex)));
2819    }
2820
2821    #[test]
2822    fn finite_integer_and_nan_predicates_read_typed_integer_storage_exactly() {
2823        let tensor = Tensor::new_integer(IntegerStorage::I16(vec![-1, 0, 2]), vec![1, 3]).unwrap();
2824        let value = Value::Tensor(tensor);
2825
2826        assert!(value_is_finite(&value));
2827        assert!(value_is_integer(&value));
2828        assert!(value_is_non_nan(&value));
2829        ok("mustBeFinite", vec![value.clone()]);
2830        ok("mustBeInteger", vec![value.clone()]);
2831        ok("mustBeNonNan", vec![value]);
2832
2833        let sparse = Value::SparseTensor(
2834            SparseTensor::new_integer(
2835                2,
2836                2,
2837                vec![0, 1, 2],
2838                vec![0, 1],
2839                IntegerStorage::U8(vec![1, 2]),
2840            )
2841            .unwrap(),
2842        );
2843        assert!(value_is_finite(&sparse));
2844        assert!(value_is_integer(&sparse));
2845        assert!(value_is_non_nan(&sparse));
2846    }
2847
2848    #[test]
2849    fn real_and_integer_predicates_read_authoritative_complex_integer_storage() {
2850        let real_storage = IntegerStorage::I16(vec![1, -2]);
2851        let zero_imag = IntegerStorage::I16(vec![0, 0]);
2852        let real_complex = ComplexTensor::new_integer(
2853            IntegerComplexStorage::new(real_storage, zero_imag).unwrap(),
2854            vec![1, 2],
2855        )
2856        .unwrap();
2857        let value = Value::ComplexTensor(real_complex);
2858        assert!(value_is_finite(&value));
2859        assert!(value_is_real(&value));
2860        assert!(value_is_integer(&value));
2861        assert!(value_is_non_nan(&value));
2862
2863        let nonreal_complex = ComplexTensor::new_integer(
2864            IntegerComplexStorage::new(IntegerStorage::I16(vec![1]), IntegerStorage::I16(vec![1]))
2865                .unwrap(),
2866            vec![1, 1],
2867        )
2868        .unwrap();
2869        let value = Value::ComplexTensor(nonreal_complex);
2870        assert!(!value_is_real(&value));
2871        assert!(value_is_integer(&value));
2872        ok("mustBeInteger", vec![value]);
2873    }
2874
2875    #[test]
2876    fn complex_validators_use_component_integrality_real_ordering_and_exact_membership() {
2877        ok("mustBeInteger", vec![Value::Complex(1.0, 2.0)]);
2878        err("mustBeInteger", vec![Value::Complex(1.0, 2.5)]);
2879        ok("mustBeNegative", vec![Value::Complex(-1.0, 9.0)]);
2880        ok("mustBePositive", vec![Value::Complex(1.0, -9.0)]);
2881        ok(
2882            "mustBeMember",
2883            vec![Value::Complex(1.0, 2.0), Value::Complex(1.0, 2.0)],
2884        );
2885
2886        let wide = Value::ComplexTensor(
2887            ComplexTensor::new_integer(
2888                IntegerComplexStorage::new(
2889                    IntegerStorage::U64(vec![9_007_199_254_740_993]),
2890                    IntegerStorage::U64(vec![7]),
2891                )
2892                .expect("integer components"),
2893                vec![1, 1],
2894            )
2895            .expect("complex integer tensor"),
2896        );
2897        ok("mustBeMember", vec![wide.clone(), wide]);
2898        ok("mustBeMember", vec![Value::Bool(true), Value::Num(1.0)]);
2899        ok("mustBeMember", vec![Value::Num(0.0), Value::Bool(false)]);
2900        ok(
2901            "mustBeInteger",
2902            vec![Value::CharArray(
2903                CharArray::new(vec!['a', 'b'], 1, 2).expect("character row"),
2904            )],
2905        );
2906        ok("mustBeNonzero", vec![Value::Num(f64::INFINITY)]);
2907        ok("mustBeNonzero", vec![Value::Num(f64::NAN)]);
2908        ok("mustBePositive", vec![Value::Num(f64::INFINITY)]);
2909        ok("mustBeNegative", vec![Value::Num(f64::NEG_INFINITY)]);
2910        ok(
2911            "mustBeNonnegative",
2912            vec![Value::Complex(f64::INFINITY, 7.0)],
2913        );
2914        ok(
2915            "mustBeNonpositive",
2916            vec![Value::Complex(f64::NEG_INFINITY, 7.0)],
2917        );
2918    }
2919
2920    #[test]
2921    fn resident_secondary_validator_operands_require_coherent_metadata() {
2922        test_support::with_test_provider(|provider| {
2923            let value =
2924                Tensor::new_integer(IntegerStorage::U8(vec![2]), vec![1, 1]).expect("value");
2925            let lower =
2926                Tensor::new_integer(IntegerStorage::U8(vec![1]), vec![1, 1]).expect("lower");
2927            let value_handle =
2928                crate::builtins::common::gpu_helpers::upload_tensor(provider, &value)
2929                    .expect("upload value");
2930            let mut lower_handle =
2931                crate::builtins::common::gpu_helpers::upload_tensor(provider, &lower)
2932                    .expect("upload lower");
2933            lower_handle.descriptor.storage =
2934                Some(runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved);
2935            let error = dispatch_validator(
2936                "mustBeGreaterThan",
2937                vec![
2938                    Value::GpuTensor(value_handle.clone()),
2939                    Value::GpuTensor(lower_handle.clone()),
2940                ],
2941            )
2942            .expect_err("contradictory bound metadata must reject");
2943            assert_eq!(
2944                error.identifier(),
2945                Some("RunMat:validators:ProviderPayloadMismatch")
2946            );
2947            provider.free(&value_handle).ok();
2948            provider.free(&lower_handle).ok();
2949            runmat_accelerate_api::clear_handle_metadata(&value_handle);
2950            runmat_accelerate_api::clear_handle_metadata(&lower_handle);
2951        });
2952    }
2953
2954    #[test]
2955    fn threshold_validators_read_typed_sparse_and_complex_integer_storage() {
2956        let sparse =
2957            SparseTensor::new_integer(1, 2, vec![0, 1, 1], vec![0], IntegerStorage::U8(vec![1]))
2958                .unwrap();
2959        ok("mustBeNonnegative", vec![Value::SparseTensor(sparse)]);
2960
2961        let complex = ComplexTensor::new_integer(
2962            IntegerComplexStorage::new(IntegerStorage::I16(vec![2]), IntegerStorage::I16(vec![0]))
2963                .unwrap(),
2964            vec![1, 1],
2965        )
2966        .unwrap();
2967        ok("mustBePositive", vec![Value::ComplexTensor(complex)]);
2968    }
2969
2970    #[test]
2971    fn member_validator_accepts_numeric_and_text_sets() {
2972        let allowed = Tensor::new(vec![1.0, 3.0, 5.0], vec![1, 3]).unwrap();
2973        assert!(dispatch_validator(
2974            "mustBeMember",
2975            vec![Value::Num(3.0), Value::Tensor(allowed)]
2976        )
2977        .is_ok());
2978
2979        let allowed = StringArray::new(vec!["on".into(), "off".into()], vec![1, 2]).unwrap();
2980        assert!(dispatch_validator(
2981            "mustBeMember",
2982            vec![Value::String("on".into()), Value::StringArray(allowed)]
2983        )
2984        .is_ok());
2985    }
2986
2987    #[test]
2988    fn class_validators_use_native_integer_storage_metadata() {
2989        let dense = integer_tensor(
2990            IntegerStorage::U64(vec![u64::MAX, 9_007_199_254_740_993]),
2991            vec![1, 2],
2992        );
2993        ok(
2994            "mustBeA",
2995            vec![dense.clone(), Value::String("integer".into())],
2996        );
2997        err("mustBeFloat", vec![dense.clone()]);
2998        err("mustBeA", vec![dense, Value::String("double".into())]);
2999
3000        let sparse = Value::SparseTensor(
3001            SparseTensor::new_integer(
3002                2,
3003                2,
3004                vec![0, 1, 1],
3005                vec![1],
3006                IntegerStorage::I64(vec![i64::MIN]),
3007            )
3008            .unwrap(),
3009        );
3010        ok(
3011            "mustBeA",
3012            vec![sparse.clone(), Value::String("integer".into())],
3013        );
3014        err("mustBeFloat", vec![sparse.clone()]);
3015        err("mustBeA", vec![sparse, Value::String("double".into())]);
3016
3017        let typed_complex = Value::ComplexTensor(
3018            ComplexTensor::new_integer(
3019                IntegerComplexStorage::new(
3020                    IntegerStorage::I16(vec![1]),
3021                    IntegerStorage::I16(vec![2]),
3022                )
3023                .unwrap(),
3024                vec![1, 1],
3025            )
3026            .unwrap(),
3027        );
3028        ok(
3029            "mustBeA",
3030            vec![typed_complex.clone(), Value::String("integer".into())],
3031        );
3032        err("mustBeFloat", vec![typed_complex]);
3033    }
3034
3035    #[test]
3036    fn class_validators_use_gpu_integer_metadata_without_gather() {
3037        use crate::builtins::common::test_support;
3038        use runmat_accelerate_api::{HostIntegerDataView, HostIntegerTensorView};
3039
3040        test_support::with_test_provider(|provider| {
3041            let values = [u64::MAX, 9_007_199_254_740_993];
3042            let shape = [1usize, 2usize];
3043            let handle = provider
3044                .upload_integer(&HostIntegerTensorView {
3045                    data: HostIntegerDataView::U64(&values),
3046                    shape: &shape,
3047                })
3048                .expect("upload integer gpu tensor");
3049            let gpu = Value::GpuTensor(handle);
3050
3051            ok(
3052                "mustBeA",
3053                vec![gpu.clone(), Value::String("integer".into())],
3054            );
3055            ok("mustBeInteger", vec![gpu.clone()]);
3056            err("mustBeFloat", vec![gpu.clone()]);
3057            err("mustBeA", vec![gpu, Value::String("double".into())]);
3058        });
3059    }
3060
3061    #[test]
3062    fn must_be_a_distinguishes_explicit_gpuarray_from_automatic_residency() {
3063        test_support::with_test_provider(|provider| {
3064            let tensor = Tensor::new_integer(IntegerStorage::U16(vec![7]), vec![1, 1])
3065                .expect("integer source");
3066            let handle = crate::builtins::common::gpu_helpers::upload_tensor(provider, &tensor)
3067                .expect("upload integer source");
3068            let handle =
3069                handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
3070            ok(
3071                "mustBeA",
3072                vec![
3073                    Value::GpuTensor(handle.clone()),
3074                    Value::String("uint16".into()),
3075                ],
3076            );
3077            err(
3078                "mustBeA",
3079                vec![
3080                    Value::GpuTensor(handle.clone()),
3081                    Value::String("gpuArray".into()),
3082                ],
3083            );
3084            let handle =
3085                handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
3086            ok(
3087                "mustBeA",
3088                vec![
3089                    Value::GpuTensor(handle.clone()),
3090                    Value::String("gpuArray".into()),
3091                ],
3092            );
3093            err(
3094                "mustBeA",
3095                vec![
3096                    Value::GpuTensor(handle.clone()),
3097                    Value::String("uint16".into()),
3098                ],
3099            );
3100            provider.free(&handle).expect("free resident source");
3101            runmat_accelerate_api::clear_handle_metadata(&handle);
3102        });
3103    }
3104
3105    #[test]
3106    fn member_validator_compares_native_integers_exactly() {
3107        let wide = 9_007_199_254_740_993_u64;
3108        let adjacent = wide - 1;
3109        assert_eq!(wide as f64, adjacent as f64);
3110
3111        let allowed = Tensor::new_integer(IntegerStorage::U64(vec![wide]), vec![1, 1]).unwrap();
3112        ok(
3113            "mustBeMember",
3114            vec![
3115                Value::Int(IntValue::U64(wide)),
3116                Value::Tensor(allowed.clone()),
3117            ],
3118        );
3119        err(
3120            "mustBeMember",
3121            vec![Value::Int(IntValue::U64(adjacent)), Value::Tensor(allowed)],
3122        );
3123
3124        let sparse_allowed = SparseTensor::new_integer(
3125            2,
3126            2,
3127            vec![0, 1, 1],
3128            vec![1],
3129            IntegerStorage::U64(vec![wide]),
3130        )
3131        .unwrap();
3132        ok(
3133            "mustBeMember",
3134            vec![
3135                Value::Int(IntValue::U64(0)),
3136                Value::SparseTensor(sparse_allowed),
3137            ],
3138        );
3139    }
3140
3141    #[test]
3142    fn member_validator_does_not_equate_wide_integer_with_rounded_double() {
3143        let wide = 9_007_199_254_740_993_u64;
3144        let rounded = (wide - 1) as f64;
3145        assert_eq!(wide as f64, rounded);
3146
3147        err(
3148            "mustBeMember",
3149            vec![Value::Int(IntValue::U64(wide)), Value::Num(rounded)],
3150        );
3151    }
3152
3153    #[test]
3154    fn text_and_varname_validators_follow_core_shapes() {
3155        assert!(dispatch_validator(
3156            "mustBeNonzeroLengthText",
3157            vec![Value::CharArray(CharArray::new_row("alpha"))]
3158        )
3159        .is_ok());
3160        assert!(isvarname_value(&Value::String("alpha_1".into())));
3161        assert!(!isvarname_value(&Value::String("1alpha".into())));
3162    }
3163
3164    #[test]
3165    fn isvarname_returns_false_for_all_integer_classes() {
3166        for value in [
3167            IntValue::I8(-1),
3168            IntValue::I16(-2),
3169            IntValue::I32(-3),
3170            IntValue::I64(i64::MIN),
3171            IntValue::U8(1),
3172            IntValue::U16(2),
3173            IntValue::U32(3),
3174            IntValue::U64(u64::MAX),
3175        ] {
3176            assert!(!isvarname_value(&Value::Int(value)));
3177        }
3178    }
3179
3180    #[test]
3181    fn isvarname_returns_false_for_resident_integer_without_gather() {
3182        test_support::with_test_provider(|provider| {
3183            let tensor = Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1])
3184                .expect("integer tensor");
3185            let handle = crate::builtins::common::gpu_helpers::upload_tensor(provider, &tensor)
3186                .expect("upload integer");
3187            assert!(!isvarname_value(&Value::GpuTensor(handle)));
3188        });
3189    }
3190
3191    #[test]
3192    fn namedargs2cell_preserves_field_order() {
3193        let mut st = StructValue::new();
3194        st.insert("Name", Value::String("Ada".into()));
3195        st.insert("Value", Value::Num(7.0));
3196        let out = namedargs2cell_value(Value::Struct(st)).expect("namedargs2cell");
3197        let Value::Cell(cell) = out else {
3198            panic!("expected cell");
3199        };
3200        assert_eq!(cell.rows, 1);
3201        assert_eq!(cell.cols, 4);
3202        assert_eq!(cell.data[0], Value::String("Name".into()));
3203        assert_eq!(cell.data[2], Value::String("Value".into()));
3204    }
3205
3206    #[test]
3207    fn namedargs2cell_rejects_top_level_integers_and_preserves_integer_fields_exactly() {
3208        let cases = [
3209            IntValue::I8(i8::MIN),
3210            IntValue::I16(i16::MIN),
3211            IntValue::I32(i32::MIN),
3212            IntValue::I64(i64::MIN),
3213            IntValue::U8(u8::MAX),
3214            IntValue::U16(u16::MAX),
3215            IntValue::U32(u32::MAX),
3216            IntValue::U64(u64::MAX),
3217        ];
3218        for value in cases {
3219            assert!(namedargs2cell_value(Value::Int(value.clone())).is_err());
3220            let mut structure = StructValue::new();
3221            structure.insert("Exact", Value::Int(value.clone()));
3222            let output = namedargs2cell_value(Value::Struct(structure))
3223                .expect("scalar struct with integer field");
3224            let Value::Cell(cell) = output else {
3225                panic!("expected name-value cell");
3226            };
3227            assert_eq!(cell.data[1], Value::Int(value));
3228        }
3229
3230        let resident = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
3231            shape: vec![1, 1],
3232            device_id: 0,
3233            buffer_id: 9_419_006,
3234            descriptor: Default::default(),
3235        });
3236        assert!(namedargs2cell_value(resident).is_err());
3237    }
3238
3239    #[test]
3240    fn validator_surface_accepts_and_rejects_representative_values() {
3241        let temp_dir = tempfile::tempdir().unwrap();
3242        let file_path = temp_dir.path().join("data.txt");
3243        std::fs::write(&file_path, "ok").unwrap();
3244        let dir_text = Value::String(temp_dir.path().to_string_lossy().into_owned());
3245        let file_text = Value::String(file_path.to_string_lossy().into_owned());
3246
3247        ok(
3248            "mustBeA",
3249            vec![Value::Num(1.0), Value::String("double".into())],
3250        );
3251        err(
3252            "mustBeA",
3253            vec![Value::String("x".into()), Value::String("double".into())],
3254        );
3255        ok("mustBeColumn", vec![tensor(vec![1.0, 2.0], 2, 1)]);
3256        err("mustBeColumn", vec![tensor(vec![1.0, 2.0], 1, 2)]);
3257        ok("mustBeFile", vec![file_text.clone()]);
3258        err("mustBeFile", vec![dir_text.clone()]);
3259        ok("mustBeFolder", vec![dir_text.clone()]);
3260        err("mustBeFolder", vec![file_text.clone()]);
3261        ok("mustBeFinite", vec![tensor(vec![1.0, 2.0], 1, 2)]);
3262        err("mustBeFinite", vec![Value::Num(f64::INFINITY)]);
3263        ok("mustBeFloat", vec![Value::Num(1.0)]);
3264        err("mustBeFloat", vec![Value::Int(IntValue::I32(1))]);
3265        ok("mustBeInteger", vec![tensor(vec![1.0, 2.0], 1, 2)]);
3266        ok("mustBeInteger", vec![Value::Bool(true)]);
3267        ok(
3268            "mustBeInteger",
3269            vec![Value::LogicalArray(
3270                LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap(),
3271            )],
3272        );
3273        err("mustBeInteger", vec![Value::Num(1.5)]);
3274        ok("mustBeNumeric", vec![Value::Complex(1.0, 2.0)]);
3275        err("mustBeNumeric", vec![Value::String("1".into())]);
3276        ok(
3277            "mustBeNumericOrLogical",
3278            vec![Value::LogicalArray(
3279                LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap(),
3280            )],
3281        );
3282        err("mustBeNumericOrLogical", vec![Value::String("true".into())]);
3283        ok("mustBeReal", vec![Value::Complex(1.0, 0.0)]);
3284        err("mustBeReal", vec![Value::Complex(1.0, 1.0)]);
3285        ok("mustBeVector", vec![tensor(vec![1.0, 2.0], 1, 2)]);
3286        err("mustBeVector", vec![tensor(vec![1.0, 2.0, 3.0, 4.0], 2, 2)]);
3287        ok("mustBeScalarOrEmpty", vec![Value::Num(1.0)]);
3288        err("mustBeScalarOrEmpty", vec![tensor(vec![1.0, 2.0], 1, 2)]);
3289        ok("mustBeSparse", vec![sparse(vec![1.0])]);
3290        err("mustBeSparse", vec![Value::Num(1.0)]);
3291        ok("mustBeNonsparse", vec![Value::Num(1.0)]);
3292        err("mustBeNonsparse", vec![sparse(vec![1.0])]);
3293        ok(
3294            "mustBeText",
3295            vec![Value::CharArray(CharArray::new_row("abc"))],
3296        );
3297        err("mustBeText", vec![Value::Num(1.0)]);
3298        ok("mustBeTextScalar", vec![Value::String("abc".into())]);
3299        err(
3300            "mustBeTextScalar",
3301            vec![Value::StringArray(
3302                StringArray::new_2d(vec!["a".into(), "b".into()], 1, 2).unwrap(),
3303            )],
3304        );
3305        ok(
3306            "mustBeNonzeroLengthText",
3307            vec![Value::StringArray(
3308                StringArray::new_2d(vec!["a".into(), "b".into()], 1, 2).unwrap(),
3309            )],
3310        );
3311        err(
3312            "mustBeNonzeroLengthText",
3313            vec![Value::String(String::new())],
3314        );
3315        ok("mustBeNonempty", vec![Value::String("x".into())]);
3316        err(
3317            "mustBeNonempty",
3318            vec![Value::StringArray(
3319                StringArray::new_2d(vec![], 0, 0).unwrap(),
3320            )],
3321        );
3322        ok("mustBeNonmissing", vec![Value::Num(1.0)]);
3323        err("mustBeNonmissing", vec![Value::Num(f64::NAN)]);
3324        ok("mustBeNonNan", vec![Value::Complex(1.0, 0.0)]);
3325        err("mustBeNonNan", vec![Value::Complex(f64::NAN, 0.0)]);
3326        ok(
3327            "mustBeUnderlyingType",
3328            vec![Value::Int(IntValue::I16(1)), Value::String("int16".into())],
3329        );
3330        err(
3331            "mustBeUnderlyingType",
3332            vec![Value::Bool(true), Value::String("double".into())],
3333        );
3334        ok(
3335            "mustBeValidVariableName",
3336            vec![Value::String("alpha_1".into())],
3337        );
3338        err(
3339            "mustBeValidVariableName",
3340            vec![Value::String("_alpha".into())],
3341        );
3342        ok(
3343            "mustBeMember",
3344            vec![
3345                Value::String("on".into()),
3346                Value::Cell(
3347                    CellArray::new(
3348                        vec![Value::String("on".into()), Value::String("off".into())],
3349                        1,
3350                        2,
3351                    )
3352                    .unwrap(),
3353                ),
3354            ],
3355        );
3356        err(
3357            "mustBeMember",
3358            vec![
3359                Value::String("bad".into()),
3360                Value::Cell(
3361                    CellArray::new(
3362                        vec![Value::String("on".into()), Value::String("off".into())],
3363                        1,
3364                        2,
3365                    )
3366                    .unwrap(),
3367                ),
3368            ],
3369        );
3370    }
3371
3372    #[test]
3373    fn numeric_threshold_validators_cover_boundaries() {
3374        ok("mustBePositive", vec![Value::Num(1.0)]);
3375        ok("mustBePositive", vec![Value::Bool(true)]);
3376        err("mustBePositive", vec![Value::Num(0.0)]);
3377        ok("mustBeNegative", vec![Value::Num(-1.0)]);
3378        err("mustBeNegative", vec![Value::Num(0.0)]);
3379        ok("mustBeNonnegative", vec![Value::Num(0.0)]);
3380        ok(
3381            "mustBeNonnegative",
3382            vec![Value::LogicalArray(
3383                LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap(),
3384            )],
3385        );
3386        err("mustBeNonnegative", vec![Value::Num(-1.0)]);
3387        ok("mustBeNonpositive", vec![Value::Num(0.0)]);
3388        err("mustBeNonpositive", vec![Value::Num(1.0)]);
3389        ok("mustBeNonzero", vec![Value::Complex(0.0, 1.0)]);
3390        err("mustBeNonzero", vec![Value::Num(0.0)]);
3391        ok("mustBeGreaterThan", vec![Value::Num(2.0), Value::Num(1.0)]);
3392        err("mustBeGreaterThan", vec![Value::Num(1.0), Value::Num(1.0)]);
3393        ok(
3394            "mustBeGreaterThanOrEqual",
3395            vec![Value::Num(1.0), Value::Num(1.0)],
3396        );
3397        err(
3398            "mustBeGreaterThanOrEqual",
3399            vec![Value::Num(0.0), Value::Num(1.0)],
3400        );
3401        ok("mustBeLessThan", vec![Value::Num(0.0), Value::Num(1.0)]);
3402        err("mustBeLessThan", vec![Value::Num(1.0), Value::Num(1.0)]);
3403        ok(
3404            "mustBeLessThanOrEqual",
3405            vec![Value::Num(1.0), Value::Num(1.0)],
3406        );
3407        err(
3408            "mustBeLessThanOrEqual",
3409            vec![Value::Num(2.0), Value::Num(1.0)],
3410        );
3411    }
3412
3413    #[test]
3414    fn threshold_validators_read_typed_integer_storage_exactly() {
3415        let lower = Tensor::new_integer(IntegerStorage::U16(vec![1]), vec![1, 1]).expect("lower");
3416        ok(
3417            "mustBeGreaterThan",
3418            vec![Value::Num(2.0), Value::Tensor(lower)],
3419        );
3420
3421        let upper = Tensor::new_integer(IntegerStorage::U16(vec![3]), vec![1, 1]).expect("upper");
3422        ok(
3423            "mustBeLessThan",
3424            vec![Value::Num(2.0), Value::Tensor(upper)],
3425        );
3426
3427        let range_lower =
3428            Tensor::new_integer(IntegerStorage::U16(vec![1]), vec![1, 1]).expect("range lower");
3429        let range_upper =
3430            Tensor::new_integer(IntegerStorage::U16(vec![3]), vec![1, 1]).expect("range upper");
3431        ok(
3432            "mustBeInRange",
3433            vec![
3434                Value::Tensor(
3435                    Tensor::new_integer(IntegerStorage::U16(vec![2]), vec![1, 1])
3436                        .expect("range value"),
3437                ),
3438                Value::Tensor(range_lower),
3439                Value::Tensor(range_upper),
3440            ],
3441        );
3442
3443        let wide = 9_007_199_254_740_993_u64;
3444        let adjacent = wide - 1;
3445        let rounded = adjacent as f64;
3446        assert_eq!(wide as f64, rounded);
3447
3448        let wide_value =
3449            Tensor::new_integer(IntegerStorage::U64(vec![wide]), vec![1, 1]).expect("wide value");
3450        ok(
3451            "mustBeGreaterThan",
3452            vec![Value::Tensor(wide_value.clone()), Value::Num(rounded)],
3453        );
3454        err(
3455            "mustBeLessThanOrEqual",
3456            vec![Value::Tensor(wide_value.clone()), Value::Num(rounded)],
3457        );
3458        err(
3459            "mustBeInRange",
3460            vec![
3461                Value::Tensor(wide_value.clone()),
3462                Value::Tensor(
3463                    Tensor::new_integer(IntegerStorage::U64(vec![0]), vec![1, 1])
3464                        .expect("wide lower"),
3465                ),
3466                Value::Tensor(
3467                    Tensor::new_integer(IntegerStorage::U64(vec![adjacent]), vec![1, 1])
3468                        .expect("wide upper"),
3469                ),
3470            ],
3471        );
3472
3473        let complex_value = ComplexTensor::new_integer(
3474            IntegerComplexStorage::new(
3475                IntegerStorage::U64(vec![wide]),
3476                IntegerStorage::U64(vec![0]),
3477            )
3478            .expect("complex integer storage"),
3479            vec![1, 1],
3480        )
3481        .expect("complex integer tensor");
3482        ok(
3483            "mustBeGreaterThan",
3484            vec![Value::ComplexTensor(complex_value), Value::Num(rounded)],
3485        );
3486
3487        let sparse_value = Value::SparseTensor(
3488            SparseTensor::new_integer(1, 1, vec![0, 1], vec![0], IntegerStorage::U64(vec![wide]))
3489                .expect("sparse integer value"),
3490        );
3491        ok(
3492            "mustBeGreaterThan",
3493            vec![sparse_value.clone(), Value::Num(rounded)],
3494        );
3495        err(
3496            "mustBeInRange",
3497            vec![
3498                sparse_value,
3499                Value::SparseTensor(
3500                    SparseTensor::new_integer(
3501                        1,
3502                        1,
3503                        vec![0, 0],
3504                        vec![],
3505                        IntegerStorage::U64(vec![]),
3506                    )
3507                    .expect("sparse lower"),
3508                ),
3509                Value::SparseTensor(
3510                    SparseTensor::new_integer(
3511                        1,
3512                        1,
3513                        vec![0, 1],
3514                        vec![0],
3515                        IntegerStorage::U64(vec![adjacent]),
3516                    )
3517                    .expect("sparse upper"),
3518                ),
3519            ],
3520        );
3521    }
3522
3523    #[test]
3524    fn in_range_supports_interval_flags_and_rejects_extra_inputs() {
3525        ok(
3526            "mustBeInRange",
3527            vec![Value::Num(1.0), Value::Num(1.0), Value::Num(2.0)],
3528        );
3529        err(
3530            "mustBeInRange",
3531            vec![
3532                Value::Num(1.0),
3533                Value::Num(1.0),
3534                Value::Num(2.0),
3535                Value::String("exclusive".into()),
3536            ],
3537        );
3538        ok(
3539            "mustBeInRange",
3540            vec![
3541                Value::Num(1.5),
3542                Value::Num(1.0),
3543                Value::Num(2.0),
3544                Value::String("exclusive".into()),
3545            ],
3546        );
3547        err(
3548            "mustBeInRange",
3549            vec![
3550                Value::Num(1.0),
3551                Value::Num(1.0),
3552                Value::Num(2.0),
3553                Value::String("exclusive".into()),
3554                Value::String("inclusive".into()),
3555            ],
3556        );
3557        ok(
3558            "mustBeInRange",
3559            vec![
3560                Value::Num(2.0),
3561                Value::Num(1.0),
3562                Value::Num(2.0),
3563                Value::String("exclude-lower".into()),
3564            ],
3565        );
3566        err(
3567            "mustBeInRange",
3568            vec![
3569                Value::Num(2.0),
3570                Value::Num(1.0),
3571                Value::Num(2.0),
3572                Value::String("inclusive".into()),
3573                Value::String("exclusive".into()),
3574                Value::String("extra".into()),
3575            ],
3576        );
3577    }
3578
3579    #[test]
3580    fn varname_rules_reject_keywords_underscores_and_overlong_names() {
3581        assert!(isvarname_value(&Value::String("alpha_1".into())));
3582        assert!(!isvarname_value(&Value::String("_alpha".into())));
3583        assert!(!isvarname_value(&Value::String("1alpha".into())));
3584        assert!(!isvarname_value(&Value::String("for".into())));
3585        assert!(!isvarname_value(&Value::String("end".into())));
3586        assert!(isvarname_value(&Value::String(
3587            "a".repeat(MATLAB_NAME_LENGTH_MAX)
3588        )));
3589        assert!(!isvarname_value(&Value::String(
3590            "a".repeat(MATLAB_NAME_LENGTH_MAX + 1)
3591        )));
3592    }
3593
3594    #[test]
3595    fn callable_validators_reject_unexpected_extra_arguments() {
3596        err("mustBePositive", vec![Value::Num(1.0), Value::Num(2.0)]);
3597        err("mustBeMember", vec![Value::String("on".into())]);
3598        err(
3599            "mustBeA",
3600            vec![
3601                Value::Num(1.0),
3602                Value::String("double".into()),
3603                Value::String("extra".into()),
3604            ],
3605        );
3606    }
3607
3608    #[test]
3609    fn ordered_validators_support_exact_compatible_integer_bounds() {
3610        let value = integer_tensor(
3611            IntegerStorage::U64(vec![9_007_199_254_740_993, 4]),
3612            vec![2, 1],
3613        );
3614        let bounds = integer_tensor(
3615            IntegerStorage::U64(vec![9_007_199_254_740_992, 3]),
3616            vec![2, 1],
3617        );
3618        ok("mustBeGreaterThan", vec![value.clone(), bounds]);
3619        err(
3620            "mustBeLessThanOrEqual",
3621            vec![value, Value::Num(9_007_199_254_740_992.0)],
3622        );
3623
3624        let matrix = integer_tensor(IntegerStorage::I16(vec![1, 2]), vec![2, 1]);
3625        let row_bounds = integer_tensor(IntegerStorage::I16(vec![0, 0]), vec![1, 2]);
3626        ok("mustBeGreaterThan", vec![matrix, row_bounds]);
3627
3628        let huge_sparse = SparseTensor::new_integer(
3629            1_000_000,
3630            1_000_000,
3631            {
3632                let mut pointers = vec![0; 1_000_001];
3633                pointers[1..].fill(1);
3634                pointers
3635            },
3636            vec![0],
3637            IntegerStorage::I64(vec![1]),
3638        )
3639        .expect("huge sparse sentinel");
3640        ok("mustBeNonnegative", vec![Value::SparseTensor(huge_sparse)]);
3641    }
3642
3643    #[test]
3644    fn must_be_in_range_requires_same_class_and_compares_wide_integers_exactly() {
3645        let value = integer_tensor(IntegerStorage::U64(vec![9_007_199_254_740_993]), vec![1, 1]);
3646        let lower = integer_tensor(IntegerStorage::U64(vec![9_007_199_254_740_993]), vec![1, 1]);
3647        let upper = integer_tensor(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1]);
3648        ok("mustBeInRange", vec![value.clone(), lower, upper]);
3649        err(
3650            "mustBeInRange",
3651            vec![value, Value::Num(0.0), Value::Num(f64::INFINITY)],
3652        );
3653    }
3654
3655    #[test]
3656    fn must_be_member_enforces_nondouble_class_compatibility_without_rounding() {
3657        let wide = integer_tensor(IntegerStorage::U64(vec![9_007_199_254_740_993]), vec![1, 1]);
3658        let rounded_double = tensor(vec![9_007_199_254_740_992.0], 1, 1);
3659        err("mustBeMember", vec![wide.clone(), rounded_double]);
3660        let exact = integer_tensor(IntegerStorage::U64(vec![9_007_199_254_740_993]), vec![1, 1]);
3661        ok("mustBeMember", vec![wide.clone(), exact]);
3662        let unlike = integer_tensor(IntegerStorage::I64(vec![9]), vec![1, 1]);
3663        err("mustBeMember", vec![wide, unlike]);
3664    }
3665
3666    #[test]
3667    fn resident_content_validators_download_exactly_and_preserve_source() {
3668        use futures::executor::block_on;
3669
3670        test_support::with_test_provider(|provider| {
3671            let tensor = Tensor::new_integer(IntegerStorage::I64(vec![-1, 2]), vec![2, 1])
3672                .expect("resident integer");
3673            let handle = crate::builtins::common::gpu_helpers::upload_tensor(provider, &tensor)
3674                .expect("upload resident integer");
3675            let handle =
3676                handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
3677            let value = Value::GpuTensor(handle.clone());
3678            assert!(block_on(dispatch_validator_async(
3679                "mustBeNonzero",
3680                vec![value.clone()],
3681            ))
3682            .is_ok());
3683            assert!(block_on(dispatch_validator_async("mustBePositive", vec![value],)).is_err());
3684            assert!(
3685                crate::builtins::common::gpu_helpers::exact_provider_for_handle(&handle).is_some()
3686            );
3687            let gathered = block_on(provider.download_integer(&handle)).expect("source survives");
3688            assert_eq!(
3689                gathered.data,
3690                runmat_accelerate_api::HostIntegerDataOwned::I64(vec![-1, 2])
3691            );
3692            provider.free(&handle).expect("free resident source");
3693            runmat_accelerate_api::clear_handle_metadata(&handle);
3694        });
3695    }
3696
3697    #[cfg(feature = "wgpu")]
3698    #[test]
3699    fn wgpu_resident_integer_validators_preserve_exact_source_and_provenance() {
3700        use futures::executor::block_on;
3701        use runmat_accelerate_api::AccelProvider;
3702
3703        let _lock = test_support::accel_test_lock();
3704        let provider = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
3705            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
3706        )
3707        .expect("register WGPU provider for integer validator coverage");
3708        let _provider = runmat_accelerate_api::ThreadProviderGuard::set(Some(provider));
3709        let tensor = Tensor::new_integer(
3710            IntegerStorage::U64(vec![9_007_199_254_740_993, u64::MAX]),
3711            vec![1, 2],
3712        )
3713        .expect("wide integer source");
3714        let handle = crate::builtins::common::gpu_helpers::upload_tensor(provider, &tensor)
3715            .expect("upload wide integer source");
3716        let handle = handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
3717        let value = Value::GpuTensor(handle.clone());
3718
3719        for name in [
3720            "mustBeFinite",
3721            "mustBeInteger",
3722            "mustBeNonNan",
3723            "mustBeNonzero",
3724            "mustBePositive",
3725        ] {
3726            block_on(dispatch_validator_async(name, vec![value.clone()]))
3727                .unwrap_or_else(|error| panic!("{name} unexpectedly failed: {error}"));
3728        }
3729        let handle = handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
3730        let value = Value::GpuTensor(handle.clone());
3731        let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
3732        for name in ["mustBeReal", "mustBeVector"] {
3733            block_on(dispatch_validator_async(name, vec![value.clone()]))
3734                .unwrap_or_else(|error| panic!("{name} unexpectedly failed: {error}"));
3735        }
3736        block_on(dispatch_validator_async(
3737            "mustBeUnderlyingType",
3738            vec![value.clone(), Value::String("uint64".into())],
3739        ))
3740        .expect("resident underlying type metadata");
3741        assert!(block_on(dispatch_validator_async(
3742            "mustBeScalarOrEmpty",
3743            vec![value.clone()],
3744        ))
3745        .is_err());
3746        assert!(block_on(dispatch_validator_async(
3747            "mustBeSparse",
3748            vec![value.clone()],
3749        ))
3750        .is_err());
3751        for name in ["mustBeText", "mustBeTextScalar", "mustBeValidVariableName"] {
3752            let error = block_on(dispatch_validator_async(name, vec![value.clone()]))
3753                .expect_err("resident integer must reject text validation");
3754            assert_eq!(error.gpu_gather_retry(), crate::GpuGatherRetry::Never);
3755        }
3756        assert_eq!(
3757            runmat_accelerate_api::handle_provenance(&handle),
3758            Some(runmat_accelerate_api::GpuHandleProvenance::Explicit)
3759        );
3760        let gathered = block_on(provider.download_integer(&handle)).expect("source survives");
3761        assert_eq!(
3762            gathered.data,
3763            runmat_accelerate_api::HostIntegerDataOwned::U64(
3764                vec![9_007_199_254_740_993, u64::MAX,]
3765            )
3766        );
3767        provider.free(&handle).expect("free source");
3768        runmat_accelerate_api::clear_handle_metadata(&handle);
3769    }
3770
3771    #[test]
3772    fn undocumented_explicit_resident_validators_are_mode_gated() {
3773        test_support::with_test_provider(|provider| {
3774            let tensor = Tensor::new_integer(IntegerStorage::U8(vec![1]), vec![1, 1])
3775                .expect("resident integer");
3776            let handle = crate::builtins::common::gpu_helpers::upload_tensor(provider, &tensor)
3777                .expect("upload resident integer");
3778            let handle =
3779                handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
3780            let value = Value::GpuTensor(handle.clone());
3781            let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
3782            for name in [
3783                "mustBeFinite",
3784                "mustBeInteger",
3785                "mustBeNonNan",
3786                "mustBeNonzero",
3787            ] {
3788                let error = dispatch_validator(name, vec![value.clone()])
3789                    .expect_err("explicit resident extension must reject in compatibility mode");
3790                assert_eq!(error.gpu_gather_retry(), crate::GpuGatherRetry::Never);
3791            }
3792            drop(_strict);
3793            let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
3794            for name in [
3795                "mustBeFinite",
3796                "mustBeInteger",
3797                "mustBeNonNan",
3798                "mustBeNonzero",
3799            ] {
3800                ok(name, vec![value.clone()]);
3801            }
3802            provider.free(&handle).expect("free resident source");
3803            runmat_accelerate_api::clear_handle_metadata(&handle);
3804        });
3805    }
3806
3807    #[test]
3808    fn validate_function_signatures_json_checks_json_syntax() {
3809        assert_eq!(
3810            VALIDATE_FUNCTION_SIGNATURES_JSON_INTEGER_AUDIT.kind,
3811            BuiltinIntegerAuditKind::NotApplicable
3812        );
3813        ok(
3814            "validateFunctionSignaturesJSON",
3815            vec![Value::String(r#"{"functions":[]}"#.into())],
3816        );
3817        err(
3818            "validateFunctionSignaturesJSON",
3819            vec![Value::String("{not json}".into())],
3820        );
3821        err(
3822            "validateFunctionSignaturesJSON",
3823            vec![Value::Int(IntValue::U64(u64::MAX))],
3824        );
3825        let resident = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
3826            shape: vec![1, 1],
3827            device_id: u32::MAX,
3828            buffer_id: u64::MAX,
3829            descriptor: Default::default(),
3830        });
3831        let error = dispatch_validator("validateFunctionSignaturesJSON", vec![resident])
3832            .expect_err("resident numeric input must reject as invalid text");
3833        assert_eq!(error.gpu_gather_retry(), crate::GpuGatherRetry::Never);
3834    }
3835}