Skip to main content

runmat_runtime/call/
function_abi.rs

1//! Executor-neutral function-entry and function-output semantics.
2
3use crate::builtins::common::validation;
4use crate::object::cell::expand_all_cell_values;
5use crate::runtime_error::semantic_error;
6use crate::RuntimeError;
7use runmat_types::{
8    FunctionArgDefaultValue, FunctionArgDim, FunctionArgSizeSpec, FunctionArgValidationLiteral,
9    FunctionArgValidator,
10};
11use runmat_value::{CellArray, IntValue, Tensor, Value};
12
13/// Borrowed semantic contract for one fixed function input.
14///
15/// Executors project their local/binding identity to a fixed-input index and
16/// borrow the shared constraint vocabulary; Runtime owns the behavior.
17#[derive(Clone, Copy, Debug)]
18pub struct FunctionInputSpec<'a> {
19    pub input_index: usize,
20    pub size: Option<&'a FunctionArgSizeSpec>,
21    pub class_name: Option<&'a str>,
22    pub validators: &'a [FunctionArgValidator],
23    pub default_value: Option<&'a FunctionArgDefaultValue>,
24}
25
26#[derive(Clone, Debug)]
27pub struct PreparedFunctionInputs {
28    /// One value per fixed input. `None` preserves MATLAB's unassigned state
29    /// for an omitted argument that has no default.
30    pub fixed: Vec<Option<Value>>,
31    pub varargin: Option<CellArray>,
32    pub nargin: usize,
33}
34
35pub fn prepare_function_inputs(
36    function_name: &str,
37    supplied: &[Value],
38    fixed_input_count: usize,
39    accepts_varargin: bool,
40    specs: &[FunctionInputSpec<'_>],
41) -> Result<PreparedFunctionInputs, RuntimeError> {
42    let preparation = prepare_function_inputs_async(
43        function_name,
44        supplied,
45        fixed_input_count,
46        accepts_varargin,
47        specs,
48    );
49    #[cfg(not(target_arch = "wasm32"))]
50    {
51        pollster::block_on(preparation)
52    }
53    #[cfg(target_arch = "wasm32")]
54    {
55        futures::executor::block_on(preparation)
56    }
57}
58
59pub async fn prepare_function_inputs_async(
60    function_name: &str,
61    supplied: &[Value],
62    fixed_input_count: usize,
63    accepts_varargin: bool,
64    specs: &[FunctionInputSpec<'_>],
65) -> Result<PreparedFunctionInputs, RuntimeError> {
66    if supplied.len() > fixed_input_count && !accepts_varargin {
67        return Err(semantic_error(
68            "TooManyInputs",
69            format!(
70                "semantic function {function_name} expected {fixed_input_count} inputs, got {}",
71                supplied.len()
72            ),
73        ));
74    }
75    if specs
76        .iter()
77        .any(|spec| spec.input_index >= fixed_input_count)
78    {
79        return Err(semantic_error(
80            "InvalidInputSlot",
81            "function argument slot out of bounds",
82        ));
83    }
84
85    let mut fixed = vec![None; fixed_input_count];
86    for (target, value) in fixed.iter_mut().zip(supplied) {
87        *target = Some(value.clone());
88    }
89    for spec in specs {
90        if fixed[spec.input_index].is_none() {
91            if let Some(default) = spec.default_value {
92                fixed[spec.input_index] = Some(default_value(default));
93            }
94        }
95        if let Some(value) = &fixed[spec.input_index] {
96            validate_input(function_name, spec.input_index, value, spec).await?;
97        }
98    }
99
100    let varargin = if accepts_varargin {
101        let values = supplied
102            .get(fixed_input_count..)
103            .unwrap_or_default()
104            .to_vec();
105        let columns = values.len();
106        Some(
107            CellArray::new(values, 1, columns)
108                .map_err(|error| semantic_error("VararginPack", format!("varargin: {error}")))?,
109        )
110    } else {
111        None
112    };
113    Ok(PreparedFunctionInputs {
114        fixed,
115        varargin,
116        nargin: supplied.len(),
117    })
118}
119
120pub fn collect_function_outputs(
121    function_name: &str,
122    fixed_outputs: &[Value],
123    varargout: Option<&Value>,
124    requested_outputs: usize,
125) -> Result<Vec<Value>, RuntimeError> {
126    if requested_outputs > fixed_outputs.len() && varargout.is_none() {
127        return Err(semantic_error(
128            "TooManyOutputs",
129            format!(
130                "semantic function {function_name} expected {} outputs, got {requested_outputs}",
131                fixed_outputs.len()
132            ),
133        ));
134    }
135    let mut values = fixed_outputs
136        .iter()
137        .take(requested_outputs)
138        .cloned()
139        .collect::<Vec<_>>();
140    if values.len() < requested_outputs {
141        if let Some(varargout) = varargout {
142            let expanded = match varargout {
143                Value::Cell(cell) => expand_all_cell_values(cell)?,
144                _ => Vec::new(),
145            };
146            let available = expanded.len();
147            values.extend(expanded.into_iter().take(requested_outputs - values.len()));
148            if values.len() < requested_outputs {
149                let needed = requested_outputs - fixed_outputs.len();
150                return Err(semantic_error(
151                    "VarargoutMismatch",
152                    format!(
153                        "Function '{function_name}' returned {available} varargout values, {needed} requested"
154                    ),
155                ));
156            }
157        }
158    }
159    values.resize(requested_outputs, Value::Num(0.0));
160    Ok(values)
161}
162
163fn default_value(default: &FunctionArgDefaultValue) -> Value {
164    match default {
165        FunctionArgDefaultValue::Number(value) => Value::Num(*value),
166        FunctionArgDefaultValue::Integer(value) => Value::Int(IntValue::from(value)),
167        FunctionArgDefaultValue::Bool(value) => Value::Bool(*value),
168        FunctionArgDefaultValue::String(value) => Value::String(value.clone()),
169        FunctionArgDefaultValue::EmptyArray => Value::Tensor(
170            Tensor::new(Vec::new(), vec![0, 0]).expect("empty default tensor is always valid"),
171        ),
172    }
173}
174
175async fn validate_input(
176    function_name: &str,
177    input_index: usize,
178    value: &Value,
179    spec: &FunctionInputSpec<'_>,
180) -> Result<(), RuntimeError> {
181    if let Some(size) = spec.size {
182        let (rows, columns) = validation::value_shape_2d(value);
183        if !dim_matches(&size.rows, rows) || !dim_matches(&size.cols, columns) {
184            return Err(semantic_error(
185                "ArgumentValidationSize",
186                format!(
187                    "Function '{function_name}' argument #{} failed size validation",
188                    input_index + 1
189                ),
190            ));
191        }
192    }
193    if let Some(class_name) = spec.class_name {
194        if !validation::value_matches_class(value, class_name) {
195            return Err(semantic_error(
196                "ArgumentValidationClass",
197                format!(
198                    "Function '{function_name}' argument #{} failed class validation (expected {class_name})",
199                    input_index + 1
200                ),
201            ));
202        }
203    }
204    for validator in spec.validators {
205        if !validator_passes(value, validator).await? {
206            return Err(semantic_error(
207                "ArgumentValidationFunction",
208                format!(
209                    "Function '{function_name}' argument #{} failed {} validation",
210                    input_index + 1,
211                    validator_name(validator)
212                ),
213            ));
214        }
215    }
216    Ok(())
217}
218
219fn dim_matches(dim: &FunctionArgDim, actual: usize) -> bool {
220    matches!(dim, FunctionArgDim::Any)
221        || matches!(dim, FunctionArgDim::Exact(expected) if *expected == actual)
222}
223
224async fn validator_passes(
225    value: &Value,
226    validator: &FunctionArgValidator,
227) -> Result<bool, RuntimeError> {
228    use FunctionArgValidator as V;
229    Ok(match validator {
230        V::A(names) => validation::must_be_a(value, names.clone())?,
231        V::Column => validation::value_is_column(value),
232        V::Finite => {
233            validation::ensure_resident_extension(value, "mustBeFinite")?;
234            validation::value_is_finite_async(value).await?
235        }
236        V::Float => validation::value_is_float(value),
237        V::Folder => validation::dispatch_validator_async("mustBeFolder", vec![value.clone()])
238            .await
239            .is_ok(),
240        V::File => validation::dispatch_validator_async("mustBeFile", vec![value.clone()])
241            .await
242            .is_ok(),
243        V::NumericOrLogical => validation::value_is_numeric_or_logical(value),
244        V::Numeric => validation::value_is_numeric(value),
245        V::Text => validation::value_is_text(value),
246        V::TextScalar => validation::value_is_text_scalar(value),
247        V::NonzeroLengthText => validation::value_is_nonzero_length_text(value),
248        V::Nonempty => !validation::value_is_empty(value),
249        V::ScalarOrEmpty => validation::value_is_scalar_or_empty(value),
250        V::Real => validation::value_is_real_async(value).await?,
251        V::Integer => {
252            validation::ensure_resident_extension(value, "mustBeInteger")?;
253            validation::value_is_integer_async(value).await?
254        }
255        V::Vector { allow_all_empties } => {
256            validation::value_satisfies_vector_validator(value, *allow_all_empties)?
257        }
258        V::Positive => validation::value_is_positive_async(value).await?,
259        V::Negative => validation::value_is_negative_async(value).await?,
260        V::Nonnegative => validation::value_is_nonnegative_async(value).await?,
261        V::Nonmissing => validation::value_is_nonmissing_async(value).await?,
262        V::NonNan => {
263            validation::ensure_resident_extension(value, "mustBeNonNan")?;
264            validation::value_is_non_nan_async(value).await?
265        }
266        V::Nonzero => {
267            validation::ensure_resident_extension(value, "mustBeNonzero")?;
268            validation::value_is_nonzero_async(value).await?
269        }
270        V::Nonpositive => validation::value_is_nonpositive_async(value).await?,
271        V::Nonsparse => {
272            validation::dispatch_validator_async("mustBeNonsparse", vec![value.clone()])
273                .await
274                .is_ok()
275        }
276        V::Sparse => validation::dispatch_validator_async("mustBeSparse", vec![value.clone()])
277            .await
278            .is_ok(),
279        V::ValidVariableName => validation::isvarname_value(value),
280        V::UnderlyingType(names) => {
281            validation::value_underlying_type_matches(value, names.clone())?
282        }
283        V::Member(literals) => {
284            let allowed = literals.iter().map(literal_atom).collect::<Vec<_>>();
285            validation::value_is_member_atoms_async(value, &allowed).await?
286        }
287        V::InRange(lower, upper, inclusivity) => {
288            validation::value_is_in_range_documented_async(
289                value,
290                &Value::Num(*lower),
291                &Value::Num(*upper),
292                validation::RangeInclusivity {
293                    lower: inclusivity.lower,
294                    upper: inclusivity.upper,
295                },
296            )
297            .await?
298        }
299        V::GreaterThanOrEqual(threshold) => {
300            validation::value_is_greater_than_or_equal_values_async(value, &Value::Num(*threshold))
301                .await?
302        }
303        V::LessThanOrEqual(threshold) => {
304            validation::value_is_less_than_or_equal_values_async(value, &Value::Num(*threshold))
305                .await?
306        }
307        V::GreaterThan(threshold) => {
308            validation::value_is_greater_than_values_async(value, &Value::Num(*threshold)).await?
309        }
310        V::LessThan(threshold) => {
311            validation::value_is_less_than_values_async(value, &Value::Num(*threshold)).await?
312        }
313    })
314}
315
316fn literal_atom(literal: &FunctionArgValidationLiteral) -> validation::ValidationAtom {
317    match literal {
318        FunctionArgValidationLiteral::Number(value) => validation::ValidationAtom::Number(*value),
319        FunctionArgValidationLiteral::Integer(value) => {
320            validation::ValidationAtom::Integer(IntValue::from(value))
321        }
322        FunctionArgValidationLiteral::Text(value) => {
323            validation::ValidationAtom::Text(value.clone())
324        }
325        FunctionArgValidationLiteral::Bool(value) => validation::ValidationAtom::Bool(*value),
326    }
327}
328
329fn validator_name(validator: &FunctionArgValidator) -> &'static str {
330    use FunctionArgValidator as V;
331    match validator {
332        V::A(_) => "mustBeA",
333        V::Column => "mustBeColumn",
334        V::Finite => "mustBeFinite",
335        V::Float => "mustBeFloat",
336        V::Folder => "mustBeFolder",
337        V::File => "mustBeFile",
338        V::NumericOrLogical => "mustBeNumericOrLogical",
339        V::Numeric => "mustBeNumeric",
340        V::Text => "mustBeText",
341        V::TextScalar => "mustBeTextScalar",
342        V::NonzeroLengthText => "mustBeNonzeroLengthText",
343        V::Nonempty => "mustBeNonempty",
344        V::ScalarOrEmpty => "mustBeScalarOrEmpty",
345        V::Real => "mustBeReal",
346        V::Integer => "mustBeInteger",
347        V::Vector { .. } => "mustBeVector",
348        V::Positive => "mustBePositive",
349        V::Negative => "mustBeNegative",
350        V::Nonnegative => "mustBeNonnegative",
351        V::Nonmissing => "mustBeNonmissing",
352        V::NonNan => "mustBeNonNan",
353        V::Nonzero => "mustBeNonzero",
354        V::Nonpositive => "mustBeNonpositive",
355        V::Nonsparse => "mustBeNonsparse",
356        V::Sparse => "mustBeSparse",
357        V::ValidVariableName => "mustBeValidVariableName",
358        V::UnderlyingType(_) => "mustBeUnderlyingType",
359        V::Member(_) => "mustBeMember",
360        V::InRange(_, _, _) => "mustBeInRange",
361        V::GreaterThanOrEqual(_) => "mustBeGreaterThanOrEqual",
362        V::LessThanOrEqual(_) => "mustBeLessThanOrEqual",
363        V::GreaterThan(_) => "mustBeGreaterThan",
364        V::LessThan(_) => "mustBeLessThan",
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn prepares_defaults_varargin_and_nargin() {
374        let positive = [FunctionArgValidator::Positive];
375        let default = FunctionArgDefaultValue::Number(3.0);
376        let specs = [FunctionInputSpec {
377            input_index: 1,
378            size: None,
379            class_name: None,
380            validators: &positive,
381            default_value: Some(&default),
382        }];
383        let prepared = prepare_function_inputs("sample", &[Value::Num(1.0)], 2, true, &specs)
384            .expect("prepare inputs");
385        assert_eq!(
386            prepared.fixed,
387            vec![Some(Value::Num(1.0)), Some(Value::Num(3.0))]
388        );
389        assert_eq!(prepared.nargin, 1);
390        assert_eq!(prepared.varargin.unwrap().data.len(), 0);
391    }
392
393    #[test]
394    fn preserves_omitted_unassigned_inputs_and_rejects_validation_failures() {
395        let positive = [FunctionArgValidator::Positive];
396        let specs = [FunctionInputSpec {
397            input_index: 0,
398            size: None,
399            class_name: None,
400            validators: &positive,
401            default_value: None,
402        }];
403        assert_eq!(
404            prepare_function_inputs("sample", &[], 1, false, &specs)
405                .unwrap()
406                .fixed,
407            vec![None]
408        );
409        let error =
410            prepare_function_inputs("sample", &[Value::Num(-1.0)], 1, false, &specs).unwrap_err();
411        assert_eq!(
412            error.identifier(),
413            Some("RunMat:ArgumentValidationFunction")
414        );
415    }
416
417    #[test]
418    fn collects_fixed_and_variadic_outputs_exactly() {
419        let cell = CellArray::new(vec![Value::Num(2.0), Value::Num(3.0)], 1, 2).unwrap();
420        assert_eq!(
421            collect_function_outputs("sample", &[Value::Num(1.0)], Some(&Value::Cell(cell)), 3,)
422                .unwrap(),
423            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)]
424        );
425    }
426}