1use 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}