Skip to main content

runmat_runtime/builtins/diagnostics/
error.rs

1//! MATLAB-compatible `error` builtin with structured exception handling semantics.
2
3use std::convert::TryFrom;
4
5use runmat_builtins::{
6    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
7    BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
8    BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
9    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
10    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
11    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
12};
13use runmat_macros::runtime_builtin;
14use runmat_value::{StructValue, Value};
15
16use crate::builtins::common::format::{
17    decode_escape_sequences, flatten_arguments, format_variadic,
18};
19use crate::builtins::common::spec::{
20    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
21    ReductionNaN, ResidencyPolicy, ShapeRequirements,
22};
23use crate::builtins::diagnostics::type_resolvers::error_type;
24use crate::{build_runtime_error, RuntimeError};
25
26const BUILTIN_NAME: &str = "error";
27
28const ERROR_INPUTS_MESSAGE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
29    name: "message",
30    ty: BuiltinParamType::StringScalar,
31    arity: BuiltinParamArity::Required,
32    default: None,
33    description: "Error message text.",
34}];
35
36const ERROR_INPUTS_MESSAGE_VARIADIC: [BuiltinParamDescriptor; 2] = [
37    BuiltinParamDescriptor {
38        name: "message",
39        ty: BuiltinParamType::StringScalar,
40        arity: BuiltinParamArity::Required,
41        default: None,
42        description: "Error message template text.",
43    },
44    BuiltinParamDescriptor {
45        name: "A",
46        ty: BuiltinParamType::Any,
47        arity: BuiltinParamArity::Variadic,
48        default: None,
49        description: "Formatting values for the message template.",
50    },
51];
52
53const ERROR_INPUTS_IDENTIFIER_MESSAGE: [BuiltinParamDescriptor; 3] = [
54    BuiltinParamDescriptor {
55        name: "message_id",
56        ty: BuiltinParamType::StringScalar,
57        arity: BuiltinParamArity::Required,
58        default: None,
59        description: "Message identifier.",
60    },
61    BuiltinParamDescriptor {
62        name: "message",
63        ty: BuiltinParamType::StringScalar,
64        arity: BuiltinParamArity::Required,
65        default: None,
66        description: "Error message text.",
67    },
68    BuiltinParamDescriptor {
69        name: "A",
70        ty: BuiltinParamType::Any,
71        arity: BuiltinParamArity::Variadic,
72        default: None,
73        description: "Formatting values for the message template.",
74    },
75];
76
77const ERROR_INPUTS_STRUCT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
78    name: "msg_struct",
79    ty: BuiltinParamType::Any,
80    arity: BuiltinParamArity::Required,
81    default: None,
82    description: "Struct containing identifier/message fields.",
83}];
84
85const ERROR_INPUTS_CORRECTION: [BuiltinParamDescriptor; 2] = [
86    BuiltinParamDescriptor {
87        name: "correction",
88        ty: BuiltinParamType::Any,
89        arity: BuiltinParamArity::Required,
90        default: None,
91        description: "MATLAB correction object.",
92    },
93    BuiltinParamDescriptor {
94        name: "messageArguments",
95        ty: BuiltinParamType::Any,
96        arity: BuiltinParamArity::Variadic,
97        default: None,
98        description: "Identifier, message, and optional formatting values.",
99    },
100];
101
102const ERROR_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
103    BuiltinSignatureDescriptor {
104        label: "error(msg)",
105        inputs: &ERROR_INPUTS_MESSAGE,
106        outputs: &[],
107    },
108    BuiltinSignatureDescriptor {
109        label: "error(msg, A)",
110        inputs: &ERROR_INPUTS_MESSAGE_VARIADIC,
111        outputs: &[],
112    },
113    BuiltinSignatureDescriptor {
114        label: "error(errID, ___)",
115        inputs: &ERROR_INPUTS_IDENTIFIER_MESSAGE,
116        outputs: &[],
117    },
118    BuiltinSignatureDescriptor {
119        label: "error(errorStruct)",
120        inputs: &ERROR_INPUTS_STRUCT,
121        outputs: &[],
122    },
123    BuiltinSignatureDescriptor {
124        label: "error(correction, ___)",
125        inputs: &ERROR_INPUTS_CORRECTION,
126        outputs: &[],
127    },
128];
129
130const ERROR_ERROR_MISSING_MESSAGE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
131    code: "RM.ERROR.MISSING_MESSAGE",
132    identifier: Some("RunMat:error"),
133    when: "No arguments are supplied.",
134    message: "error: missing message argument",
135};
136
137const ERROR_ERROR_EXTRA_ARGS_MEXCEPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
138    code: "RM.ERROR.MEXCEPTION_EXTRA_ARGS",
139    identifier: Some("RunMat:error"),
140    when: "Additional arguments are supplied after an MException input.",
141    message: "error: additional arguments are not allowed when passing an MException",
142};
143
144const ERROR_ERROR_EXTRA_ARGS_STRUCT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
145    code: "RM.ERROR.STRUCT_EXTRA_ARGS",
146    identifier: Some("RunMat:error"),
147    when: "Additional arguments are supplied after a message-struct input.",
148    message: "error: additional arguments are not allowed when passing a message struct",
149};
150
151const ERROR_ERROR_STRUCT_NO_FIELDS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
152    code: "RM.ERROR.STRUCT_NO_FIELDS",
153    identifier: Some("RunMat:error"),
154    when: "Message struct contains none of message, identifier, or stack.",
155    message: "error: message struct must contain 'message', 'identifier', or 'stack'",
156};
157
158const ERROR_ERROR_STRUCT_STACK_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
159    code: "RM.ERROR.STRUCT_STACK_UNSUPPORTED",
160    identifier: Some("RunMat:error"),
161    when: "Message struct requests an explicit MATLAB stack that cannot be represented yet.",
162    message: "error: explicit errorStruct stack is not supported yet",
163};
164
165const ERROR_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
166    code: "RM.ERROR.INVALID_INPUT",
167    identifier: Some("RunMat:error"),
168    when: "Identifier/message inputs or format arguments are not string-compatible.",
169    message: "error: invalid input argument",
170};
171
172const ERROR_ERROR_INVALID_IDENTIFIER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
173    code: "RM.ERROR.INVALID_IDENTIFIER",
174    identifier: Some("RunMat:error"),
175    when: "Identifier fields do not follow documented colon-separated identifier grammar.",
176    message: "error: invalid error identifier",
177};
178
179const ERROR_ERROR_CORRECTION_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
180    code: "RM.ERROR.CORRECTION_UNSUPPORTED",
181    identifier: Some("RunMat:error"),
182    when: "A documented matlab.lang.correction object is supplied before correction objects are representable.",
183    message: "error: matlab.lang.correction objects are not supported yet",
184};
185
186const ERROR_ERRORS: [BuiltinErrorDescriptor; 8] = [
187    ERROR_ERROR_MISSING_MESSAGE,
188    ERROR_ERROR_EXTRA_ARGS_MEXCEPTION,
189    ERROR_ERROR_EXTRA_ARGS_STRUCT,
190    ERROR_ERROR_STRUCT_NO_FIELDS,
191    ERROR_ERROR_STRUCT_STACK_UNSUPPORTED,
192    ERROR_ERROR_INVALID_INPUT,
193    ERROR_ERROR_INVALID_IDENTIFIER,
194    ERROR_ERROR_CORRECTION_UNSUPPORTED,
195];
196
197pub const ERROR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
198    signatures: &ERROR_SIGNATURES,
199    output_mode: BuiltinOutputMode::Fixed,
200    completion_policy: BuiltinCompletionPolicy::Public,
201    errors: &ERROR_ERRORS,
202};
203
204const ERROR_MEXCEPTION_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
205    id: "error-mexception-input",
206    mode: BuiltinExtensionMode::RunMatOnly,
207    description: "error(MException) is a RunMat extension; MATLAB uses throw or rethrow",
208    error_identifier: Some("RunMat:compatibility:ErrorMExceptionExtension"),
209};
210const ERROR_UNQUALIFIED_IDENTIFIER_EXTENSION: BuiltinExtensionDescriptor =
211    BuiltinExtensionDescriptor {
212        id: "error-unqualified-identifier",
213        mode: BuiltinExtensionMode::RunMatOnly,
214        description:
215            "Treating an unqualified leading token as an error identifier is a RunMat extension",
216        error_identifier: Some("RunMat:compatibility:ErrorUnqualifiedIdentifierExtension"),
217    };
218const ERROR_STRUCT_ALIAS_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
219    id: "error-struct-field-aliases",
220    mode: BuiltinExtensionMode::RunMatOnly,
221    description: "messageid and msg error-structure aliases are a RunMat extension",
222    error_identifier: Some("RunMat:compatibility:ErrorStructAliasExtension"),
223};
224pub const ERROR_EXTENSIONS: [BuiltinExtensionDescriptor; 3] = [
225    ERROR_MEXCEPTION_EXTENSION,
226    ERROR_UNQUALIFIED_IDENTIFIER_EXTENSION,
227    ERROR_STRUCT_ALIAS_EXTENSION,
228];
229
230const ERROR_INTEGER_FORMAT_INPUT: [BuiltinIntegerInputCapability; 1] =
231    [BuiltinIntegerInputCapability {
232        name: "A",
233        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
234        availability: BuiltinIntegerInputAvailability::Documented,
235        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
236        notes: "All eight typed-integer classes are documented numeric formatting values and remain exact through integer conversion specifiers.",
237    }];
238const ERROR_REJECTED_INTEGER_TEXT_INPUTS: [BuiltinIntegerInputCapability; 2] = [
239    BuiltinIntegerInputCapability {
240        name: "msg",
241        classes: &[],
242        availability: BuiltinIntegerInputAvailability::Rejected,
243        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
244        notes: "The message role is a text scalar and never converts integer data to text implicitly.",
245    },
246    BuiltinIntegerInputCapability {
247        name: "errID",
248        classes: &[],
249        availability: BuiltinIntegerInputAvailability::Rejected,
250        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
251        notes: "The identifier role is a text scalar and rejects integer values before provider access.",
252    },
253];
254pub const ERROR_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
255    BuiltinIntegerCapabilityDescriptor {
256        form: "error(msg, integer_A) or error(errID, msg, integer_A)",
257        inputs: &ERROR_INTEGER_FORMAT_INPUT,
258        computation_domain: BuiltinIntegerComputationDomain::FunctionSpecific,
259        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
260        overflow: BuiltinIntegerOverflowRule::NotApplicable,
261        backend: BuiltinIntegerBackendRule::GatherFallback,
262        overload: BuiltinIntegerOverloadKind::Multiple,
263        notes: "Host formatting is exact for integer conversions; documented GPU-array arguments gather only after host message and identifier validation.",
264    },
265    BuiltinIntegerCapabilityDescriptor {
266        form: "error(integer_msg, ...) or error(integer_errID, ...)",
267        inputs: &ERROR_REJECTED_INTEGER_TEXT_INPUTS,
268        computation_domain: BuiltinIntegerComputationDomain::FunctionSpecific,
269        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
270        overflow: BuiltinIntegerOverflowRule::NotApplicable,
271        backend: BuiltinIntegerBackendRule::HostOnly,
272        overload: BuiltinIntegerOverloadKind::FunctionSpecific,
273        notes: "Integer values are valid only in formatting-value roles, never as message or identifier text.",
274    },
275];
276
277#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::diagnostics::error")]
278pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
279    name: "error",
280    op_kind: GpuOpKind::Custom("control"),
281    supported_precisions: &[],
282    broadcast: BroadcastSemantics::None,
283    provider_hooks: &[],
284    constant_strategy: ConstantStrategy::InlineLiteral,
285    residency: ResidencyPolicy::GatherImmediately,
286    nan_mode: ReductionNaN::Include,
287    two_pass_threshold: None,
288    workgroup_size: None,
289    accepts_nan_mode: false,
290    notes: "Control-flow builtin; never dispatched to GPU backends.",
291};
292
293#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::diagnostics::error")]
294pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
295    name: "error",
296    shape: ShapeRequirements::Any,
297    constant_strategy: ConstantStrategy::InlineLiteral,
298    elementwise: None,
299    reduction: None,
300    emits_nan: false,
301    notes: "Control-flow builtin; excluded from fusion planning.",
302};
303
304fn error_flow(identifier: &str, message: impl Into<String>) -> RuntimeError {
305    build_runtime_error(message)
306        .with_builtin(BUILTIN_NAME)
307        .with_identifier(normalize_identifier(identifier))
308        .build()
309}
310
311fn error_default_identifier() -> &'static str {
312    ERROR_ERROR_MISSING_MESSAGE
313        .identifier
314        .expect("error default identifier must be defined")
315}
316
317fn error_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
318    error_error_with_message(error.message, error)
319}
320
321fn error_error_with_message(
322    message: impl Into<String>,
323    error: &'static BuiltinErrorDescriptor,
324) -> RuntimeError {
325    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
326    if let Some(identifier) = error.identifier {
327        builder = builder.with_identifier(normalize_identifier(identifier));
328    }
329    builder.build()
330}
331
332fn remap_error_flow(err: RuntimeError, error: &'static BuiltinErrorDescriptor) -> RuntimeError {
333    let mut builder = build_runtime_error(err.message().to_string())
334        .with_builtin(BUILTIN_NAME)
335        .with_source(err);
336    if let Some(identifier) = error.identifier {
337        builder = builder.with_identifier(normalize_identifier(identifier));
338    }
339    builder.build()
340}
341
342#[runtime_builtin(
343    name = "error",
344    category = "diagnostics",
345    summary = "Throw exceptions with identifiers and formatted messages.",
346    keywords = "error,exception,diagnostics,throw",
347    accel = "metadata",
348    type_resolver(error_type),
349    descriptor(crate::builtins::diagnostics::error::ERROR_DESCRIPTOR),
350    extensions(crate::builtins::diagnostics::error::ERROR_EXTENSIONS),
351    integer_capabilities(crate::builtins::diagnostics::error::ERROR_INTEGER_CAPABILITIES),
352    builtin_path = "crate::builtins::diagnostics::error"
353)]
354async fn error_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
355    if args.is_empty() {
356        return Err(error_error(&ERROR_ERROR_MISSING_MESSAGE));
357    }
358    if args.iter().all(value_is_empty_array) {
359        return Ok(Value::Num(0.0));
360    }
361
362    let mut iter = args.into_iter();
363    let first = iter.next().expect("checked above");
364    let rest: Vec<Value> = iter.collect();
365
366    match first {
367        Value::MException(mex) => {
368            crate::compatibility::ensure_builtin_extension_enabled(
369                &ERROR_MEXCEPTION_EXTENSION,
370                BUILTIN_NAME,
371            )?;
372            if !rest.is_empty() {
373                return Err(error_error(&ERROR_ERROR_EXTRA_ARGS_MEXCEPTION));
374            }
375            Err(error_flow(&mex.identifier, &mex.message))
376        }
377        Value::Struct(ref st) => {
378            if !rest.is_empty() {
379                return Err(error_error(&ERROR_ERROR_EXTRA_ARGS_STRUCT));
380            }
381            let (identifier, message) = extract_struct_error_fields(st)?;
382            Err(error_flow(&identifier, &message))
383        }
384        Value::Object(object) if object.class_name.starts_with("matlab.lang.correction.") => {
385            Err(error_error(&ERROR_ERROR_CORRECTION_UNSUPPORTED))
386        }
387        other => handle_message_arguments(other, rest).await,
388    }
389}
390
391async fn handle_message_arguments(first: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
392    let first_string = value_to_string("error", &first)?;
393
394    if rest.is_empty() {
395        return Err(error_flow(error_default_identifier(), first_string));
396    }
397
398    let mut identifier = error_default_identifier().to_string();
399    let mut format_string = first_string;
400    let mut format_args: &[Value] = &rest;
401
402    if !rest.is_empty() && is_message_identifier(&format_string) {
403        identifier = normalize_identifier(&format_string);
404        let (message_value, extra_args) = rest.split_first().expect("rest not empty");
405        format_string = value_to_string("error", message_value)?;
406        format_args = extra_args;
407    } else if !rest.is_empty() && looks_like_unqualified_identifier(&format_string) {
408        crate::compatibility::ensure_builtin_extension_enabled(
409            &ERROR_UNQUALIFIED_IDENTIFIER_EXTENSION,
410            BUILTIN_NAME,
411        )?;
412        identifier = normalize_identifier(&format_string);
413        let (message_value, extra_args) = rest.split_first().expect("rest not empty");
414        format_string = value_to_string("error", message_value)?;
415        format_args = extra_args;
416    }
417
418    let decoded = decode_escape_sequences(BUILTIN_NAME, &format_string)
419        .map_err(|flow| remap_error_flow(flow, &ERROR_ERROR_INVALID_INPUT))?;
420    let message = if format_args.is_empty() {
421        decoded
422    } else {
423        let flattened = flatten_arguments(format_args, BUILTIN_NAME)
424            .await
425            .map_err(|flow| remap_error_flow(flow, &ERROR_ERROR_INVALID_INPUT))?;
426        format_variadic(&decoded, &flattened)
427            .map_err(|flow| remap_error_flow(flow, &ERROR_ERROR_INVALID_INPUT))?
428    };
429
430    Err(error_flow(&identifier, message))
431}
432
433fn extract_struct_error_fields(
434    struct_value: &StructValue,
435) -> crate::BuiltinResult<(String, String)> {
436    if struct_value.fields.contains_key("stack") {
437        return Err(error_error(&ERROR_ERROR_STRUCT_STACK_UNSUPPORTED));
438    }
439    let mut identifier_value = struct_value.fields.get("identifier");
440    let mut message_value = struct_value.fields.get("message");
441    let uses_alias = identifier_value.is_none() && struct_value.fields.contains_key("messageid")
442        || message_value.is_none() && struct_value.fields.contains_key("msg");
443    if uses_alias {
444        crate::compatibility::ensure_builtin_extension_enabled(
445            &ERROR_STRUCT_ALIAS_EXTENSION,
446            BUILTIN_NAME,
447        )?;
448        identifier_value = identifier_value.or_else(|| struct_value.fields.get("messageid"));
449        message_value = message_value.or_else(|| struct_value.fields.get("msg"));
450    }
451    if identifier_value.is_none() && message_value.is_none() {
452        return Err(error_error(&ERROR_ERROR_STRUCT_NO_FIELDS));
453    }
454
455    let identifier = match identifier_value {
456        Some(value) => value_to_string("error", value)?,
457        None => error_default_identifier().to_string(),
458    };
459    if !identifier.is_empty() && !is_message_identifier(&identifier) {
460        return Err(error_error(&ERROR_ERROR_INVALID_IDENTIFIER));
461    }
462    let message = match message_value {
463        Some(value) => value_to_string("error", value)?,
464        None => String::new(),
465    };
466    Ok((identifier, message))
467}
468
469fn value_is_empty_array(value: &Value) -> bool {
470    match value {
471        Value::CharArray(array) => array.data.is_empty(),
472        Value::StringArray(array) => array.data.is_empty(),
473        Value::Tensor(tensor) => tensor.is_empty(),
474        Value::ComplexTensor(tensor) => tensor.is_empty(),
475        Value::LogicalArray(array) => array.data.is_empty(),
476        Value::Cell(cell) => cell.data.is_empty(),
477        _ => false,
478    }
479}
480
481fn value_to_string(context: &str, value: &Value) -> crate::BuiltinResult<String> {
482    String::try_from(value).map_err(|e| {
483        error_error_with_message(format!("{context}: {e}"), &ERROR_ERROR_INVALID_INPUT)
484    })
485}
486
487fn normalize_identifier(raw: &str) -> String {
488    let trimmed = raw.trim();
489    if trimmed.is_empty() {
490        error_default_identifier().to_string()
491    } else if trimmed.contains(':') {
492        trimmed.to_string()
493    } else {
494        format!("RunMat:{trimmed}")
495    }
496}
497
498fn is_message_identifier(text: &str) -> bool {
499    let trimmed = text.trim();
500    if trimmed.is_empty() || !trimmed.contains(':') {
501        return false;
502    }
503    trimmed.split(':').all(|field| {
504        let mut chars = field.chars();
505        chars.next().is_some_and(|ch| ch.is_ascii_alphabetic())
506            && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
507    })
508}
509
510fn looks_like_unqualified_identifier(text: &str) -> bool {
511    let trimmed = text.trim();
512    if trimmed.is_empty() || trimmed.contains(char::is_whitespace) {
513        return false;
514    }
515    trimmed
516        .chars()
517        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.'))
518}
519
520#[cfg(test)]
521pub(crate) mod tests {
522    use super::*;
523    use runmat_builtins::{ResolveContext, Type};
524    use runmat_value::{CharArray, IntValue, IntegerStorage, MException, Tensor};
525
526    fn run_error(args: Vec<Value>) -> crate::BuiltinResult<Value> {
527        futures::executor::block_on(super::error_builtin(args))
528    }
529
530    fn unwrap_error(err: crate::RuntimeError) -> crate::RuntimeError {
531        err
532    }
533
534    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
535    #[test]
536    fn error_requires_message() {
537        let err = unwrap_error(run_error(Vec::new()).expect_err("should error"));
538        assert_eq!(err.identifier(), Some(error_default_identifier()));
539        assert!(err.message().contains("missing message"));
540    }
541
542    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
543    #[test]
544    fn default_identifier_is_applied() {
545        let err = unwrap_error(run_error(vec![Value::from("Failure!")]).expect_err("should error"));
546        assert_eq!(err.identifier(), Some(error_default_identifier()));
547        assert_eq!(err.message(), "Failure!");
548    }
549
550    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
551    #[test]
552    fn custom_identifier_is_preserved() {
553        let err = unwrap_error(
554            run_error(vec![
555                Value::from("runmat:tests:badValue"),
556                Value::from("Value %d is not allowed."),
557                Value::from(5.0),
558            ])
559            .expect_err("should error"),
560        );
561        assert_eq!(err.identifier(), Some("runmat:tests:badValue"));
562        assert_eq!(err.message(), "Value 5 is not allowed.");
563    }
564
565    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
566    #[test]
567    fn identifier_is_normalised_when_namespace_missing() {
568        let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
569        let err = unwrap_error(
570            run_error(vec![
571                Value::from("missingNamespace"),
572                Value::from("Message"),
573            ])
574            .expect_err("should error"),
575        );
576        assert_eq!(err.identifier(), Some("RunMat:missingNamespace"));
577        assert_eq!(err.message(), "Message");
578    }
579
580    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
581    #[test]
582    fn format_string_with_colon_not_treated_as_identifier() {
583        let err = unwrap_error(
584            run_error(vec![
585                Value::from("Value: %d."),
586                Value::Int(IntValue::I32(7)),
587            ])
588            .expect_err("should error"),
589        );
590        assert_eq!(err.identifier(), Some(error_default_identifier()));
591        assert_eq!(err.message(), "Value: 7.");
592    }
593
594    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
595    #[test]
596    fn error_accepts_mexception() {
597        let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
598        let mex = MException::new("RunMat:demo:test".to_string(), "broken".to_string());
599        let err = unwrap_error(run_error(vec![Value::MException(mex)]).expect_err("should error"));
600        assert_eq!(err.identifier(), Some("RunMat:demo:test"));
601        assert_eq!(err.message(), "broken");
602    }
603
604    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
605    #[test]
606    fn error_rejects_extra_args_after_mexception() {
607        let _runmat = crate::compatibility::push_runmat_extensions_enabled(true);
608        let mex = MException::new("RunMat:demo:test".to_string(), "broken".to_string());
609        let err = unwrap_error(
610            run_error(vec![Value::MException(mex), Value::from(1.0)]).expect_err("should error"),
611        );
612        assert_eq!(err.identifier(), Some(error_default_identifier()));
613        assert!(err.message().contains("additional arguments"));
614    }
615
616    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
617    #[test]
618    fn error_accepts_message_struct() {
619        let mut st = StructValue::new();
620        st.fields
621            .insert("identifier".to_string(), Value::from("pkg:demo:failure"));
622        st.fields
623            .insert("message".to_string(), Value::from("Struct message."));
624        let err = unwrap_error(run_error(vec![Value::Struct(st)]).expect_err("should error"));
625        assert_eq!(err.identifier(), Some("pkg:demo:failure"));
626        assert_eq!(err.message(), "Struct message.");
627    }
628
629    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
630    #[test]
631    fn error_struct_accepts_identifier_only() {
632        let mut st = StructValue::new();
633        st.fields
634            .insert("identifier".to_string(), Value::from("pkg:demo:oops"));
635        let err = unwrap_error(run_error(vec![Value::Struct(st)]).expect_err("should error"));
636        assert_eq!(err.identifier(), Some("pkg:demo:oops"));
637        assert_eq!(err.message(), "");
638    }
639
640    #[test]
641    fn error_all_empty_inputs_do_not_throw() {
642        let empty = CharArray::new(Vec::new(), 1, 0).expect("empty char vector");
643        assert_eq!(
644            run_error(vec![Value::CharArray(empty)]).expect("all-empty error is a no-op"),
645            Value::Num(0.0)
646        );
647    }
648
649    #[test]
650    fn error_multiargument_form_decodes_escapes() {
651        let err = run_error(vec![
652            Value::String("value=%d\\nnext".to_string()),
653            Value::Int(IntValue::I32(7)),
654        ])
655        .expect_err("error must throw");
656        assert_eq!(err.message(), "value=7\nnext");
657    }
658
659    #[test]
660    fn error_formats_all_integer_classes_without_f64_mirroring() {
661        for value in [
662            IntValue::I8(-8),
663            IntValue::I16(-16),
664            IntValue::I32(-32),
665            IntValue::I64(i64::MIN),
666            IntValue::U8(8),
667            IntValue::U16(16),
668            IntValue::U32(32),
669            IntValue::U64(u64::MAX),
670        ] {
671            let expected = match &value {
672                IntValue::I8(v) => v.to_string(),
673                IntValue::I16(v) => v.to_string(),
674                IntValue::I32(v) => v.to_string(),
675                IntValue::I64(v) => v.to_string(),
676                IntValue::U8(v) => v.to_string(),
677                IntValue::U16(v) => v.to_string(),
678                IntValue::U32(v) => v.to_string(),
679                IntValue::U64(v) => v.to_string(),
680            };
681            let format = if matches!(
682                &value,
683                IntValue::U8(_) | IntValue::U16(_) | IntValue::U32(_) | IntValue::U64(_)
684            ) {
685                "value=%u"
686            } else {
687                "value=%d"
688            };
689            let err = run_error(vec![Value::String(format.to_string()), Value::Int(value)])
690                .expect_err("error must throw");
691            assert_eq!(err.message(), format!("value={expected}"));
692        }
693    }
694
695    #[test]
696    fn strict_mode_gates_error_extensions_independently() {
697        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
698        let unqualified = run_error(vec![
699            Value::String("unqualified".to_string()),
700            Value::String("message".to_string()),
701        ])
702        .expect_err("unqualified identifier extension");
703        assert_eq!(
704            unqualified.identifier(),
705            ERROR_UNQUALIFIED_IDENTIFIER_EXTENSION.error_identifier
706        );
707
708        let mex = MException::new("pkg:test".to_string(), "message".to_string());
709        let mex_error =
710            run_error(vec![Value::MException(mex)]).expect_err("MException input extension");
711        assert_eq!(
712            mex_error.identifier(),
713            ERROR_MEXCEPTION_EXTENSION.error_identifier
714        );
715    }
716
717    #[test]
718    fn error_descriptor_and_integer_capabilities_cover_settled_forms() {
719        assert_eq!(ERROR_DESCRIPTOR.signatures.len(), 5);
720        assert!(ERROR_DESCRIPTOR
721            .signatures
722            .iter()
723            .all(|signature| signature.outputs.is_empty()));
724        let builtin = runmat_builtins::builtin_function_by_name("error").expect("registered");
725        assert_eq!(builtin.integer_capabilities.len(), 2);
726        assert_eq!(builtin.integer_capabilities[0].inputs[0].classes.len(), 8);
727        assert_eq!(builtin.extensions.len(), 3);
728    }
729
730    #[test]
731    fn resident_message_rejects_without_provider_access() {
732        let resident = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
733            shape: vec![1, 1],
734            device_id: u32::MAX,
735            buffer_id: u64::MAX,
736            descriptor: Default::default(),
737        });
738        let err = run_error(vec![resident]).expect_err("resident msg is not text");
739        assert_eq!(err.identifier(), ERROR_ERROR_INVALID_INPUT.identifier);
740        assert!(!err.message().contains("provider"));
741    }
742
743    #[test]
744    fn resident_integer_format_argument_gathers_exactly_from_its_owner() {
745        crate::builtins::common::test_support::with_test_provider(|provider| {
746            let tensor =
747                Tensor::new_integer(IntegerStorage::U64(vec![(1_u64 << 53) + 1]), vec![1, 1])
748                    .expect("wide integer tensor");
749            let handle = crate::builtins::common::gpu_helpers::upload_tensor(provider, &tensor)
750                .expect("upload wide integer");
751            let error = run_error(vec![
752                Value::from("integer:resident"),
753                Value::from("value %d"),
754                Value::GpuTensor(handle),
755            ])
756            .expect_err("error must throw after formatting");
757            assert_eq!(error.identifier(), Some("integer:resident"));
758            assert_eq!(error.message(), "value 9007199254740993");
759        });
760    }
761
762    #[test]
763    fn error_type_is_unknown() {
764        assert_eq!(
765            error_type(&[Type::String], &ResolveContext::new(Vec::new())),
766            Type::Unknown
767        );
768    }
769}