1use std::convert::TryFrom;
4
5use runmat_builtins::{
6 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
7 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
8 StructValue, Value,
9};
10use runmat_macros::runtime_builtin;
11
12use crate::builtins::common::format::format_variadic;
13use crate::builtins::common::spec::{
14 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
15 ReductionNaN, ResidencyPolicy, ShapeRequirements,
16};
17use crate::builtins::diagnostics::type_resolvers::error_type;
18use crate::{build_runtime_error, RuntimeError};
19
20const BUILTIN_NAME: &str = "error";
21
22const ERROR_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
23 name: "out",
24 ty: BuiltinParamType::Any,
25 arity: BuiltinParamArity::Required,
26 default: None,
27 description: "Never returned because error always throws.",
28}];
29
30const ERROR_INPUTS_MESSAGE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
31 name: "message",
32 ty: BuiltinParamType::StringScalar,
33 arity: BuiltinParamArity::Required,
34 default: None,
35 description: "Error message text.",
36}];
37
38const ERROR_INPUTS_MESSAGE_VARIADIC: [BuiltinParamDescriptor; 2] = [
39 BuiltinParamDescriptor {
40 name: "message",
41 ty: BuiltinParamType::StringScalar,
42 arity: BuiltinParamArity::Required,
43 default: None,
44 description: "Error message template text.",
45 },
46 BuiltinParamDescriptor {
47 name: "A",
48 ty: BuiltinParamType::Any,
49 arity: BuiltinParamArity::Variadic,
50 default: None,
51 description: "Formatting values for the message template.",
52 },
53];
54
55const ERROR_INPUTS_IDENTIFIER_MESSAGE: [BuiltinParamDescriptor; 2] = [
56 BuiltinParamDescriptor {
57 name: "message_id",
58 ty: BuiltinParamType::StringScalar,
59 arity: BuiltinParamArity::Required,
60 default: Some("\"RunMat:error\""),
61 description: "Message identifier.",
62 },
63 BuiltinParamDescriptor {
64 name: "message",
65 ty: BuiltinParamType::StringScalar,
66 arity: BuiltinParamArity::Required,
67 default: None,
68 description: "Error message text.",
69 },
70];
71
72const ERROR_INPUTS_IDENTIFIER_MESSAGE_VARIADIC: [BuiltinParamDescriptor; 3] = [
73 BuiltinParamDescriptor {
74 name: "message_id",
75 ty: BuiltinParamType::StringScalar,
76 arity: BuiltinParamArity::Required,
77 default: Some("\"RunMat:error\""),
78 description: "Message identifier.",
79 },
80 BuiltinParamDescriptor {
81 name: "message",
82 ty: BuiltinParamType::StringScalar,
83 arity: BuiltinParamArity::Required,
84 default: None,
85 description: "Error message template text.",
86 },
87 BuiltinParamDescriptor {
88 name: "A",
89 ty: BuiltinParamType::Any,
90 arity: BuiltinParamArity::Variadic,
91 default: None,
92 description: "Formatting values for the message template.",
93 },
94];
95
96const ERROR_INPUTS_MEXCEPTION: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
97 name: "mex",
98 ty: BuiltinParamType::Any,
99 arity: BuiltinParamArity::Required,
100 default: None,
101 description: "MException value to rethrow.",
102}];
103
104const ERROR_INPUTS_STRUCT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
105 name: "msg_struct",
106 ty: BuiltinParamType::Any,
107 arity: BuiltinParamArity::Required,
108 default: None,
109 description: "Struct containing identifier/message fields.",
110}];
111
112const ERROR_SIGNATURES: [BuiltinSignatureDescriptor; 6] = [
113 BuiltinSignatureDescriptor {
114 label: "out = error(message)",
115 inputs: &ERROR_INPUTS_MESSAGE,
116 outputs: &ERROR_OUTPUT,
117 },
118 BuiltinSignatureDescriptor {
119 label: "out = error(message, A...)",
120 inputs: &ERROR_INPUTS_MESSAGE_VARIADIC,
121 outputs: &ERROR_OUTPUT,
122 },
123 BuiltinSignatureDescriptor {
124 label: "out = error(message_id, message)",
125 inputs: &ERROR_INPUTS_IDENTIFIER_MESSAGE,
126 outputs: &ERROR_OUTPUT,
127 },
128 BuiltinSignatureDescriptor {
129 label: "out = error(message_id, message, A...)",
130 inputs: &ERROR_INPUTS_IDENTIFIER_MESSAGE_VARIADIC,
131 outputs: &ERROR_OUTPUT,
132 },
133 BuiltinSignatureDescriptor {
134 label: "out = error(mex)",
135 inputs: &ERROR_INPUTS_MEXCEPTION,
136 outputs: &ERROR_OUTPUT,
137 },
138 BuiltinSignatureDescriptor {
139 label: "out = error(msg_struct)",
140 inputs: &ERROR_INPUTS_STRUCT,
141 outputs: &ERROR_OUTPUT,
142 },
143];
144
145const ERROR_ERROR_MISSING_MESSAGE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
146 code: "RM.ERROR.MISSING_MESSAGE",
147 identifier: Some("RunMat:error"),
148 when: "No arguments are supplied.",
149 message: "error: missing message argument",
150};
151
152const ERROR_ERROR_EXTRA_ARGS_MEXCEPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
153 code: "RM.ERROR.MEXCEPTION_EXTRA_ARGS",
154 identifier: Some("RunMat:error"),
155 when: "Additional arguments are supplied after an MException input.",
156 message: "error: additional arguments are not allowed when passing an MException",
157};
158
159const ERROR_ERROR_EXTRA_ARGS_STRUCT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
160 code: "RM.ERROR.STRUCT_EXTRA_ARGS",
161 identifier: Some("RunMat:error"),
162 when: "Additional arguments are supplied after a message-struct input.",
163 message: "error: additional arguments are not allowed when passing a message struct",
164};
165
166const ERROR_ERROR_STRUCT_MISSING_IDENTIFIER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
167 code: "RM.ERROR.STRUCT_MISSING_IDENTIFIER",
168 identifier: Some("RunMat:error"),
169 when: "Message struct does not contain an identifier field.",
170 message: "error: message struct must contain an 'identifier' field",
171};
172
173const ERROR_ERROR_STRUCT_MISSING_MESSAGE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
174 code: "RM.ERROR.STRUCT_MISSING_MESSAGE",
175 identifier: Some("RunMat:error"),
176 when: "Message struct does not contain a message field.",
177 message: "error: message struct must contain a 'message' field",
178};
179
180const ERROR_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
181 code: "RM.ERROR.INVALID_INPUT",
182 identifier: Some("RunMat:error"),
183 when: "Identifier/message inputs or format arguments are not string-compatible.",
184 message: "error: invalid input argument",
185};
186
187const ERROR_ERRORS: [BuiltinErrorDescriptor; 6] = [
188 ERROR_ERROR_MISSING_MESSAGE,
189 ERROR_ERROR_EXTRA_ARGS_MEXCEPTION,
190 ERROR_ERROR_EXTRA_ARGS_STRUCT,
191 ERROR_ERROR_STRUCT_MISSING_IDENTIFIER,
192 ERROR_ERROR_STRUCT_MISSING_MESSAGE,
193 ERROR_ERROR_INVALID_INPUT,
194];
195
196pub const ERROR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
197 signatures: &ERROR_SIGNATURES,
198 output_mode: BuiltinOutputMode::Fixed,
199 completion_policy: BuiltinCompletionPolicy::Public,
200 errors: &ERROR_ERRORS,
201};
202
203#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::diagnostics::error")]
204pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
205 name: "error",
206 op_kind: GpuOpKind::Custom("control"),
207 supported_precisions: &[],
208 broadcast: BroadcastSemantics::None,
209 provider_hooks: &[],
210 constant_strategy: ConstantStrategy::InlineLiteral,
211 residency: ResidencyPolicy::GatherImmediately,
212 nan_mode: ReductionNaN::Include,
213 two_pass_threshold: None,
214 workgroup_size: None,
215 accepts_nan_mode: false,
216 notes: "Control-flow builtin; never dispatched to GPU backends.",
217};
218
219#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::diagnostics::error")]
220pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
221 name: "error",
222 shape: ShapeRequirements::Any,
223 constant_strategy: ConstantStrategy::InlineLiteral,
224 elementwise: None,
225 reduction: None,
226 emits_nan: false,
227 notes: "Control-flow builtin; excluded from fusion planning.",
228};
229
230fn error_flow(identifier: &str, message: impl Into<String>) -> RuntimeError {
231 build_runtime_error(message)
232 .with_builtin(BUILTIN_NAME)
233 .with_identifier(normalize_identifier(identifier))
234 .build()
235}
236
237fn error_default_identifier() -> &'static str {
238 ERROR_ERROR_MISSING_MESSAGE
239 .identifier
240 .expect("error default identifier must be defined")
241}
242
243fn error_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
244 error_error_with_message(error.message, error)
245}
246
247fn error_error_with_message(
248 message: impl Into<String>,
249 error: &'static BuiltinErrorDescriptor,
250) -> RuntimeError {
251 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
252 if let Some(identifier) = error.identifier {
253 builder = builder.with_identifier(normalize_identifier(identifier));
254 }
255 builder.build()
256}
257
258fn remap_error_flow(err: RuntimeError, error: &'static BuiltinErrorDescriptor) -> RuntimeError {
259 let mut builder = build_runtime_error(err.message().to_string())
260 .with_builtin(BUILTIN_NAME)
261 .with_source(err);
262 if let Some(identifier) = error.identifier {
263 builder = builder.with_identifier(normalize_identifier(identifier));
264 }
265 builder.build()
266}
267
268#[runtime_builtin(
269 name = "error",
270 category = "diagnostics",
271 summary = "Throw exceptions with identifiers and formatted messages.",
272 keywords = "error,exception,diagnostics,throw",
273 accel = "metadata",
274 type_resolver(error_type),
275 descriptor(crate::builtins::diagnostics::error::ERROR_DESCRIPTOR),
276 builtin_path = "crate::builtins::diagnostics::error"
277)]
278fn error_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
279 if args.is_empty() {
280 return Err(error_error(&ERROR_ERROR_MISSING_MESSAGE));
281 }
282
283 let mut iter = args.into_iter();
284 let first = iter.next().expect("checked above");
285 let rest: Vec<Value> = iter.collect();
286
287 match first {
288 Value::MException(mex) => {
289 if !rest.is_empty() {
290 return Err(error_error(&ERROR_ERROR_EXTRA_ARGS_MEXCEPTION));
291 }
292 Err(error_flow(&mex.identifier, &mex.message))
293 }
294 Value::Struct(ref st) => {
295 if !rest.is_empty() {
296 return Err(error_error(&ERROR_ERROR_EXTRA_ARGS_STRUCT));
297 }
298 let (identifier, message) = extract_struct_error_fields(st)?;
299 Err(error_flow(&identifier, &message))
300 }
301 other => handle_message_arguments(other, rest),
302 }
303}
304
305fn handle_message_arguments(first: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
306 let first_string = value_to_string("error", &first)?;
307
308 if rest.is_empty() {
309 return Err(error_flow(error_default_identifier(), first_string));
310 }
311
312 let mut identifier = error_default_identifier().to_string();
313 let mut format_string = first_string;
314 let mut format_args: &[Value] = &rest;
315
316 if !rest.is_empty()
317 && (is_message_identifier(&format_string)
318 || looks_like_unqualified_identifier(&format_string))
319 {
320 identifier = normalize_identifier(&format_string);
321 let (message_value, extra_args) = rest.split_first().expect("rest not empty");
322 format_string = value_to_string("error", message_value)?;
323 format_args = extra_args;
324 }
325
326 let message = if format_args.is_empty() {
327 format_string
328 } else {
329 format_variadic(&format_string, format_args)
330 .map_err(|flow| remap_error_flow(flow, &ERROR_ERROR_INVALID_INPUT))?
331 };
332
333 Err(error_flow(&identifier, message))
334}
335
336fn extract_struct_error_fields(
337 struct_value: &StructValue,
338) -> crate::BuiltinResult<(String, String)> {
339 let identifier_value = struct_value
340 .fields
341 .get("identifier")
342 .or_else(|| struct_value.fields.get("messageid"))
343 .ok_or_else(|| error_error(&ERROR_ERROR_STRUCT_MISSING_IDENTIFIER))?;
344 let message_value = struct_value
345 .fields
346 .get("message")
347 .or_else(|| struct_value.fields.get("msg"))
348 .ok_or_else(|| error_error(&ERROR_ERROR_STRUCT_MISSING_MESSAGE))?;
349
350 let identifier = value_to_string("error", identifier_value)?;
351 let message = value_to_string("error", message_value)?;
352 Ok((identifier, message))
353}
354
355fn value_to_string(context: &str, value: &Value) -> crate::BuiltinResult<String> {
356 String::try_from(value).map_err(|e| {
357 error_error_with_message(format!("{context}: {e}"), &ERROR_ERROR_INVALID_INPUT)
358 })
359}
360
361fn normalize_identifier(raw: &str) -> String {
362 let trimmed = raw.trim();
363 if trimmed.is_empty() {
364 error_default_identifier().to_string()
365 } else if trimmed.contains(':') {
366 trimmed.to_string()
367 } else {
368 format!("RunMat:{trimmed}")
369 }
370}
371
372fn is_message_identifier(text: &str) -> bool {
373 let trimmed = text.trim();
374 if trimmed.is_empty() || !trimmed.contains(':') {
375 return false;
376 }
377 trimmed
378 .chars()
379 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ':' | '_' | '.'))
380}
381
382fn looks_like_unqualified_identifier(text: &str) -> bool {
383 let trimmed = text.trim();
384 if trimmed.is_empty() || trimmed.contains(char::is_whitespace) {
385 return false;
386 }
387 trimmed
388 .chars()
389 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.'))
390}
391
392#[cfg(test)]
393pub(crate) mod tests {
394 use super::*;
395 use runmat_builtins::{IntValue, MException, ResolveContext, Type};
396
397 fn unwrap_error(err: crate::RuntimeError) -> crate::RuntimeError {
398 err
399 }
400
401 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
402 #[test]
403 fn error_requires_message() {
404 let err = unwrap_error(error_builtin(Vec::new()).expect_err("should error"));
405 assert_eq!(err.identifier(), Some(error_default_identifier()));
406 assert!(err.message().contains("missing message"));
407 }
408
409 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
410 #[test]
411 fn default_identifier_is_applied() {
412 let err =
413 unwrap_error(error_builtin(vec![Value::from("Failure!")]).expect_err("should error"));
414 assert_eq!(err.identifier(), Some(error_default_identifier()));
415 assert_eq!(err.message(), "Failure!");
416 }
417
418 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
419 #[test]
420 fn custom_identifier_is_preserved() {
421 let err = unwrap_error(
422 error_builtin(vec![
423 Value::from("runmat:tests:badValue"),
424 Value::from("Value %d is not allowed."),
425 Value::from(5.0),
426 ])
427 .expect_err("should error"),
428 );
429 assert_eq!(err.identifier(), Some("runmat:tests:badValue"));
430 assert_eq!(err.message(), "Value 5 is not allowed.");
431 }
432
433 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
434 #[test]
435 fn identifier_is_normalised_when_namespace_missing() {
436 let err = unwrap_error(
437 error_builtin(vec![
438 Value::from("missingNamespace"),
439 Value::from("Message"),
440 ])
441 .expect_err("should error"),
442 );
443 assert_eq!(err.identifier(), Some("RunMat:missingNamespace"));
444 assert_eq!(err.message(), "Message");
445 }
446
447 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
448 #[test]
449 fn format_string_with_colon_not_treated_as_identifier() {
450 let err = unwrap_error(
451 error_builtin(vec![
452 Value::from("Value: %d."),
453 Value::Int(IntValue::I32(7)),
454 ])
455 .expect_err("should error"),
456 );
457 assert_eq!(err.identifier(), Some(error_default_identifier()));
458 assert_eq!(err.message(), "Value: 7.");
459 }
460
461 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
462 #[test]
463 fn error_accepts_mexception() {
464 let mex = MException::new("RunMat:demo:test".to_string(), "broken".to_string());
465 let err =
466 unwrap_error(error_builtin(vec![Value::MException(mex)]).expect_err("should error"));
467 assert_eq!(err.identifier(), Some("RunMat:demo:test"));
468 assert_eq!(err.message(), "broken");
469 }
470
471 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
472 #[test]
473 fn error_rejects_extra_args_after_mexception() {
474 let mex = MException::new("RunMat:demo:test".to_string(), "broken".to_string());
475 let err = unwrap_error(
476 error_builtin(vec![Value::MException(mex), Value::from(1.0)])
477 .expect_err("should error"),
478 );
479 assert_eq!(err.identifier(), Some(error_default_identifier()));
480 assert!(err.message().contains("additional arguments"));
481 }
482
483 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
484 #[test]
485 fn error_accepts_message_struct() {
486 let mut st = StructValue::new();
487 st.fields
488 .insert("identifier".to_string(), Value::from("pkg:demo:failure"));
489 st.fields
490 .insert("message".to_string(), Value::from("Struct message."));
491 let err = unwrap_error(error_builtin(vec![Value::Struct(st)]).expect_err("should error"));
492 assert_eq!(err.identifier(), Some("pkg:demo:failure"));
493 assert_eq!(err.message(), "Struct message.");
494 }
495
496 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
497 #[test]
498 fn error_struct_requires_message_field() {
499 let mut st = StructValue::new();
500 st.fields
501 .insert("identifier".to_string(), Value::from("pkg:demo:oops"));
502 let err = unwrap_error(error_builtin(vec![Value::Struct(st)]).expect_err("should error"));
503 assert_eq!(err.identifier(), Some(error_default_identifier()));
504 assert!(err
505 .message()
506 .contains("message struct must contain a 'message' field"));
507 }
508
509 #[test]
510 fn error_type_is_unknown() {
511 assert_eq!(
512 error_type(&[Type::String], &ResolveContext::new(Vec::new())),
513 Type::Unknown
514 );
515 }
516}