Skip to main content

runmat_runtime/builtins/common/
validation.rs

1//! Shared MATLAB argument-validation helpers and callable `mustBe*` builtins.
2
3use std::path::Path;
4
5use runmat_accelerate_api::handle_is_logical;
6use runmat_builtins::{
7    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
8    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
9    CellArray, CharArray, NumericDType, SparseTensor, Value,
10};
11use runmat_macros::runtime_builtin;
12
13use crate::builtins::common::identifiers::is_valid_varname;
14use crate::builtins::introspection::class::class_name_for_value;
15use crate::builtins::introspection::underlying_type::underlying_type_matches;
16use crate::{build_runtime_error, BuiltinResult, RuntimeError};
17
18#[derive(Debug, Clone, PartialEq)]
19pub enum ValidationAtom {
20    Number(f64),
21    Text(String),
22    Bool(bool),
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
26pub struct RangeInclusivity {
27    pub lower: bool,
28    pub upper: bool,
29}
30
31impl RangeInclusivity {
32    pub const CLOSED: Self = Self {
33        lower: true,
34        upper: true,
35    };
36
37    pub const OPEN: Self = Self {
38        lower: false,
39        upper: false,
40    };
41
42    pub const OPEN_LEFT: Self = Self {
43        lower: false,
44        upper: true,
45    };
46
47    pub const OPEN_RIGHT: Self = Self {
48        lower: true,
49        upper: false,
50    };
51}
52
53const VALUE_INPUT: BuiltinParamDescriptor = BuiltinParamDescriptor {
54    name: "A",
55    ty: BuiltinParamType::Any,
56    arity: BuiltinParamArity::Required,
57    default: None,
58    description: "Value to validate.",
59};
60
61const EXTRA_INPUT: BuiltinParamDescriptor = BuiltinParamDescriptor {
62    name: "B",
63    ty: BuiltinParamType::Any,
64    arity: BuiltinParamArity::Variadic,
65    default: None,
66    description: "Additional validator-specific argument.",
67};
68
69const PREDICATE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
70    name: "tf",
71    ty: BuiltinParamType::LogicalArray,
72    arity: BuiltinParamArity::Required,
73    default: None,
74    description: "Validation result.",
75}];
76
77const VALIDATOR_INPUTS: [BuiltinParamDescriptor; 2] = [VALUE_INPUT, EXTRA_INPUT];
78const VALIDATOR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
79    label: "mustBe*(A, ...)",
80    inputs: &VALIDATOR_INPUTS,
81    outputs: &[],
82}];
83
84const PREDICATE_INPUTS: [BuiltinParamDescriptor; 1] = [VALUE_INPUT];
85const ISVARNAME_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
86    label: "tf = isvarname(S)",
87    inputs: &PREDICATE_INPUTS,
88    outputs: &PREDICATE_OUTPUT,
89}];
90
91const NAMEDARGS_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
92    label: "C = namedargs2cell(S)",
93    inputs: &PREDICATE_INPUTS,
94    outputs: &[BuiltinParamDescriptor {
95        name: "C",
96        ty: BuiltinParamType::Any,
97        arity: BuiltinParamArity::Required,
98        default: None,
99        description: "Cell row vector of alternating field names and values.",
100    }],
101}];
102
103const VALIDATION_ERROR_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
104    code: "RM.ARGUMENT_VALIDATION.FAILED",
105    identifier: Some("RunMat:validators:ValidationFailed"),
106    when: "A value does not satisfy the requested validator.",
107    message: "argument validation failed",
108};
109
110const VALIDATION_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
111    code: "RM.ARGUMENT_VALIDATION.INVALID_ARGUMENT",
112    identifier: Some("RunMat:validators:InvalidArgument"),
113    when: "A validator receives an unsupported argument count or argument type.",
114    message: "invalid argument validation input",
115};
116
117const VALIDATION_ERRORS: [BuiltinErrorDescriptor; 2] =
118    [VALIDATION_ERROR_FAILED, VALIDATION_ERROR_INVALID_ARGUMENT];
119
120pub const VALIDATOR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
121    signatures: &VALIDATOR_SIGNATURES,
122    output_mode: BuiltinOutputMode::Fixed,
123    completion_policy: BuiltinCompletionPolicy::Public,
124    errors: &VALIDATION_ERRORS,
125};
126
127pub const ISVARNAME_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
128    signatures: &ISVARNAME_SIGNATURES,
129    output_mode: BuiltinOutputMode::Fixed,
130    completion_policy: BuiltinCompletionPolicy::Public,
131    errors: &[],
132};
133
134pub const NAMEDARGS2CELL_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
135    signatures: &NAMEDARGS_SIGNATURES,
136    output_mode: BuiltinOutputMode::Fixed,
137    completion_policy: BuiltinCompletionPolicy::Public,
138    errors: &VALIDATION_ERRORS,
139};
140
141pub fn validation_error(builtin: &str, detail: impl AsRef<str>) -> RuntimeError {
142    let detail = detail.as_ref();
143    let message = if detail.is_empty() {
144        format!("{builtin}: validation failed")
145    } else {
146        format!("{builtin}: {detail}")
147    };
148    build_runtime_error(message)
149        .with_builtin(builtin)
150        .with_identifier(format!("RunMat:{builtin}:ValidationFailed"))
151        .build()
152}
153
154fn invalid_argument_error(builtin: &str, detail: impl AsRef<str>) -> RuntimeError {
155    let detail = detail.as_ref();
156    let message = if detail.is_empty() {
157        format!("{builtin}: invalid argument")
158    } else {
159        format!("{builtin}: {detail}")
160    };
161    build_runtime_error(message)
162        .with_builtin(builtin)
163        .with_identifier(format!("RunMat:{builtin}:InvalidArgument"))
164        .build()
165}
166
167fn pass() -> BuiltinResult<Value> {
168    Ok(Value::Num(0.0))
169}
170
171fn require_args<'a>(
172    builtin: &str,
173    args: &'a [Value],
174    min: usize,
175    max: usize,
176) -> BuiltinResult<&'a Value> {
177    if args.len() < min || args.len() > max {
178        return Err(invalid_argument_error(builtin, "invalid number of inputs").into());
179    }
180    args.first()
181        .ok_or_else(|| invalid_argument_error(builtin, "missing value").into())
182}
183
184fn require_arg_count(builtin: &str, args: &[Value], min: usize, max: usize) -> BuiltinResult<()> {
185    if args.len() < min || args.len() > max {
186        return Err(invalid_argument_error(builtin, "invalid number of inputs").into());
187    }
188    Ok(())
189}
190
191fn require_exact_arg_count(builtin: &str, args: &[Value], expected: usize) -> BuiltinResult<()> {
192    require_arg_count(builtin, args, expected, expected)
193}
194
195fn check_validator(builtin: &str, ok: bool) -> BuiltinResult<Value> {
196    if ok {
197        pass()
198    } else {
199        Err(validation_error(builtin, "value does not satisfy validator").into())
200    }
201}
202
203pub fn dispatch_validator(builtin: &str, args: Vec<Value>) -> BuiltinResult<Value> {
204    let value = require_args(builtin, &args, 1, usize::MAX)?;
205    match builtin {
206        "mustBeA" => {
207            require_exact_arg_count(builtin, &args, 2)?;
208            check_validator(builtin, must_be_a(value, type_names_arg(&args, 1)?)?)
209        }
210        "mustBeColumn" => {
211            require_exact_arg_count(builtin, &args, 1)?;
212            check_validator(builtin, value_is_column(value))
213        }
214        "mustBeFile" => check_validator(builtin, {
215            require_exact_arg_count(builtin, &args, 1)?;
216            value_texts(value)?.iter().all(|p| Path::new(p).is_file())
217        }),
218        "mustBeFinite" => {
219            require_exact_arg_count(builtin, &args, 1)?;
220            check_validator(builtin, value_is_finite(value))
221        }
222        "mustBeFloat" => {
223            require_exact_arg_count(builtin, &args, 1)?;
224            check_validator(builtin, value_is_float(value))
225        }
226        "mustBeFolder" => check_validator(builtin, {
227            require_exact_arg_count(builtin, &args, 1)?;
228            value_texts(value)?.iter().all(|p| Path::new(p).is_dir())
229        }),
230        "mustBeGreaterThan" => {
231            require_exact_arg_count(builtin, &args, 2)?;
232            check_validator(
233                builtin,
234                value_is_greater_than(value, numeric_arg(&args, 1)?),
235            )
236        }
237        "mustBeGreaterThanOrEqual" => {
238            require_exact_arg_count(builtin, &args, 2)?;
239            check_validator(
240                builtin,
241                value_is_greater_than_or_equal(value, numeric_arg(&args, 1)?),
242            )
243        }
244        "mustBeInRange" => {
245            require_arg_count(builtin, &args, 3, 5)?;
246            let lower = numeric_arg(&args, 1)?;
247            let upper = numeric_arg(&args, 2)?;
248            let inclusivity = range_inclusivity_arg(builtin, &args[3..])?;
249            check_validator(builtin, value_is_in_range(value, lower, upper, inclusivity))
250        }
251        "mustBeInteger" => {
252            require_exact_arg_count(builtin, &args, 1)?;
253            check_validator(builtin, value_is_integer(value))
254        }
255        "mustBeLessThan" => {
256            require_exact_arg_count(builtin, &args, 2)?;
257            check_validator(builtin, value_is_less_than(value, numeric_arg(&args, 1)?))
258        }
259        "mustBeLessThanOrEqual" => {
260            require_exact_arg_count(builtin, &args, 2)?;
261            check_validator(
262                builtin,
263                value_is_less_than_or_equal(value, numeric_arg(&args, 1)?),
264            )
265        }
266        "mustBeMember" => {
267            require_exact_arg_count(builtin, &args, 2)?;
268            check_validator(builtin, value_is_member(value, &args[1])?)
269        }
270        "mustBeNegative" => {
271            require_exact_arg_count(builtin, &args, 1)?;
272            check_validator(builtin, value_is_negative(value))
273        }
274        "mustBeNonempty" => {
275            require_exact_arg_count(builtin, &args, 1)?;
276            check_validator(builtin, !value_is_empty(value))
277        }
278        "mustBeNonmissing" => {
279            require_exact_arg_count(builtin, &args, 1)?;
280            check_validator(builtin, value_is_nonmissing(value))
281        }
282        "mustBeNonNan" => {
283            require_exact_arg_count(builtin, &args, 1)?;
284            check_validator(builtin, value_is_non_nan(value))
285        }
286        "mustBeNonnegative" => {
287            require_exact_arg_count(builtin, &args, 1)?;
288            check_validator(builtin, value_is_nonnegative(value))
289        }
290        "mustBeNonpositive" => {
291            require_exact_arg_count(builtin, &args, 1)?;
292            check_validator(builtin, value_is_nonpositive(value))
293        }
294        "mustBeNonsparse" => {
295            require_exact_arg_count(builtin, &args, 1)?;
296            check_validator(builtin, !matches!(value, Value::SparseTensor(_)))
297        }
298        "mustBeNonzero" => {
299            require_exact_arg_count(builtin, &args, 1)?;
300            check_validator(builtin, value_is_nonzero(value))
301        }
302        "mustBeNonzeroLengthText" => {
303            require_exact_arg_count(builtin, &args, 1)?;
304            check_validator(builtin, value_is_nonzero_length_text(value))
305        }
306        "mustBeNumeric" => {
307            require_exact_arg_count(builtin, &args, 1)?;
308            check_validator(builtin, value_is_numeric(value))
309        }
310        "mustBeNumericOrLogical" => {
311            require_exact_arg_count(builtin, &args, 1)?;
312            check_validator(builtin, value_is_numeric_or_logical(value))
313        }
314        "mustBePositive" => {
315            require_exact_arg_count(builtin, &args, 1)?;
316            check_validator(builtin, value_is_positive(value))
317        }
318        "mustBeReal" => {
319            require_exact_arg_count(builtin, &args, 1)?;
320            check_validator(builtin, value_is_real(value))
321        }
322        "mustBeScalarOrEmpty" => {
323            require_exact_arg_count(builtin, &args, 1)?;
324            check_validator(builtin, value_is_scalar_or_empty(value))
325        }
326        "mustBeSparse" => {
327            require_exact_arg_count(builtin, &args, 1)?;
328            check_validator(builtin, matches!(value, Value::SparseTensor(_)))
329        }
330        "mustBeText" => {
331            require_exact_arg_count(builtin, &args, 1)?;
332            check_validator(builtin, value_is_text(value))
333        }
334        "mustBeTextScalar" => {
335            require_exact_arg_count(builtin, &args, 1)?;
336            check_validator(builtin, value_is_text_scalar(value))
337        }
338        "mustBeUnderlyingType" => {
339            require_exact_arg_count(builtin, &args, 2)?;
340            check_validator(
341                builtin,
342                value_underlying_type_matches(value, type_names_arg(&args, 1)?)?,
343            )
344        }
345        "mustBeValidVariableName" => {
346            require_exact_arg_count(builtin, &args, 1)?;
347            check_validator(
348                builtin,
349                value_texts(value)?
350                    .iter()
351                    .all(|name| is_valid_varname(name)),
352            )
353        }
354        "mustBeVector" => {
355            require_exact_arg_count(builtin, &args, 1)?;
356            check_validator(builtin, value_is_vector(value)?)
357        }
358        "validateFunctionSignaturesJSON" => {
359            require_exact_arg_count(builtin, &args, 1)?;
360            validate_function_signatures_json(value)?;
361            pass()
362        }
363        _ => Err(invalid_argument_error(builtin, "unknown validator").into()),
364    }
365}
366
367pub fn value_shape_2d(value: &Value) -> (usize, usize) {
368    match value {
369        Value::Tensor(t) => (t.rows, t.cols),
370        Value::SparseTensor(t) => (t.rows, t.cols),
371        Value::ComplexTensor(t) => (t.rows, t.cols),
372        Value::LogicalArray(a) => {
373            let rows = a.shape.first().copied().unwrap_or(0);
374            let cols = a.shape.get(1).copied().unwrap_or(1);
375            (rows, cols)
376        }
377        Value::Cell(c) => (c.rows, c.cols),
378        Value::CharArray(c) => (c.rows, c.cols),
379        Value::StringArray(s) => (s.rows, s.cols),
380        Value::GpuTensor(handle) => {
381            let rows = handle.shape.first().copied().unwrap_or(1);
382            let cols = handle.shape.get(1).copied().unwrap_or(1);
383            (rows, cols)
384        }
385        _ => (1, 1),
386    }
387}
388
389pub fn value_is_empty(value: &Value) -> bool {
390    match value {
391        Value::Tensor(t) => t.data.is_empty(),
392        Value::SparseTensor(t) => t.rows == 0 || t.cols == 0,
393        Value::ComplexTensor(t) => t.data.is_empty(),
394        Value::LogicalArray(a) => a.data.is_empty(),
395        Value::StringArray(s) => s.data.is_empty(),
396        Value::CharArray(c) => c.rows == 0 || c.cols == 0,
397        Value::Cell(c) => c.data.is_empty(),
398        Value::GpuTensor(handle) => handle.shape.contains(&0),
399        _ => false,
400    }
401}
402
403pub fn value_is_finite(value: &Value) -> bool {
404    match value {
405        Value::Num(v) => v.is_finite(),
406        Value::Int(_) | Value::Bool(_) => true,
407        Value::Complex(re, im) => re.is_finite() && im.is_finite(),
408        Value::Tensor(t) => t.data.iter().all(|v| v.is_finite()),
409        Value::SparseTensor(t) => t.values.iter().all(|v| v.is_finite()),
410        Value::ComplexTensor(t) => t
411            .data
412            .iter()
413            .all(|(re, im)| re.is_finite() && im.is_finite()),
414        Value::LogicalArray(_) | Value::CharArray(_) => true,
415        Value::GpuTensor(_) => true,
416        _ => false,
417    }
418}
419
420pub fn value_is_numeric(value: &Value) -> bool {
421    match value {
422        Value::Num(_)
423        | Value::Int(_)
424        | Value::Complex(_, _)
425        | Value::Tensor(_)
426        | Value::SparseTensor(_)
427        | Value::ComplexTensor(_) => true,
428        Value::GpuTensor(handle) => !handle_is_logical(handle),
429        _ => false,
430    }
431}
432
433pub fn value_is_float(value: &Value) -> bool {
434    match value {
435        Value::Num(_) | Value::Complex(_, _) | Value::ComplexTensor(_) => true,
436        Value::Tensor(t) => matches!(t.dtype, NumericDType::F64 | NumericDType::F32),
437        Value::SparseTensor(_) => true,
438        Value::GpuTensor(handle) => !handle_is_logical(handle),
439        _ => false,
440    }
441}
442
443pub fn value_is_numeric_or_logical(value: &Value) -> bool {
444    value_is_numeric(value) || matches!(value, Value::Bool(_) | Value::LogicalArray(_))
445}
446
447pub fn value_is_text(value: &Value) -> bool {
448    match value {
449        Value::String(_) | Value::StringArray(_) => true,
450        Value::CharArray(chars) => chars.rows == 1,
451        Value::Cell(cell) => cell.data.iter().all(value_is_text),
452        _ => false,
453    }
454}
455
456pub fn value_is_text_scalar(value: &Value) -> bool {
457    match value {
458        Value::String(_) => true,
459        Value::StringArray(strings) => {
460            strings.data.len() == 1 && strings.rows == 1 && strings.cols == 1
461        }
462        Value::CharArray(chars) => chars.rows == 1,
463        _ => false,
464    }
465}
466
467pub fn value_is_nonzero_length_text(value: &Value) -> bool {
468    if !value_is_text(value) {
469        return false;
470    }
471    match value {
472        Value::String(s) => !s.is_empty(),
473        Value::StringArray(s) => s.data.iter().all(|value| !value.is_empty()),
474        Value::CharArray(c) => c.rows == 1 && c.cols > 0,
475        Value::Cell(c) => c.data.iter().all(value_is_nonzero_length_text),
476        _ => false,
477    }
478}
479
480pub fn value_is_scalar_or_empty(value: &Value) -> bool {
481    let (rows, cols) = value_shape_2d(value);
482    (rows == 1 && cols == 1) || rows == 0 || cols == 0
483}
484
485pub fn value_is_real(value: &Value) -> bool {
486    match value {
487        Value::Complex(_, im) => *im == 0.0,
488        Value::ComplexTensor(t) => t.data.iter().all(|(_, im)| *im == 0.0),
489        _ => true,
490    }
491}
492
493pub fn value_is_integer(value: &Value) -> bool {
494    match value {
495        Value::Int(_) | Value::Bool(_) | Value::LogicalArray(_) => true,
496        Value::Num(v) => v.is_finite() && v.fract() == 0.0,
497        Value::Tensor(t) => t.data.iter().all(|v| v.is_finite() && v.fract() == 0.0),
498        Value::SparseTensor(t) => t.values.iter().all(|v| v.is_finite() && v.fract() == 0.0),
499        Value::Complex(re, im) => *im == 0.0 && re.is_finite() && re.fract() == 0.0,
500        Value::ComplexTensor(t) => t
501            .data
502            .iter()
503            .all(|(re, im)| *im == 0.0 && re.is_finite() && re.fract() == 0.0),
504        _ => false,
505    }
506}
507
508pub fn value_is_non_nan(value: &Value) -> bool {
509    match value {
510        Value::Num(v) => !v.is_nan(),
511        Value::Complex(re, im) => !re.is_nan() && !im.is_nan(),
512        Value::Tensor(t) => t.data.iter().all(|v| !v.is_nan()),
513        Value::SparseTensor(t) => t.values.iter().all(|v| !v.is_nan()),
514        Value::ComplexTensor(t) => t.data.iter().all(|(re, im)| !re.is_nan() && !im.is_nan()),
515        Value::Cell(c) => c.data.iter().all(value_is_non_nan),
516        _ => true,
517    }
518}
519
520pub fn value_is_nonmissing(value: &Value) -> bool {
521    value_is_non_nan(value)
522}
523
524pub fn value_is_positive(value: &Value) -> bool {
525    numeric_values_all(value, |v| v.is_finite() && v > 0.0)
526}
527
528pub fn value_is_negative(value: &Value) -> bool {
529    numeric_values_all(value, |v| v.is_finite() && v < 0.0)
530}
531
532pub fn value_is_nonnegative(value: &Value) -> bool {
533    numeric_values_all(value, |v| v.is_finite() && v >= 0.0)
534}
535
536pub fn value_is_nonpositive(value: &Value) -> bool {
537    numeric_values_all(value, |v| v.is_finite() && v <= 0.0)
538}
539
540pub fn value_is_nonzero(value: &Value) -> bool {
541    match value {
542        Value::Complex(re, im) => re.is_finite() && im.is_finite() && (*re != 0.0 || *im != 0.0),
543        Value::ComplexTensor(t) => t
544            .data
545            .iter()
546            .all(|(re, im)| re.is_finite() && im.is_finite() && (*re != 0.0 || *im != 0.0)),
547        _ => numeric_values_all(value, |v| v.is_finite() && v != 0.0),
548    }
549}
550
551pub fn value_is_greater_than_or_equal(value: &Value, threshold: f64) -> bool {
552    numeric_values_all(value, |v| v.is_finite() && v >= threshold)
553}
554
555pub fn value_is_less_than_or_equal(value: &Value, threshold: f64) -> bool {
556    numeric_values_all(value, |v| v.is_finite() && v <= threshold)
557}
558
559pub fn value_is_greater_than(value: &Value, threshold: f64) -> bool {
560    numeric_values_all(value, |v| v.is_finite() && v > threshold)
561}
562
563pub fn value_is_less_than(value: &Value, threshold: f64) -> bool {
564    numeric_values_all(value, |v| v.is_finite() && v < threshold)
565}
566
567pub fn value_is_in_range(
568    value: &Value,
569    lower: f64,
570    upper: f64,
571    inclusivity: RangeInclusivity,
572) -> bool {
573    numeric_values_all(value, |v| {
574        v.is_finite()
575            && if inclusivity.lower {
576                v >= lower
577            } else {
578                v > lower
579            }
580            && if inclusivity.upper {
581                v <= upper
582            } else {
583                v < upper
584            }
585    })
586}
587
588pub fn value_is_column(value: &Value) -> bool {
589    let (_, cols) = value_shape_2d(value);
590    cols == 1
591}
592
593pub fn value_is_vector(value: &Value) -> Result<bool, RuntimeError> {
594    let (rows, cols) = value_shape_2d(value);
595    Ok((rows == 1 || cols == 1) && !(rows == 0 && cols > 1) && !(cols == 0 && rows > 1))
596}
597
598pub fn value_matches_class(value: &Value, class_name: &str) -> bool {
599    let requested = class_name.trim();
600    if requested.is_empty() {
601        return false;
602    }
603    match requested.to_ascii_lowercase().as_str() {
604        "numeric" => value_is_numeric(value),
605        "float" => value_is_float(value),
606        "integer" => matches!(value, Value::Int(_)),
607        "logical" => matches!(value, Value::Bool(_) | Value::LogicalArray(_)),
608        "char" => matches!(value, Value::CharArray(_)),
609        "string" => matches!(value, Value::String(_) | Value::StringArray(_)),
610        "cell" => matches!(value, Value::Cell(_)),
611        "struct" => matches!(value, Value::Struct(_)),
612        "sparse" => matches!(value, Value::SparseTensor(_)),
613        "double" => {
614            matches!(
615                value,
616                Value::Num(_) | Value::Complex(_, _) | Value::SparseTensor(_)
617            ) || matches!(value, Value::Tensor(t) if t.dtype == NumericDType::F64)
618                || matches!(value, Value::ComplexTensor(_))
619        }
620        "single" => matches!(value, Value::Tensor(t) if t.dtype == NumericDType::F32),
621        "gpuarray" => matches!(value, Value::GpuTensor(_)),
622        _ => class_name_for_value(value).eq_ignore_ascii_case(requested),
623    }
624}
625
626pub fn must_be_a(value: &Value, class_names: Vec<String>) -> Result<bool, RuntimeError> {
627    Ok(class_names
628        .iter()
629        .any(|class_name| value_matches_class(value, class_name)))
630}
631
632pub fn value_underlying_type_matches(
633    value: &Value,
634    class_names: Vec<String>,
635) -> Result<bool, RuntimeError> {
636    Ok(class_names
637        .iter()
638        .any(|class_name| underlying_type_matches(value, class_name)))
639}
640
641pub fn value_is_member(value: &Value, set: &Value) -> Result<bool, RuntimeError> {
642    let values = atoms(value)?;
643    let allowed = atoms(set)?;
644    value_is_member_atoms_inner(&values, &allowed)
645}
646
647pub fn value_is_member_atoms(
648    value: &Value,
649    allowed: &[ValidationAtom],
650) -> Result<bool, RuntimeError> {
651    let values = atoms(value)?;
652    value_is_member_atoms_inner(&values, allowed)
653}
654
655fn value_is_member_atoms_inner(
656    values: &[ValidationAtom],
657    allowed: &[ValidationAtom],
658) -> Result<bool, RuntimeError> {
659    Ok(values
660        .iter()
661        .all(|value| allowed.iter().any(|allowed| atom_eq(value, allowed))))
662}
663
664pub fn atoms(value: &Value) -> Result<Vec<ValidationAtom>, RuntimeError> {
665    match value {
666        Value::Num(v) => Ok(vec![ValidationAtom::Number(*v)]),
667        Value::Int(v) => Ok(vec![ValidationAtom::Number(v.to_f64())]),
668        Value::Bool(v) => Ok(vec![ValidationAtom::Bool(*v)]),
669        Value::String(s) => Ok(vec![ValidationAtom::Text(s.clone())]),
670        Value::CharArray(c) if c.rows == 1 => Ok(vec![ValidationAtom::Text(chars_to_string(c))]),
671        Value::Tensor(t) => Ok(t.data.iter().copied().map(ValidationAtom::Number).collect()),
672        Value::SparseTensor(t) => sparse_atoms(t),
673        Value::LogicalArray(a) => Ok(a
674            .data
675            .iter()
676            .map(|v| ValidationAtom::Bool(*v != 0))
677            .collect()),
678        Value::StringArray(s) => Ok(s.data.iter().cloned().map(ValidationAtom::Text).collect()),
679        Value::Cell(c) => {
680            let mut out = Vec::new();
681            for entry in &c.data {
682                out.extend(atoms(entry)?);
683            }
684            Ok(out)
685        }
686        _ => Err(invalid_argument_error(
687            "mustBeMember",
688            "unsupported member value type",
689        )),
690    }
691}
692
693fn sparse_atoms(t: &SparseTensor) -> Result<Vec<ValidationAtom>, RuntimeError> {
694    let numel = t.rows.saturating_mul(t.cols);
695    let mut out = Vec::with_capacity(numel.min(t.values.len().saturating_add(1)));
696    out.extend(t.values.iter().copied().map(ValidationAtom::Number));
697    if t.values.len() < numel {
698        out.push(ValidationAtom::Number(0.0));
699    }
700    Ok(out)
701}
702
703fn atom_eq(left: &ValidationAtom, right: &ValidationAtom) -> bool {
704    match (left, right) {
705        (ValidationAtom::Number(a), ValidationAtom::Number(b)) => a == b,
706        (ValidationAtom::Text(a), ValidationAtom::Text(b)) => a == b,
707        (ValidationAtom::Bool(a), ValidationAtom::Bool(b)) => a == b,
708        _ => false,
709    }
710}
711
712fn numeric_values_all(value: &Value, pred: impl Fn(f64) -> bool) -> bool {
713    match value {
714        Value::Num(v) => pred(*v),
715        Value::Int(v) => pred(v.to_f64()),
716        Value::Bool(v) => pred(if *v { 1.0 } else { 0.0 }),
717        Value::LogicalArray(a) => a.data.iter().map(|v| f64::from(*v != 0)).all(pred),
718        Value::Tensor(t) => t.data.iter().copied().all(pred),
719        Value::SparseTensor(t) => {
720            let numel = t.rows.saturating_mul(t.cols);
721            t.values.iter().copied().all(&pred) && (t.values.len() >= numel || pred(0.0))
722        }
723        Value::Complex(re, im) => *im == 0.0 && pred(*re),
724        Value::ComplexTensor(t) => t.data.iter().all(|(re, im)| *im == 0.0 && pred(*re)),
725        _ => false,
726    }
727}
728
729fn numeric_arg(args: &[Value], index: usize) -> Result<f64, RuntimeError> {
730    match args.get(index) {
731        Some(Value::Num(v)) => Ok(*v),
732        Some(Value::Int(v)) => Ok(v.to_f64()),
733        Some(Value::Tensor(t)) if t.data.len() == 1 => Ok(t.data[0]),
734        Some(other) => Err(invalid_argument_error(
735            "argumentValidation",
736            format!(
737                "expected numeric scalar argument, got {}",
738                class_name_for_value(other)
739            ),
740        )),
741        None => Err(invalid_argument_error(
742            "argumentValidation",
743            "missing numeric scalar argument",
744        )),
745    }
746}
747
748fn type_names_arg(args: &[Value], index: usize) -> Result<Vec<String>, RuntimeError> {
749    match args.get(index) {
750        Some(value) => value_texts(value),
751        None => Err(invalid_argument_error(
752            "argumentValidation",
753            "missing type name argument",
754        )),
755    }
756}
757
758fn range_inclusivity_arg(builtin: &str, args: &[Value]) -> Result<RangeInclusivity, RuntimeError> {
759    match args {
760        [] => Ok(RangeInclusivity::CLOSED),
761        [flag] => range_inclusivity_single_flag(builtin, text_scalar_arg(builtin, flag)?.as_str()),
762        [lower, upper] => {
763            let lower =
764                range_bound_inclusive_flag(builtin, text_scalar_arg(builtin, lower)?.as_str())?;
765            let upper =
766                range_bound_inclusive_flag(builtin, text_scalar_arg(builtin, upper)?.as_str())?;
767            Ok(RangeInclusivity { lower, upper })
768        }
769        _ => Err(invalid_argument_error(
770            builtin,
771            "invalid range inclusivity flags",
772        )),
773    }
774}
775
776fn range_inclusivity_single_flag(
777    builtin: &str,
778    flag: &str,
779) -> Result<RangeInclusivity, RuntimeError> {
780    match flag.trim().to_ascii_lowercase().as_str() {
781        "inclusive" => Ok(RangeInclusivity::CLOSED),
782        "exclusive" => Ok(RangeInclusivity::OPEN),
783        "exclude-lower" | "openleft" | "open-left" => Ok(RangeInclusivity::OPEN_LEFT),
784        "exclude-upper" | "openright" | "open-right" => Ok(RangeInclusivity::OPEN_RIGHT),
785        _ => Err(invalid_argument_error(
786            builtin,
787            "range flag must be 'inclusive', 'exclusive', 'exclude-lower', or 'exclude-upper'",
788        )),
789    }
790}
791
792fn range_bound_inclusive_flag(builtin: &str, flag: &str) -> Result<bool, RuntimeError> {
793    match flag.trim().to_ascii_lowercase().as_str() {
794        "inclusive" => Ok(true),
795        "exclusive" => Ok(false),
796        _ => Err(invalid_argument_error(
797            builtin,
798            "range bound flag must be 'inclusive' or 'exclusive'",
799        )),
800    }
801}
802
803fn text_scalar_arg(builtin: &str, value: &Value) -> Result<String, RuntimeError> {
804    let texts = value_texts(value)?;
805    match texts.as_slice() {
806        [text] => Ok(text.clone()),
807        _ => Err(invalid_argument_error(builtin, "expected text scalar")),
808    }
809}
810
811fn value_texts(value: &Value) -> Result<Vec<String>, RuntimeError> {
812    match value {
813        Value::String(s) => Ok(vec![s.clone()]),
814        Value::StringArray(s) => Ok(s.data.clone()),
815        Value::CharArray(c) if c.rows == 1 => Ok(vec![chars_to_string(c)]),
816        Value::Cell(c) => {
817            let mut out = Vec::with_capacity(c.data.len());
818            for entry in &c.data {
819                out.extend(value_texts(entry)?);
820            }
821            Ok(out)
822        }
823        other => Err(invalid_argument_error(
824            "argumentValidation",
825            format!("expected text, got {}", class_name_for_value(other)),
826        )),
827    }
828}
829
830fn chars_to_string(chars: &CharArray) -> String {
831    chars.data.iter().collect()
832}
833
834pub fn isvarname_value(value: &Value) -> bool {
835    value_texts(value)
836        .map(|names| names.iter().all(|name| is_valid_varname(name)))
837        .unwrap_or(false)
838}
839
840pub fn namedargs2cell_value(value: Value) -> BuiltinResult<Value> {
841    let Value::Struct(struct_value) = value else {
842        return Err(
843            invalid_argument_error("namedargs2cell", "input must be a scalar struct").into(),
844        );
845    };
846    let mut data = Vec::with_capacity(struct_value.fields.len().saturating_mul(2));
847    for (field, value) in struct_value.fields {
848        data.push(Value::String(field));
849        data.push(value);
850    }
851    let cols = data.len();
852    let cell = CellArray::new(data, 1, cols)
853        .map_err(|err| invalid_argument_error("namedargs2cell", err))?;
854    Ok(Value::Cell(cell))
855}
856
857pub fn validate_function_signatures_json(value: &Value) -> BuiltinResult<()> {
858    for text in value_texts(value)? {
859        serde_json::from_str::<serde_json::Value>(&text).map_err(|err| {
860            invalid_argument_error(
861                "validateFunctionSignaturesJSON",
862                format!("invalid JSON signature payload: {err}"),
863            )
864        })?;
865    }
866    Ok(())
867}
868
869fn bool_type(
870    _: &[runmat_builtins::Type],
871    _: &runmat_builtins::ResolveContext,
872) -> runmat_builtins::Type {
873    runmat_builtins::Type::Bool
874}
875
876fn any_type(
877    _: &[runmat_builtins::Type],
878    _: &runmat_builtins::ResolveContext,
879) -> runmat_builtins::Type {
880    runmat_builtins::Type::Unknown
881}
882
883#[runtime_builtin(
884    name = "isvarname",
885    category = "argument-validation",
886    summary = "Return true when text is a valid MATLAB variable name.",
887    type_resolver(bool_type),
888    descriptor(self::ISVARNAME_DESCRIPTOR),
889    builtin_path = "crate::builtins::common::validation"
890)]
891fn isvarname_builtin(value: Value) -> BuiltinResult<Value> {
892    Ok(Value::Bool(isvarname_value(&value)))
893}
894
895#[runtime_builtin(
896    name = "namedargs2cell",
897    category = "argument-validation",
898    summary = "Convert a scalar name-value struct to an alternating name/value cell row.",
899    type_resolver(any_type),
900    descriptor(self::NAMEDARGS2CELL_DESCRIPTOR),
901    builtin_path = "crate::builtins::common::validation"
902)]
903fn namedargs2cell_builtin(value: Value) -> BuiltinResult<Value> {
904    namedargs2cell_value(value)
905}
906
907macro_rules! validator_builtin {
908    ($func:ident, $name:literal) => {
909        #[runtime_builtin(
910            name = $name,
911            category = "argument-validation",
912            summary = "Validate an input argument and throw if the constraint is not satisfied.",
913            sink = true,
914            suppress_auto_output = true,
915            descriptor(self::VALIDATOR_DESCRIPTOR),
916            builtin_path = "crate::builtins::common::validation"
917        )]
918        fn $func(args: Vec<Value>) -> BuiltinResult<Value> {
919            dispatch_validator($name, args)
920        }
921    };
922}
923
924validator_builtin!(must_be_a_builtin, "mustBeA");
925validator_builtin!(must_be_column_builtin, "mustBeColumn");
926validator_builtin!(must_be_file_builtin, "mustBeFile");
927validator_builtin!(must_be_finite_builtin, "mustBeFinite");
928validator_builtin!(must_be_float_builtin, "mustBeFloat");
929validator_builtin!(must_be_folder_builtin, "mustBeFolder");
930validator_builtin!(must_be_greater_than_builtin, "mustBeGreaterThan");
931validator_builtin!(
932    must_be_greater_than_or_equal_builtin,
933    "mustBeGreaterThanOrEqual"
934);
935validator_builtin!(must_be_in_range_builtin, "mustBeInRange");
936validator_builtin!(must_be_integer_builtin, "mustBeInteger");
937validator_builtin!(must_be_less_than_builtin, "mustBeLessThan");
938validator_builtin!(must_be_less_than_or_equal_builtin, "mustBeLessThanOrEqual");
939validator_builtin!(must_be_member_builtin, "mustBeMember");
940validator_builtin!(must_be_negative_builtin, "mustBeNegative");
941validator_builtin!(must_be_nonempty_builtin, "mustBeNonempty");
942validator_builtin!(must_be_nonmissing_builtin, "mustBeNonmissing");
943validator_builtin!(must_be_non_nan_builtin, "mustBeNonNan");
944validator_builtin!(must_be_nonnegative_builtin, "mustBeNonnegative");
945validator_builtin!(must_be_nonpositive_builtin, "mustBeNonpositive");
946validator_builtin!(must_be_nonsparse_builtin, "mustBeNonsparse");
947validator_builtin!(must_be_nonzero_builtin, "mustBeNonzero");
948validator_builtin!(
949    must_be_nonzero_length_text_builtin,
950    "mustBeNonzeroLengthText"
951);
952validator_builtin!(must_be_numeric_builtin, "mustBeNumeric");
953validator_builtin!(must_be_numeric_or_logical_builtin, "mustBeNumericOrLogical");
954validator_builtin!(must_be_positive_builtin, "mustBePositive");
955validator_builtin!(must_be_real_builtin, "mustBeReal");
956validator_builtin!(must_be_scalar_or_empty_builtin, "mustBeScalarOrEmpty");
957validator_builtin!(must_be_sparse_builtin, "mustBeSparse");
958validator_builtin!(must_be_text_builtin, "mustBeText");
959validator_builtin!(must_be_text_scalar_builtin, "mustBeTextScalar");
960validator_builtin!(must_be_underlying_type_builtin, "mustBeUnderlyingType");
961validator_builtin!(
962    must_be_valid_variable_name_builtin,
963    "mustBeValidVariableName"
964);
965validator_builtin!(must_be_vector_builtin, "mustBeVector");
966validator_builtin!(
967    validate_function_signatures_json_builtin,
968    "validateFunctionSignaturesJSON"
969);
970
971#[cfg(test)]
972mod tests {
973    use super::*;
974    use crate::builtins::common::identifiers::MATLAB_NAME_LENGTH_MAX;
975    use runmat_builtins::{IntValue, LogicalArray, StringArray, StructValue, Tensor};
976
977    fn ok(builtin: &str, args: Vec<Value>) {
978        dispatch_validator(builtin, args).unwrap_or_else(|err| {
979            panic!("{builtin} unexpectedly failed: {err}");
980        });
981    }
982
983    fn err(builtin: &str, args: Vec<Value>) {
984        assert!(
985            dispatch_validator(builtin, args).is_err(),
986            "{builtin} unexpectedly passed"
987        );
988    }
989
990    fn tensor(data: Vec<f64>, rows: usize, cols: usize) -> Value {
991        Value::Tensor(Tensor::new_2d(data, rows, cols).unwrap())
992    }
993
994    fn sparse(values: Vec<f64>) -> Value {
995        Value::SparseTensor(SparseTensor::new(2, 2, vec![0, 1, 1], vec![0], values).unwrap())
996    }
997
998    #[test]
999    fn numeric_validators_check_all_elements() {
1000        let ok = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
1001        assert!(dispatch_validator("mustBePositive", vec![Value::Tensor(ok)]).is_ok());
1002
1003        let bad = Tensor::new(vec![1.0, 0.0], vec![1, 2]).unwrap();
1004        assert!(dispatch_validator("mustBePositive", vec![Value::Tensor(bad)]).is_err());
1005    }
1006
1007    #[test]
1008    fn member_validator_accepts_numeric_and_text_sets() {
1009        let allowed = Tensor::new(vec![1.0, 3.0, 5.0], vec![1, 3]).unwrap();
1010        assert!(dispatch_validator(
1011            "mustBeMember",
1012            vec![Value::Num(3.0), Value::Tensor(allowed)]
1013        )
1014        .is_ok());
1015
1016        let allowed = StringArray::new(vec!["on".into(), "off".into()], vec![1, 2]).unwrap();
1017        assert!(dispatch_validator(
1018            "mustBeMember",
1019            vec![Value::String("on".into()), Value::StringArray(allowed)]
1020        )
1021        .is_ok());
1022    }
1023
1024    #[test]
1025    fn text_and_varname_validators_follow_core_shapes() {
1026        assert!(dispatch_validator(
1027            "mustBeNonzeroLengthText",
1028            vec![Value::CharArray(CharArray::new_row("alpha"))]
1029        )
1030        .is_ok());
1031        assert!(isvarname_value(&Value::String("alpha_1".into())));
1032        assert!(!isvarname_value(&Value::String("1alpha".into())));
1033    }
1034
1035    #[test]
1036    fn namedargs2cell_preserves_field_order() {
1037        let mut st = StructValue::new();
1038        st.insert("Name", Value::String("Ada".into()));
1039        st.insert("Value", Value::Num(7.0));
1040        let out = namedargs2cell_value(Value::Struct(st)).expect("namedargs2cell");
1041        let Value::Cell(cell) = out else {
1042            panic!("expected cell");
1043        };
1044        assert_eq!(cell.rows, 1);
1045        assert_eq!(cell.cols, 4);
1046        assert_eq!(cell.data[0], Value::String("Name".into()));
1047        assert_eq!(cell.data[2], Value::String("Value".into()));
1048    }
1049
1050    #[test]
1051    fn validator_surface_accepts_and_rejects_representative_values() {
1052        let temp_dir = tempfile::tempdir().unwrap();
1053        let file_path = temp_dir.path().join("data.txt");
1054        std::fs::write(&file_path, "ok").unwrap();
1055        let dir_text = Value::String(temp_dir.path().to_string_lossy().into_owned());
1056        let file_text = Value::String(file_path.to_string_lossy().into_owned());
1057
1058        ok(
1059            "mustBeA",
1060            vec![Value::Num(1.0), Value::String("double".into())],
1061        );
1062        err(
1063            "mustBeA",
1064            vec![Value::String("x".into()), Value::String("double".into())],
1065        );
1066        ok("mustBeColumn", vec![tensor(vec![1.0, 2.0], 2, 1)]);
1067        err("mustBeColumn", vec![tensor(vec![1.0, 2.0], 1, 2)]);
1068        ok("mustBeFile", vec![file_text.clone()]);
1069        err("mustBeFile", vec![dir_text.clone()]);
1070        ok("mustBeFolder", vec![dir_text.clone()]);
1071        err("mustBeFolder", vec![file_text.clone()]);
1072        ok("mustBeFinite", vec![tensor(vec![1.0, 2.0], 1, 2)]);
1073        err("mustBeFinite", vec![Value::Num(f64::INFINITY)]);
1074        ok("mustBeFloat", vec![Value::Num(1.0)]);
1075        err("mustBeFloat", vec![Value::Int(IntValue::I32(1))]);
1076        ok("mustBeInteger", vec![tensor(vec![1.0, 2.0], 1, 2)]);
1077        ok("mustBeInteger", vec![Value::Bool(true)]);
1078        ok(
1079            "mustBeInteger",
1080            vec![Value::LogicalArray(
1081                LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap(),
1082            )],
1083        );
1084        err("mustBeInteger", vec![Value::Num(1.5)]);
1085        ok("mustBeNumeric", vec![Value::Complex(1.0, 2.0)]);
1086        err("mustBeNumeric", vec![Value::String("1".into())]);
1087        ok(
1088            "mustBeNumericOrLogical",
1089            vec![Value::LogicalArray(
1090                LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap(),
1091            )],
1092        );
1093        err("mustBeNumericOrLogical", vec![Value::String("true".into())]);
1094        ok("mustBeReal", vec![Value::Complex(1.0, 0.0)]);
1095        err("mustBeReal", vec![Value::Complex(1.0, 1.0)]);
1096        ok("mustBeVector", vec![tensor(vec![1.0, 2.0], 1, 2)]);
1097        err("mustBeVector", vec![tensor(vec![1.0, 2.0, 3.0, 4.0], 2, 2)]);
1098        ok("mustBeScalarOrEmpty", vec![Value::Num(1.0)]);
1099        err("mustBeScalarOrEmpty", vec![tensor(vec![1.0, 2.0], 1, 2)]);
1100        ok("mustBeSparse", vec![sparse(vec![1.0])]);
1101        err("mustBeSparse", vec![Value::Num(1.0)]);
1102        ok("mustBeNonsparse", vec![Value::Num(1.0)]);
1103        err("mustBeNonsparse", vec![sparse(vec![1.0])]);
1104        ok(
1105            "mustBeText",
1106            vec![Value::CharArray(CharArray::new_row("abc"))],
1107        );
1108        err("mustBeText", vec![Value::Num(1.0)]);
1109        ok("mustBeTextScalar", vec![Value::String("abc".into())]);
1110        err(
1111            "mustBeTextScalar",
1112            vec![Value::StringArray(
1113                StringArray::new_2d(vec!["a".into(), "b".into()], 1, 2).unwrap(),
1114            )],
1115        );
1116        ok(
1117            "mustBeNonzeroLengthText",
1118            vec![Value::StringArray(
1119                StringArray::new_2d(vec!["a".into(), "b".into()], 1, 2).unwrap(),
1120            )],
1121        );
1122        err(
1123            "mustBeNonzeroLengthText",
1124            vec![Value::String(String::new())],
1125        );
1126        ok("mustBeNonempty", vec![Value::String("x".into())]);
1127        err(
1128            "mustBeNonempty",
1129            vec![Value::StringArray(
1130                StringArray::new_2d(vec![], 0, 0).unwrap(),
1131            )],
1132        );
1133        ok("mustBeNonmissing", vec![Value::Num(1.0)]);
1134        err("mustBeNonmissing", vec![Value::Num(f64::NAN)]);
1135        ok("mustBeNonNan", vec![Value::Complex(1.0, 0.0)]);
1136        err("mustBeNonNan", vec![Value::Complex(f64::NAN, 0.0)]);
1137        ok(
1138            "mustBeUnderlyingType",
1139            vec![Value::Int(IntValue::I16(1)), Value::String("int16".into())],
1140        );
1141        err(
1142            "mustBeUnderlyingType",
1143            vec![Value::Bool(true), Value::String("double".into())],
1144        );
1145        ok(
1146            "mustBeValidVariableName",
1147            vec![Value::String("alpha_1".into())],
1148        );
1149        err(
1150            "mustBeValidVariableName",
1151            vec![Value::String("_alpha".into())],
1152        );
1153        ok(
1154            "mustBeMember",
1155            vec![
1156                Value::String("on".into()),
1157                Value::Cell(
1158                    CellArray::new(
1159                        vec![Value::String("on".into()), Value::String("off".into())],
1160                        1,
1161                        2,
1162                    )
1163                    .unwrap(),
1164                ),
1165            ],
1166        );
1167        err(
1168            "mustBeMember",
1169            vec![
1170                Value::String("bad".into()),
1171                Value::Cell(
1172                    CellArray::new(
1173                        vec![Value::String("on".into()), Value::String("off".into())],
1174                        1,
1175                        2,
1176                    )
1177                    .unwrap(),
1178                ),
1179            ],
1180        );
1181    }
1182
1183    #[test]
1184    fn numeric_threshold_validators_cover_boundaries() {
1185        ok("mustBePositive", vec![Value::Num(1.0)]);
1186        ok("mustBePositive", vec![Value::Bool(true)]);
1187        err("mustBePositive", vec![Value::Num(0.0)]);
1188        ok("mustBeNegative", vec![Value::Num(-1.0)]);
1189        err("mustBeNegative", vec![Value::Num(0.0)]);
1190        ok("mustBeNonnegative", vec![Value::Num(0.0)]);
1191        ok(
1192            "mustBeNonnegative",
1193            vec![Value::LogicalArray(
1194                LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap(),
1195            )],
1196        );
1197        err("mustBeNonnegative", vec![Value::Num(-1.0)]);
1198        ok("mustBeNonpositive", vec![Value::Num(0.0)]);
1199        err("mustBeNonpositive", vec![Value::Num(1.0)]);
1200        ok("mustBeNonzero", vec![Value::Complex(0.0, 1.0)]);
1201        err("mustBeNonzero", vec![Value::Num(0.0)]);
1202        ok("mustBeGreaterThan", vec![Value::Num(2.0), Value::Num(1.0)]);
1203        err("mustBeGreaterThan", vec![Value::Num(1.0), Value::Num(1.0)]);
1204        ok(
1205            "mustBeGreaterThanOrEqual",
1206            vec![Value::Num(1.0), Value::Num(1.0)],
1207        );
1208        err(
1209            "mustBeGreaterThanOrEqual",
1210            vec![Value::Num(0.0), Value::Num(1.0)],
1211        );
1212        ok("mustBeLessThan", vec![Value::Num(0.0), Value::Num(1.0)]);
1213        err("mustBeLessThan", vec![Value::Num(1.0), Value::Num(1.0)]);
1214        ok(
1215            "mustBeLessThanOrEqual",
1216            vec![Value::Num(1.0), Value::Num(1.0)],
1217        );
1218        err(
1219            "mustBeLessThanOrEqual",
1220            vec![Value::Num(2.0), Value::Num(1.0)],
1221        );
1222    }
1223
1224    #[test]
1225    fn in_range_supports_interval_flags_and_rejects_extra_inputs() {
1226        ok(
1227            "mustBeInRange",
1228            vec![Value::Num(1.0), Value::Num(1.0), Value::Num(2.0)],
1229        );
1230        err(
1231            "mustBeInRange",
1232            vec![
1233                Value::Num(1.0),
1234                Value::Num(1.0),
1235                Value::Num(2.0),
1236                Value::String("exclusive".into()),
1237            ],
1238        );
1239        ok(
1240            "mustBeInRange",
1241            vec![
1242                Value::Num(1.5),
1243                Value::Num(1.0),
1244                Value::Num(2.0),
1245                Value::String("exclusive".into()),
1246            ],
1247        );
1248        err(
1249            "mustBeInRange",
1250            vec![
1251                Value::Num(1.0),
1252                Value::Num(1.0),
1253                Value::Num(2.0),
1254                Value::String("exclusive".into()),
1255                Value::String("inclusive".into()),
1256            ],
1257        );
1258        ok(
1259            "mustBeInRange",
1260            vec![
1261                Value::Num(2.0),
1262                Value::Num(1.0),
1263                Value::Num(2.0),
1264                Value::String("exclude-lower".into()),
1265            ],
1266        );
1267        err(
1268            "mustBeInRange",
1269            vec![
1270                Value::Num(2.0),
1271                Value::Num(1.0),
1272                Value::Num(2.0),
1273                Value::String("inclusive".into()),
1274                Value::String("exclusive".into()),
1275                Value::String("extra".into()),
1276            ],
1277        );
1278    }
1279
1280    #[test]
1281    fn varname_rules_reject_keywords_underscores_and_overlong_names() {
1282        assert!(isvarname_value(&Value::String("alpha_1".into())));
1283        assert!(!isvarname_value(&Value::String("_alpha".into())));
1284        assert!(!isvarname_value(&Value::String("1alpha".into())));
1285        assert!(!isvarname_value(&Value::String("for".into())));
1286        assert!(!isvarname_value(&Value::String("end".into())));
1287        assert!(isvarname_value(&Value::String(
1288            "a".repeat(MATLAB_NAME_LENGTH_MAX)
1289        )));
1290        assert!(!isvarname_value(&Value::String(
1291            "a".repeat(MATLAB_NAME_LENGTH_MAX + 1)
1292        )));
1293    }
1294
1295    #[test]
1296    fn callable_validators_reject_unexpected_extra_arguments() {
1297        err("mustBePositive", vec![Value::Num(1.0), Value::Num(2.0)]);
1298        err("mustBeMember", vec![Value::String("on".into())]);
1299        err(
1300            "mustBeA",
1301            vec![
1302                Value::Num(1.0),
1303                Value::String("double".into()),
1304                Value::String("extra".into()),
1305            ],
1306        );
1307    }
1308
1309    #[test]
1310    fn validate_function_signatures_json_checks_json_syntax() {
1311        ok(
1312            "validateFunctionSignaturesJSON",
1313            vec![Value::String(r#"{"functions":[]}"#.into())],
1314        );
1315        err(
1316            "validateFunctionSignaturesJSON",
1317            vec![Value::String("{not json}".into())],
1318        );
1319    }
1320}