1use 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
37pub 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
47pub 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
58pub 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
1603pub 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
1626pub 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
1668pub 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
1754pub 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}