runmat_runtime/builtins/diagnostics/
error.rs1use std::convert::TryFrom;
4
5use runmat_builtins::{StructValue, Value};
6use runmat_macros::runtime_builtin;
7
8use crate::builtins::common::format::format_variadic;
9use crate::builtins::common::spec::{
10 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
11 ReductionNaN, ResidencyPolicy, ShapeRequirements,
12};
13#[cfg(feature = "doc_export")]
14use crate::register_builtin_doc_text;
15use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
16
17const DEFAULT_IDENTIFIER: &str = "MATLAB:error";
18
19#[cfg(feature = "doc_export")]
20pub const DOC_MD: &str = r#"---
21title: "error"
22category: "diagnostics"
23keywords: ["error", "exception", "throw", "diagnostics", "message"]
24summary: "Throw an exception with an identifier and a formatted diagnostic message."
25references: []
26gpu_support:
27 elementwise: false
28 reduction: false
29 precisions: []
30 broadcasting: "none"
31 notes: "Always executed on the host; GPU tensors are gathered only when they appear in formatted arguments."
32fusion:
33 elementwise: false
34 reduction: false
35 max_inputs: 0
36 constants: "inline"
37requires_feature: null
38tested:
39 unit: "builtins::diagnostics::error::tests"
40 integration: null
41---
42
43# What does the `error` function do in MATLAB / RunMat?
44`error` throws an exception immediately, unwinding the current execution frame and transferring control to the nearest `catch` block (or aborting the program if none exists). RunMat mirrors MATLAB's behaviour, including support for message identifiers, formatted messages, `MException` objects, and legacy message structs.
45
46## How does the `error` function behave in MATLAB / RunMat?
47- `error(message)` throws using the default identifier `MATLAB:error`.
48- `error(id, message)` uses a custom identifier. Identifiers are normalised to `MATLAB:*` when they do not already contain a namespace.
49- `error(fmt, arg1, ...)` formats the message with MATLAB's `sprintf` rules before throwing.
50- `error(id, fmt, arg1, ...)` combines both a custom identifier and formatted message text.
51- `error(MException_obj)` rethrows an existing exception without altering its identifier or message.
52- `error(struct('identifier', id, 'message', msg))` honours the legacy structure form.
53- Invalid invocations (missing message, extra arguments after an `MException`, malformed structs, etc.) themselves raise `MATLAB:error` diagnostics so the caller can correct usage.
54
55The thrown exception is observed in MATLAB-compatible `try`/`catch` constructs or by the embedding runtime, which converts the string back into an `MException` object.
56
57## GPU execution and residency
58`error` is a control-flow builtin and never executes on the GPU. When formatting messages that include GPU-resident arrays (for example, via `%g` or `%s` specifiers), RunMat first gathers those values back to host memory so that the final diagnostic message accurately reflects the data the user passed.
59
60## Examples of using the `error` function in MATLAB / RunMat
61
62### Throwing an error with a simple message
63```matlab
64try
65 error("Computation failed.");
66catch err
67 fprintf("%s -> %s\n", err.identifier, err.message);
68end
69```
70
71### Throwing an error with a custom identifier
72```matlab
73try
74 error("runmat:examples:invalidState", "State vector is empty.");
75catch err
76 fprintf("%s\n", err.identifier);
77end
78```
79
80### Formatting values inside the error message
81```matlab
82value = 42;
83try
84 error("MATLAB:demo:badValue", "Value %d is outside [%d, %d].", value, 0, 10);
85catch err
86 disp(err.message);
87end
88```
89
90### Rethrowing an existing MException
91```matlab
92try
93 try
94 error("MATLAB:inner:failure", "Inner failure.");
95 catch inner
96 error(inner); % propagate with original identifier/message
97 end
98catch err
99 fprintf("%s\n", err.identifier);
100end
101```
102
103### Using a legacy message struct
104```matlab
105S.identifier = "toolbox:demo:badInput";
106S.message = "Inputs must be positive integers.";
107try
108 error(S);
109catch err
110 fprintf("%s\n", err.identifier);
111end
112```
113
114## FAQ
115
1161. **How do I choose a custom identifier?** Use `component:mnemonic` style strings such as `"MATLAB:io:fileNotFound"` or `"runmat:tools:badInput"`. If you omit a namespace (`:`), RunMat prefixes the identifier with `MATLAB:` automatically.
1172. **Can I rethrow an existing `MException`?** Yes. Pass the object returned by `catch err` directly to `error(err)` to propagate it unchanged.
1183. **What happens if I pass extra arguments after an `MException` or struct?** RunMat treats that as invalid usage and raises `MATLAB:error` explaining that no additional arguments are allowed in those forms.
1194. **Does `error` run on the GPU?** No. The builtin executes on the host. If the message references GPU data, RunMat gathers the values before formatting the diagnostic string.
1205. **What if I call `error` without arguments?** RunMat raises `MATLAB:error` indicating that a message is required, matching MATLAB's behaviour.
1216. **Why was my identifier normalised to `MATLAB:...`?** MATLAB requires message identifiers to contain at least one namespace separator (`:`). RunMat enforces this rule so diagnostics integrate cleanly with tooling that expects fully-qualified identifiers.
1227. **Can the message span multiple lines?** Yes. Any newline characters in the formatted message are preserved exactly in the thrown exception.
1238. **Does formatting follow MATLAB rules?** Yes. `error` uses the same formatter as `sprintf`, including width/precision specifiers and numeric conversions, and will raise `MATLAB:error` if the format string is invalid or under-specified.
124#"#;
125
126pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
127 name: "error",
128 op_kind: GpuOpKind::Custom("control"),
129 supported_precisions: &[],
130 broadcast: BroadcastSemantics::None,
131 provider_hooks: &[],
132 constant_strategy: ConstantStrategy::InlineLiteral,
133 residency: ResidencyPolicy::GatherImmediately,
134 nan_mode: ReductionNaN::Include,
135 two_pass_threshold: None,
136 workgroup_size: None,
137 accepts_nan_mode: false,
138 notes: "Control-flow builtin; never dispatched to GPU backends.",
139};
140
141register_builtin_gpu_spec!(GPU_SPEC);
142
143pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
144 name: "error",
145 shape: ShapeRequirements::Any,
146 constant_strategy: ConstantStrategy::InlineLiteral,
147 elementwise: None,
148 reduction: None,
149 emits_nan: false,
150 notes: "Control-flow builtin; excluded from fusion planning.",
151};
152
153register_builtin_fusion_spec!(FUSION_SPEC);
154
155#[cfg(feature = "doc_export")]
156register_builtin_doc_text!("error", DOC_MD);
157
158#[runtime_builtin(
159 name = "error",
160 category = "diagnostics",
161 summary = "Throw an exception with an identifier and a formatted diagnostic message.",
162 keywords = "error,exception,diagnostics,throw",
163 accel = "metadata"
164)]
165fn error_builtin(args: Vec<Value>) -> Result<Value, String> {
166 if args.is_empty() {
167 return Err(build_error(
168 DEFAULT_IDENTIFIER,
169 "error: missing message argument",
170 ));
171 }
172
173 let mut iter = args.into_iter();
174 let first = iter.next().expect("checked above");
175 let rest: Vec<Value> = iter.collect();
176
177 match first {
178 Value::MException(mex) => {
179 if !rest.is_empty() {
180 return Err(build_error(
181 DEFAULT_IDENTIFIER,
182 "error: additional arguments are not allowed when passing an MException",
183 ));
184 }
185 Err(build_error(&mex.identifier, &mex.message))
186 }
187 Value::Struct(ref st) => {
188 if !rest.is_empty() {
189 return Err(build_error(
190 DEFAULT_IDENTIFIER,
191 "error: additional arguments are not allowed when passing a message struct",
192 ));
193 }
194 let (identifier, message) = extract_struct_error_fields(st)?;
195 Err(build_error(&identifier, &message))
196 }
197 other => handle_message_arguments(other, rest),
198 }
199}
200
201fn handle_message_arguments(first: Value, rest: Vec<Value>) -> Result<Value, String> {
202 let first_string = value_to_string("error", &first)?;
203
204 if rest.is_empty() {
205 return Err(build_error(DEFAULT_IDENTIFIER, &first_string));
206 }
207
208 let mut identifier = DEFAULT_IDENTIFIER.to_string();
209 let mut format_string = first_string;
210 let mut format_args: &[Value] = &rest;
211
212 if !rest.is_empty()
213 && (is_message_identifier(&format_string)
214 || looks_like_unqualified_identifier(&format_string))
215 {
216 identifier = normalize_identifier(&format_string);
217 let (message_value, extra_args) = rest.split_first().expect("rest not empty");
218 format_string = value_to_string("error", message_value)?;
219 format_args = extra_args;
220 }
221
222 let message = if format_args.is_empty() {
223 format_string
224 } else {
225 format_variadic(&format_string, format_args)
226 .map_err(|e| build_error(DEFAULT_IDENTIFIER, &e))?
227 };
228
229 Err(build_error(&identifier, &message))
230}
231
232fn extract_struct_error_fields(struct_value: &StructValue) -> Result<(String, String), String> {
233 let identifier_value = struct_value
234 .fields
235 .get("identifier")
236 .or_else(|| struct_value.fields.get("messageid"))
237 .ok_or_else(|| {
238 build_error(
239 DEFAULT_IDENTIFIER,
240 "error: message struct must contain an 'identifier' field",
241 )
242 })?;
243 let message_value = struct_value
244 .fields
245 .get("message")
246 .or_else(|| struct_value.fields.get("msg"))
247 .ok_or_else(|| {
248 build_error(
249 DEFAULT_IDENTIFIER,
250 "error: message struct must contain a 'message' field",
251 )
252 })?;
253
254 let identifier = value_to_string("error", identifier_value)?;
255 let message = value_to_string("error", message_value)?;
256 Ok((identifier, message))
257}
258
259fn value_to_string(context: &str, value: &Value) -> Result<String, String> {
260 String::try_from(value).map_err(|e| build_error(DEFAULT_IDENTIFIER, &format!("{context}: {e}")))
261}
262
263fn build_error(identifier: &str, message: &str) -> String {
264 let ident = normalize_identifier(identifier);
265 format!("{ident}: {message}")
266}
267
268fn normalize_identifier(raw: &str) -> String {
269 let trimmed = raw.trim();
270 if trimmed.is_empty() {
271 DEFAULT_IDENTIFIER.to_string()
272 } else if trimmed.contains(':') {
273 trimmed.to_string()
274 } else {
275 format!("MATLAB:{trimmed}")
276 }
277}
278
279fn is_message_identifier(text: &str) -> bool {
280 let trimmed = text.trim();
281 if trimmed.is_empty() || !trimmed.contains(':') {
282 return false;
283 }
284 trimmed
285 .chars()
286 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ':' | '_' | '.'))
287}
288
289fn looks_like_unqualified_identifier(text: &str) -> bool {
290 let trimmed = text.trim();
291 if trimmed.is_empty() || trimmed.contains(char::is_whitespace) {
292 return false;
293 }
294 trimmed
295 .chars()
296 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.'))
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use runmat_builtins::{IntValue, MException};
303
304 #[test]
305 fn error_requires_message() {
306 let err = error_builtin(Vec::new()).expect_err("should error");
307 assert!(err.contains("missing message"));
308 }
309
310 #[test]
311 fn default_identifier_is_applied() {
312 let err = error_builtin(vec![Value::from("Failure!")]).expect_err("should error");
313 assert_eq!(err, "MATLAB:error: Failure!");
314 }
315
316 #[test]
317 fn custom_identifier_is_preserved() {
318 let err = error_builtin(vec![
319 Value::from("runmat:tests:badValue"),
320 Value::from("Value %d is not allowed."),
321 Value::from(5.0),
322 ])
323 .expect_err("should error");
324 assert_eq!(err, "runmat:tests:badValue: Value 5 is not allowed.");
325 }
326
327 #[test]
328 fn identifier_is_normalised_when_namespace_missing() {
329 let err = error_builtin(vec![
330 Value::from("missingNamespace"),
331 Value::from("Message"),
332 ])
333 .expect_err("should error");
334 assert_eq!(err, "MATLAB:missingNamespace: Message");
335 }
336
337 #[test]
338 fn format_string_with_colon_not_treated_as_identifier() {
339 let err = error_builtin(vec![
340 Value::from("Value: %d."),
341 Value::Int(IntValue::I32(7)),
342 ])
343 .expect_err("should error");
344 assert_eq!(err, "MATLAB:error: Value: 7.");
345 }
346
347 #[test]
348 fn error_accepts_mexception() {
349 let mex = MException::new("MATLAB:demo:test".to_string(), "broken".to_string());
350 let err = error_builtin(vec![Value::MException(mex)]).expect_err("should error");
351 assert_eq!(err, "MATLAB:demo:test: broken");
352 }
353
354 #[test]
355 fn error_rejects_extra_args_after_mexception() {
356 let mex = MException::new("MATLAB:demo:test".to_string(), "broken".to_string());
357 let err = error_builtin(vec![Value::MException(mex), Value::from(1.0)])
358 .expect_err("should error");
359 assert!(err.contains("additional arguments"));
360 }
361
362 #[test]
363 fn error_accepts_message_struct() {
364 let mut st = StructValue::new();
365 st.fields
366 .insert("identifier".to_string(), Value::from("pkg:demo:failure"));
367 st.fields
368 .insert("message".to_string(), Value::from("Struct message."));
369 let err = error_builtin(vec![Value::Struct(st)]).expect_err("should error");
370 assert_eq!(err, "pkg:demo:failure: Struct message.");
371 }
372
373 #[test]
374 fn error_struct_requires_message_field() {
375 let mut st = StructValue::new();
376 st.fields
377 .insert("identifier".to_string(), Value::from("pkg:demo:oops"));
378 let err = error_builtin(vec![Value::Struct(st)]).expect_err("should error");
379 assert!(err.contains("message struct must contain a 'message' field"));
380 }
381
382 #[test]
383 #[cfg(feature = "doc_export")]
384 fn doc_examples_present() {
385 use crate::builtins::common::test_support;
386 let blocks = test_support::doc_examples(DOC_MD);
387 assert!(!blocks.is_empty());
388 }
389}