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};
13use crate::builtins::diagnostics::type_resolvers::error_type;
14use crate::{build_runtime_error, RuntimeError};
15
16const DEFAULT_IDENTIFIER: &str = "RunMat:error";
17
18#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::diagnostics::error")]
19pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
20 name: "error",
21 op_kind: GpuOpKind::Custom("control"),
22 supported_precisions: &[],
23 broadcast: BroadcastSemantics::None,
24 provider_hooks: &[],
25 constant_strategy: ConstantStrategy::InlineLiteral,
26 residency: ResidencyPolicy::GatherImmediately,
27 nan_mode: ReductionNaN::Include,
28 two_pass_threshold: None,
29 workgroup_size: None,
30 accepts_nan_mode: false,
31 notes: "Control-flow builtin; never dispatched to GPU backends.",
32};
33
34#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::diagnostics::error")]
35pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
36 name: "error",
37 shape: ShapeRequirements::Any,
38 constant_strategy: ConstantStrategy::InlineLiteral,
39 elementwise: None,
40 reduction: None,
41 emits_nan: false,
42 notes: "Control-flow builtin; excluded from fusion planning.",
43};
44
45fn error_flow(identifier: &str, message: impl Into<String>) -> RuntimeError {
46 build_runtime_error(message)
47 .with_builtin("error")
48 .with_identifier(normalize_identifier(identifier))
49 .build()
50}
51
52fn remap_error_flow(err: RuntimeError, identifier: &str) -> RuntimeError {
53 build_runtime_error(err.message().to_string())
54 .with_builtin("error")
55 .with_identifier(normalize_identifier(identifier))
56 .with_source(err)
57 .build()
58}
59
60#[runtime_builtin(
61 name = "error",
62 category = "diagnostics",
63 summary = "Throw an exception with an identifier and a formatted diagnostic message.",
64 keywords = "error,exception,diagnostics,throw",
65 accel = "metadata",
66 type_resolver(error_type),
67 builtin_path = "crate::builtins::diagnostics::error"
68)]
69fn error_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
70 if args.is_empty() {
71 return Err(error_flow(
72 DEFAULT_IDENTIFIER,
73 "error: missing message argument",
74 ));
75 }
76
77 let mut iter = args.into_iter();
78 let first = iter.next().expect("checked above");
79 let rest: Vec<Value> = iter.collect();
80
81 match first {
82 Value::MException(mex) => {
83 if !rest.is_empty() {
84 return Err(error_flow(
85 DEFAULT_IDENTIFIER,
86 "error: additional arguments are not allowed when passing an MException",
87 ));
88 }
89 Err(error_flow(&mex.identifier, &mex.message))
90 }
91 Value::Struct(ref st) => {
92 if !rest.is_empty() {
93 return Err(error_flow(
94 DEFAULT_IDENTIFIER,
95 "error: additional arguments are not allowed when passing a message struct",
96 ));
97 }
98 let (identifier, message) = extract_struct_error_fields(st)?;
99 Err(error_flow(&identifier, &message))
100 }
101 other => handle_message_arguments(other, rest),
102 }
103}
104
105fn handle_message_arguments(first: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
106 let first_string = value_to_string("error", &first)?;
107
108 if rest.is_empty() {
109 return Err(error_flow(DEFAULT_IDENTIFIER, first_string));
110 }
111
112 let mut identifier = DEFAULT_IDENTIFIER.to_string();
113 let mut format_string = first_string;
114 let mut format_args: &[Value] = &rest;
115
116 if !rest.is_empty()
117 && (is_message_identifier(&format_string)
118 || looks_like_unqualified_identifier(&format_string))
119 {
120 identifier = normalize_identifier(&format_string);
121 let (message_value, extra_args) = rest.split_first().expect("rest not empty");
122 format_string = value_to_string("error", message_value)?;
123 format_args = extra_args;
124 }
125
126 let message = if format_args.is_empty() {
127 format_string
128 } else {
129 format_variadic(&format_string, format_args)
130 .map_err(|flow| remap_error_flow(flow, DEFAULT_IDENTIFIER))?
131 };
132
133 Err(error_flow(&identifier, message))
134}
135
136fn extract_struct_error_fields(
137 struct_value: &StructValue,
138) -> crate::BuiltinResult<(String, String)> {
139 let identifier_value = struct_value
140 .fields
141 .get("identifier")
142 .or_else(|| struct_value.fields.get("messageid"))
143 .ok_or_else(|| {
144 error_flow(
145 DEFAULT_IDENTIFIER,
146 "error: message struct must contain an 'identifier' field",
147 )
148 })?;
149 let message_value = struct_value
150 .fields
151 .get("message")
152 .or_else(|| struct_value.fields.get("msg"))
153 .ok_or_else(|| {
154 error_flow(
155 DEFAULT_IDENTIFIER,
156 "error: message struct must contain a 'message' field",
157 )
158 })?;
159
160 let identifier = value_to_string("error", identifier_value)?;
161 let message = value_to_string("error", message_value)?;
162 Ok((identifier, message))
163}
164
165fn value_to_string(context: &str, value: &Value) -> crate::BuiltinResult<String> {
166 String::try_from(value).map_err(|e| error_flow(DEFAULT_IDENTIFIER, format!("{context}: {e}")))
167}
168
169fn normalize_identifier(raw: &str) -> String {
170 let trimmed = raw.trim();
171 if trimmed.is_empty() {
172 DEFAULT_IDENTIFIER.to_string()
173 } else if trimmed.contains(':') {
174 trimmed.to_string()
175 } else {
176 format!("RunMat:{trimmed}")
177 }
178}
179
180fn is_message_identifier(text: &str) -> bool {
181 let trimmed = text.trim();
182 if trimmed.is_empty() || !trimmed.contains(':') {
183 return false;
184 }
185 trimmed
186 .chars()
187 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ':' | '_' | '.'))
188}
189
190fn looks_like_unqualified_identifier(text: &str) -> bool {
191 let trimmed = text.trim();
192 if trimmed.is_empty() || trimmed.contains(char::is_whitespace) {
193 return false;
194 }
195 trimmed
196 .chars()
197 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.'))
198}
199
200#[cfg(test)]
201pub(crate) mod tests {
202 use super::*;
203 use runmat_builtins::{IntValue, MException, ResolveContext, Type};
204
205 fn unwrap_error(err: crate::RuntimeError) -> crate::RuntimeError {
206 err
207 }
208
209 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
210 #[test]
211 fn error_requires_message() {
212 let err = unwrap_error(error_builtin(Vec::new()).expect_err("should error"));
213 assert_eq!(err.identifier(), Some(DEFAULT_IDENTIFIER));
214 assert!(err.message().contains("missing message"));
215 }
216
217 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
218 #[test]
219 fn default_identifier_is_applied() {
220 let err =
221 unwrap_error(error_builtin(vec![Value::from("Failure!")]).expect_err("should error"));
222 assert_eq!(err.identifier(), Some(DEFAULT_IDENTIFIER));
223 assert_eq!(err.message(), "Failure!");
224 }
225
226 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
227 #[test]
228 fn custom_identifier_is_preserved() {
229 let err = unwrap_error(
230 error_builtin(vec![
231 Value::from("runmat:tests:badValue"),
232 Value::from("Value %d is not allowed."),
233 Value::from(5.0),
234 ])
235 .expect_err("should error"),
236 );
237 assert_eq!(err.identifier(), Some("runmat:tests:badValue"));
238 assert_eq!(err.message(), "Value 5 is not allowed.");
239 }
240
241 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
242 #[test]
243 fn identifier_is_normalised_when_namespace_missing() {
244 let err = unwrap_error(
245 error_builtin(vec![
246 Value::from("missingNamespace"),
247 Value::from("Message"),
248 ])
249 .expect_err("should error"),
250 );
251 assert_eq!(err.identifier(), Some("RunMat:missingNamespace"));
252 assert_eq!(err.message(), "Message");
253 }
254
255 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
256 #[test]
257 fn format_string_with_colon_not_treated_as_identifier() {
258 let err = unwrap_error(
259 error_builtin(vec![
260 Value::from("Value: %d."),
261 Value::Int(IntValue::I32(7)),
262 ])
263 .expect_err("should error"),
264 );
265 assert_eq!(err.identifier(), Some(DEFAULT_IDENTIFIER));
266 assert_eq!(err.message(), "Value: 7.");
267 }
268
269 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
270 #[test]
271 fn error_accepts_mexception() {
272 let mex = MException::new("RunMat:demo:test".to_string(), "broken".to_string());
273 let err =
274 unwrap_error(error_builtin(vec![Value::MException(mex)]).expect_err("should error"));
275 assert_eq!(err.identifier(), Some("RunMat:demo:test"));
276 assert_eq!(err.message(), "broken");
277 }
278
279 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
280 #[test]
281 fn error_rejects_extra_args_after_mexception() {
282 let mex = MException::new("RunMat:demo:test".to_string(), "broken".to_string());
283 let err = unwrap_error(
284 error_builtin(vec![Value::MException(mex), Value::from(1.0)])
285 .expect_err("should error"),
286 );
287 assert_eq!(err.identifier(), Some(DEFAULT_IDENTIFIER));
288 assert!(err.message().contains("additional arguments"));
289 }
290
291 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
292 #[test]
293 fn error_accepts_message_struct() {
294 let mut st = StructValue::new();
295 st.fields
296 .insert("identifier".to_string(), Value::from("pkg:demo:failure"));
297 st.fields
298 .insert("message".to_string(), Value::from("Struct message."));
299 let err = unwrap_error(error_builtin(vec![Value::Struct(st)]).expect_err("should error"));
300 assert_eq!(err.identifier(), Some("pkg:demo:failure"));
301 assert_eq!(err.message(), "Struct message.");
302 }
303
304 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
305 #[test]
306 fn error_struct_requires_message_field() {
307 let mut st = StructValue::new();
308 st.fields
309 .insert("identifier".to_string(), Value::from("pkg:demo:oops"));
310 let err = unwrap_error(error_builtin(vec![Value::Struct(st)]).expect_err("should error"));
311 assert_eq!(err.identifier(), Some(DEFAULT_IDENTIFIER));
312 assert!(err
313 .message()
314 .contains("message struct must contain a 'message' field"));
315 }
316
317 #[test]
318 fn error_type_is_unknown() {
319 assert_eq!(
320 error_type(&[Type::String], &ResolveContext::new(Vec::new())),
321 Type::Unknown
322 );
323 }
324}