1use once_cell::sync::Lazy;
4use runmat_builtins::{CellArray, StructValue, Value};
5use runmat_macros::runtime_builtin;
6use std::collections::{HashMap, HashSet};
7use std::convert::TryFrom;
8use std::sync::Mutex;
9
10use crate::builtins::common::format::format_variadic;
11use crate::builtins::common::spec::{
12 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
13 ReductionNaN, ResidencyPolicy, ShapeRequirements,
14};
15#[cfg(feature = "doc_export")]
16use crate::register_builtin_doc_text;
17use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
18
19const DEFAULT_IDENTIFIER: &str = "MATLAB:warning";
20
21#[cfg(feature = "doc_export")]
22pub const DOC_MD: &str = r#"---
23title: "warning"
24category: "diagnostics"
25keywords: ["warning", "diagnostics", "state", "query", "backtrace"]
26summary: "Display formatted warnings, control warning state, and query per-identifier settings."
27references: []
28gpu_support:
29 elementwise: false
30 reduction: false
31 precisions: []
32 broadcasting: "none"
33 notes: "Runs entirely on the host. GPU tensors appearing in formatted arguments are gathered before formatting."
34fusion:
35 elementwise: false
36 reduction: false
37 max_inputs: 0
38 constants: "inline"
39requires_feature: null
40tested:
41 unit: "builtins::diagnostics::warning::tests"
42 integration: null
43---
44
45# What does the `warning` function do in MATLAB / RunMat?
46`warning` emits diagnostic messages without aborting execution. It also provides a central
47interface for enabling, disabling, promoting, or querying warnings by identifier.
48
49RunMat mirrors MATLAB semantics with per-identifier modes (`'on'`, `'off'`, `'once'`, `'error'`),
50backtrace control, structured state restoration, and formatted message support. When an identifier
51is promoted to `'error'`, future uses of that warning raise an error instead of printing.
52
53## How does the `warning` function behave in MATLAB / RunMat?
54- `warning(message)` prints the message using the default identifier `MATLAB:warning`.
55- `warning(id, fmt, args...)` associates the warning with the identifier `id`, formats the message
56 using MATLAB's `sprintf` rules, and honours per-identifier state.
57- `warning(MException_obj)` reissues an existing exception as a warning.
58- `warning(struct)` restores warning state previously captured with `warning('query', ...)`.
59- `warning(state, id)` changes the mode for `id` (`'on'`, `'off'`, `'once'`, `'error'`, and the aliases `'all'` or `'last'`).
60 The call returns a struct describing the prior state so you can restore it later.
61- `warning(state, mode)` controls diagnostic verbosity modes such as `'backtrace'` and `'verbose'`, returning
62 a struct snapshot of the previous setting.
63- `warning('default', id)` resets `id` (or `'all'`, `'backtrace'`, `'verbose'`, `'last'`) to the factory default,
64 returning the state that was active before the reset. `warning('reset')` restores every warning, mode, and
65 once-tracking flag in one call.
66- `warning('query', id)` returns a struct describing the current mode for `id`. Passing `'all'`
67 returns a cell vector of structs covering every configured identifier plus the global default and diagnostic modes.
68- `warning('status')` prints the global warning table as a formatted summary.
69- All state-changing forms return struct snapshots; pass them back to `warning` later to reinstate the captured state.
70
71## GPU execution and residency
72`warning` is a control-flow builtin that runs on the host. When formatted arguments include GPU
73resident arrays (for example via `%g` specifiers), RunMat gathers the values to host memory before
74formatting the message so the diagnostic text matches MATLAB expectations.
75
76## Examples of using the `warning` function in MATLAB / RunMat
77
78### Displaying a simple warning
79```matlab
80warning("Computation took longer than expected.");
81```
82
83### Emitting a warning with a custom identifier
84```matlab
85warning("runmat:io:deprecatedOption", ...
86 "Option '%s' is deprecated and will be removed in %s.", ...
87 "VerboseMode", "1.2");
88```
89
90### Turning a specific warning off and back on
91```matlab
92warning("off", "runmat:io:deprecatedOption");
93% ... code that triggers the warning ...
94warning("on", "runmat:io:deprecatedOption");
95```
96
97### Promoting a warning to an error
98```matlab
99warning("error", "runmat:solver:illConditioned");
100% The next call raises an error instead of printing a warning.
101warning("runmat:solver:illConditioned", ...
102 "Condition number exceeds threshold %.2e.", 1e12);
103```
104
105### Querying and restoring warning state
106```matlab
107state = warning("query", "all"); % capture the current table
108warning("off", "all"); % silence all warnings temporarily
109warning("Some temporary warning.");
110warning(state); % restore original configuration
111```
112
113### Enabling backtraces for debugging
114```matlab
115warning("on", "backtrace"); % warning("backtrace","on") is equivalent
116warning("runmat:demo:slowPath", "Inspect the call stack above for context.");
117warning("off", "backtrace");
118```
119
120### Displaying verbose suppression guidance
121```matlab
122warning("on", "verbose");
123warning("runmat:demo:slowPath", "Inspect the call stack above for context.");
124warning("default", "verbose"); % restore the default verbosity
125```
126
127## FAQ
1281. **Does RunMat keep track of the last warning?** Yes. The last identifier and message are stored internally and will be exposed through the MATLAB-compatible `lastwarn` builtin.
1292. **What does `'once'` do?** The first occurrence of the warning is shown, and subsequent uses of the same identifier are suppressed until you change the state.
1303. **How do I restore defaults after experimentation?** Call `warning('reset')` to revert the global default, per-identifier table, diagnostic modes, and once-tracking.
1314. **Do state-changing calls return anything useful?** Yes. Forms such as `warning('off','id')`, `warning('on','backtrace')`, and `warning('default')` return struct snapshots describing the state before the change. Store the struct and pass it back to `warning` later to restore the captured configuration.
1325. **What does the `'verbose'` mode do?** When enabled (`warning('on','verbose')`), RunMat prints an additional hint describing how to suppress the warning. Disable it with `warning('default','verbose')`.
1336. **What happens when a warning is promoted to `'error'`?** The warning text is reused to raise an error with the same identifier, just like MATLAB.
1347. **Does `warning` run on the GPU?** No. Control-flow builtins execute on the host. GPU inputs referenced in formatted messages are gathered automatically before the message is emitted.
1358. **Can I provide multiple state structs to `warning(state)`?** Yes. Pass either a struct or a cell array of structs (as returned by `warning('query','all')`); RunMat applies them sequentially.
1369. **Does `warning('on','backtrace')` match MATLAB output exactly?** RunMat prints a Rust backtrace for now. Future releases will map this to MATLAB-style stack frames, but the enable/disable semantics match MATLAB.
13710. **Is whitespace in identifiers allowed?** Identifiers follow MATLAB rules: they must contain at least one colon and no whitespace. RunMat normalises bare names by prefixing `MATLAB:`.
13811. **What if I call `warning` with unsupported arguments?** RunMat issues a MATLAB-compatible usage error so you can correct the call.
13912. **Are repeated calls thread-safe?** Internally the warning table is guarded by a mutex, so concurrent invocations are serialised and remain deterministic.
140
141## See Also
142[error](./error), [sprintf](../strings/core/sprintf), [fprintf](../io/filetext/fprintf)
143
144## Source & Feedback
145- The full source code for the implementation of the `warning` function is available at: [`crates/runmat-runtime/src/builtins/diagnostics/warning.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/diagnostics/warning.rs)
146- Found a bug or behavioural difference? Please [open an issue](https://github.com/runmat-org/runmat/issues/new/choose) with details and a minimal repro.
147"#;
148
149pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
150 name: "warning",
151 op_kind: GpuOpKind::Custom("control"),
152 supported_precisions: &[],
153 broadcast: BroadcastSemantics::None,
154 provider_hooks: &[],
155 constant_strategy: ConstantStrategy::InlineLiteral,
156 residency: ResidencyPolicy::GatherImmediately,
157 nan_mode: ReductionNaN::Include,
158 two_pass_threshold: None,
159 workgroup_size: None,
160 accepts_nan_mode: false,
161 notes: "Control-flow builtin; GPU backends are never invoked.",
162};
163
164register_builtin_gpu_spec!(GPU_SPEC);
165
166pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
167 name: "warning",
168 shape: ShapeRequirements::Any,
169 constant_strategy: ConstantStrategy::InlineLiteral,
170 elementwise: None,
171 reduction: None,
172 emits_nan: false,
173 notes: "Control-flow builtin; excluded from fusion planning.",
174};
175
176register_builtin_fusion_spec!(FUSION_SPEC);
177
178#[cfg(feature = "doc_export")]
179register_builtin_doc_text!("warning", DOC_MD);
180
181static MANAGER: Lazy<Mutex<WarningManager>> = Lazy::new(|| Mutex::new(WarningManager::default()));
182
183fn manager() -> &'static Mutex<WarningManager> {
184 &MANAGER
185}
186
187fn with_manager<F, R>(func: F) -> R
188where
189 F: FnOnce(&mut WarningManager) -> R,
190{
191 let mut guard = manager().lock().expect("warning manager mutex poisoned");
192 func(&mut guard)
193}
194
195#[runtime_builtin(
196 name = "warning",
197 category = "diagnostics",
198 summary = "Display formatted warnings, control warning state, and query per-identifier settings.",
199 keywords = "warning,diagnostics,state,query,backtrace",
200 accel = "metadata",
201 sink = true
202)]
203fn warning_builtin(args: Vec<Value>) -> Result<Value, String> {
204 if args.is_empty() {
205 return handle_query_default();
206 }
207
208 let first = &args[0];
209 let rest = &args[1..];
210
211 match first {
212 Value::Struct(_) | Value::Cell(_) => {
213 if !rest.is_empty() {
214 return Err("warning: state restoration accepts a single argument".to_string());
215 }
216 apply_state_value(first)?;
217 Ok(Value::Num(0.0))
218 }
219 Value::MException(mex) => {
220 if !rest.is_empty() {
221 return Err(
222 "warning: additional arguments are not allowed when passing an MException"
223 .to_string(),
224 );
225 }
226 reissue_exception(mex)
227 }
228 _ => {
229 let first_string = value_to_string("warning", first)?;
230 if let Some(command) = parse_command(&first_string) {
231 return handle_command(command, rest);
232 }
233 handle_message_call(None, first_string, rest)
234 }
235 }
236}
237
238fn handle_message_call(
239 explicit_identifier: Option<String>,
240 first_string: String,
241 rest: &[Value],
242) -> Result<Value, String> {
243 if let Some(identifier) = explicit_identifier {
244 return emit_warning(&identifier, &first_string, rest);
245 }
246
247 if rest.is_empty() {
248 emit_warning(DEFAULT_IDENTIFIER, &first_string, rest)
249 } else if is_message_identifier(&first_string) {
250 let fmt = value_to_string("warning", &rest[0])?;
251 let args = &rest[1..];
252 emit_warning(&first_string, &fmt, args)
253 } else {
254 emit_warning(DEFAULT_IDENTIFIER, &first_string, rest)
255 }
256}
257
258fn emit_warning(identifier_raw: &str, fmt: &str, args: &[Value]) -> Result<Value, String> {
259 let identifier = normalize_identifier(identifier_raw);
260 let message = format_variadic(fmt, args)?;
261
262 let action = with_manager(|mgr| {
263 let action = mgr.action_for(&identifier);
264 if matches!(action, WarningAction::Display | WarningAction::AsError) {
265 mgr.record_last(&identifier, &message);
266 }
267 action
268 });
269
270 match action {
271 WarningAction::Suppress => Ok(Value::Num(0.0)),
272 WarningAction::Display => {
273 print_warning(&identifier, &message);
274 Ok(Value::Num(0.0))
275 }
276 WarningAction::AsError => Err(build_error(&identifier, &message)),
277 }
278}
279
280fn print_warning(identifier: &str, message: &str) {
281 let (backtrace_enabled, verbose_enabled) =
282 with_manager(|mgr| (mgr.backtrace_enabled, mgr.verbose_enabled));
283
284 eprintln!("Warning: {message}");
285 if identifier != DEFAULT_IDENTIFIER {
286 eprintln!("identifier: {identifier}");
287 }
288
289 if verbose_enabled {
290 let suppression = if identifier == DEFAULT_IDENTIFIER {
291 DEFAULT_IDENTIFIER.to_string()
292 } else {
293 identifier.to_string()
294 };
295 eprintln!("(Type \"warning('off','{suppression}')\" to suppress this warning.)");
296 }
297
298 if backtrace_enabled {
299 let bt = std::backtrace::Backtrace::force_capture();
300 eprintln!("{bt}");
301 }
302}
303
304fn reissue_exception(mex: &runmat_builtins::MException) -> Result<Value, String> {
305 let identifier = normalize_identifier(&mex.identifier);
306 emit_warning(&identifier, &mex.message, &[])
307}
308
309fn handle_command(command: Command, rest: &[Value]) -> Result<Value, String> {
310 match command {
311 Command::SetMode(mode) => set_mode_command(mode, rest),
312 Command::Default => default_command(rest),
313 Command::Reset => {
314 if !rest.is_empty() {
315 return Err("warning: 'reset' does not accept additional arguments".to_string());
316 }
317 with_manager(WarningManager::reset);
318 Ok(Value::Num(0.0))
319 }
320 Command::Query => query_command(rest),
321 Command::Status => status_command(rest),
322 Command::Backtrace => backtrace_command(rest),
323 }
324}
325
326fn set_mode_command(mode: WarningMode, rest: &[Value]) -> Result<Value, String> {
327 let identifier = if rest.is_empty() {
328 "all".to_string()
329 } else if rest.len() == 1 {
330 value_to_string("warning", &rest[0])?
331 } else {
332 return Err("warning: too many input arguments for state change".to_string());
333 };
334
335 let trimmed = identifier.trim();
336 if trimmed.eq_ignore_ascii_case("all") {
337 return with_manager(|mgr| {
338 let previous = mgr.default_mode;
339 let value = Value::Struct(mgr.state_struct_for("all", WarningRule::new(previous)));
340 mgr.set_global_mode(mode);
341 Ok(value)
342 });
343 }
344
345 if trimmed.eq_ignore_ascii_case("last") {
346 let last_identifier = with_manager(|mgr| mgr.last_warning.clone());
347 let Some((identifier, _)) = last_identifier else {
348 return Err("warning: there is no last warning identifier to target".to_string());
349 };
350 return set_mode_for_identifier(mode, &identifier);
351 }
352
353 if trimmed.eq_ignore_ascii_case("backtrace") || trimmed.eq_ignore_ascii_case("verbose") {
354 return set_mode_for_special_mode(mode, trimmed);
355 }
356
357 let normalized = normalize_identifier(trimmed);
358 set_mode_for_identifier(mode, &normalized)
359}
360
361fn set_mode_for_identifier(mode: WarningMode, identifier: &str) -> Result<Value, String> {
362 with_manager(|mgr| {
363 let previous = mgr.lookup_mode(identifier);
364 let value = Value::Struct(mgr.state_struct_for(identifier, previous));
365 mgr.set_identifier_mode(identifier, mode);
366 Ok(value)
367 })
368}
369
370fn reset_identifier_to_default(identifier: &str) -> Result<Value, String> {
371 with_manager(|mgr| {
372 let previous = mgr.lookup_mode(identifier);
373 let value = Value::Struct(mgr.state_struct_for(identifier, previous));
374 mgr.clear_identifier(identifier);
375 Ok(value)
376 })
377}
378
379fn set_mode_for_special_mode(mode: WarningMode, mode_name: &str) -> Result<Value, String> {
380 let mode_lower = mode_name.trim().to_ascii_lowercase();
381 if !matches!(mode, WarningMode::On | WarningMode::Off) {
382 return Err(format!(
383 "warning: only 'on' or 'off' are valid states for '{mode_lower}'"
384 ));
385 }
386
387 with_manager(|mgr| {
388 let previous_enabled = if mode_lower == "backtrace" {
389 let prev = mgr.backtrace_enabled;
390 mgr.backtrace_enabled = matches!(mode, WarningMode::On);
391 prev
392 } else if mode_lower == "verbose" {
393 let prev = mgr.verbose_enabled;
394 mgr.verbose_enabled = matches!(mode, WarningMode::On);
395 prev
396 } else {
397 return Err(format!(
398 "warning: unknown mode '{}'; expected 'backtrace' or 'verbose'",
399 mode_name
400 ));
401 };
402
403 let previous_state = if previous_enabled { "on" } else { "off" };
404 let value = state_struct_value(&mode_lower, previous_state);
405 Ok(value)
406 })
407}
408
409fn default_command(rest: &[Value]) -> Result<Value, String> {
410 match rest.len() {
411 0 => {
412 let snapshot = with_manager(|mgr| {
413 let snapshot = mgr.snapshot();
414 mgr.reset_defaults_only();
415 snapshot
416 });
417 structs_to_cell(snapshot)
418 }
419 1 => {
420 let identifier = value_to_string("warning", &rest[0])?;
421 let trimmed = identifier.trim();
422 if trimmed.eq_ignore_ascii_case("all") {
423 let snapshot = with_manager(|mgr| {
424 let snapshot = mgr.snapshot();
425 mgr.reset_defaults_only();
426 snapshot
427 });
428 return structs_to_cell(snapshot);
429 }
430 if trimmed.eq_ignore_ascii_case("backtrace") {
431 return with_manager(|mgr| {
432 let previous = if mgr.backtrace_enabled { "on" } else { "off" };
433 mgr.backtrace_enabled = false;
434 Ok(state_struct_value("backtrace", previous))
435 });
436 }
437 if trimmed.eq_ignore_ascii_case("verbose") {
438 return with_manager(|mgr| {
439 let previous = if mgr.verbose_enabled { "on" } else { "off" };
440 mgr.verbose_enabled = false;
441 Ok(state_struct_value("verbose", previous))
442 });
443 }
444 if trimmed.eq_ignore_ascii_case("last") {
445 let last_identifier = with_manager(|mgr| mgr.last_warning.clone());
446 let Some((identifier, _)) = last_identifier else {
447 return Err(
448 "warning: there is no last warning identifier to reset to default"
449 .to_string(),
450 );
451 };
452 return reset_identifier_to_default(&identifier);
453 }
454 let normalized = normalize_identifier(trimmed);
455 reset_identifier_to_default(&normalized)
456 }
457 _ => Err("warning: 'default' accepts zero or one identifier argument".to_string()),
458 }
459}
460
461fn query_command(rest: &[Value]) -> Result<Value, String> {
462 if rest.len() > 1 {
463 return Err("warning: 'query' accepts at most one identifier argument".to_string());
464 }
465
466 let target = if rest.is_empty() {
467 "all".to_string()
468 } else {
469 value_to_string("warning", &rest[0])?
470 };
471
472 with_manager(|mgr| {
473 if target.trim().eq_ignore_ascii_case("all") {
474 let snapshot = mgr.snapshot();
475 let rows = snapshot.len();
476 let entries: Vec<Value> = snapshot.into_iter().map(Value::Struct).collect();
477 let cell = CellArray::new(entries, rows, 1)
478 .map_err(|e| format!("warning: failed to assemble query cell: {e}"))?;
479 Ok(Value::Cell(cell))
480 } else if target.trim().eq_ignore_ascii_case("last") {
481 if let Some((identifier, message)) = mgr.last_warning.clone() {
482 let mut st = StructValue::new();
483 st.fields
484 .insert("identifier".to_string(), Value::from(identifier));
485 st.fields
486 .insert("message".to_string(), Value::from(message));
487 st.fields.insert("state".to_string(), Value::from("last"));
488 Ok(Value::Struct(st))
489 } else {
490 let mut st = StructValue::new();
491 st.fields.insert("identifier".to_string(), Value::from(""));
492 st.fields.insert("message".to_string(), Value::from(""));
493 st.fields.insert("state".to_string(), Value::from("none"));
494 Ok(Value::Struct(st))
495 }
496 } else if target.trim().eq_ignore_ascii_case("backtrace") {
497 Ok(state_struct_value(
498 "backtrace",
499 if mgr.backtrace_enabled { "on" } else { "off" },
500 ))
501 } else if target.trim().eq_ignore_ascii_case("verbose") {
502 Ok(state_struct_value(
503 "verbose",
504 if mgr.verbose_enabled { "on" } else { "off" },
505 ))
506 } else {
507 let normalized = normalize_identifier(&target);
508 let state = mgr.lookup_mode(&normalized);
509 Ok(Value::Struct(mgr.state_struct_for(&normalized, state)))
510 }
511 })
512}
513
514fn status_command(rest: &[Value]) -> Result<Value, String> {
515 if !rest.is_empty() {
516 return Err("warning: 'status' does not accept additional arguments".to_string());
517 }
518 let value = query_command(&[])?;
519 match &value {
520 Value::Cell(cell) => {
521 eprintln!("Warning status:");
522 for idx in 0..cell.data.len() {
523 let entry = (*cell.data[idx]).clone();
524 if let Value::Struct(st) = entry {
525 let identifier = st
526 .fields
527 .get("identifier")
528 .and_then(|v| value_to_string("warning", v).ok())
529 .unwrap_or_default();
530 let state = st
531 .fields
532 .get("state")
533 .and_then(|v| value_to_string("warning", v).ok())
534 .unwrap_or_default();
535 eprintln!(" {identifier}: {state}");
536 }
537 }
538 }
539 Value::Struct(st) => {
540 let identifier = st
541 .fields
542 .get("identifier")
543 .and_then(|v| value_to_string("warning", v).ok())
544 .unwrap_or_default();
545 let state = st
546 .fields
547 .get("state")
548 .and_then(|v| value_to_string("warning", v).ok())
549 .unwrap_or_default();
550 eprintln!("Warning status -> {identifier}: {state}");
551 }
552 _ => {}
553 }
554 Ok(value)
555}
556
557fn backtrace_command(rest: &[Value]) -> Result<Value, String> {
558 match rest.len() {
559 0 => {
560 let state = with_manager(|mgr| if mgr.backtrace_enabled { "on" } else { "off" });
561 Ok(Value::from(state))
562 }
563 1 => {
564 let setting = value_to_string("warning", &rest[0])?;
565 match setting.trim().to_ascii_lowercase().as_str() {
566 "on" => with_manager(|mgr| mgr.backtrace_enabled = true),
567 "off" => with_manager(|mgr| mgr.backtrace_enabled = false),
568 other => {
569 return Err(format!(
570 "warning: backtrace mode must be 'on' or 'off', got '{other}'"
571 ))
572 }
573 }
574 Ok(Value::Num(0.0))
575 }
576 _ => Err("warning: 'backtrace' accepts zero or one argument".to_string()),
577 }
578}
579
580fn handle_query_default() -> Result<Value, String> {
581 query_command(&[])
582}
583
584fn apply_state_value(value: &Value) -> Result<(), String> {
585 match value {
586 Value::Struct(st) => apply_state_struct(st),
587 Value::Cell(cell) => {
588 for idx in 0..cell.data.len() {
589 let entry = (*cell.data[idx]).clone();
590 apply_state_value(&entry)?;
591 }
592 Ok(())
593 }
594 other => Err(format!(
595 "warning: expected a struct or cell array of structs, got {other:?}"
596 )),
597 }
598}
599
600fn apply_state_struct(st: &StructValue) -> Result<(), String> {
601 let identifier_value = st
602 .fields
603 .get("identifier")
604 .ok_or_else(|| "warning: state struct must contain an 'identifier' field".to_string())?;
605 let state_value = st
606 .fields
607 .get("state")
608 .ok_or_else(|| "warning: state struct must contain a 'state' field".to_string())?;
609 let identifier_raw = value_to_string("warning", identifier_value)?;
610 let state_raw = value_to_string("warning", state_value)?;
611 let identifier_trimmed = identifier_raw.trim();
612 if identifier_trimmed.eq_ignore_ascii_case("all") {
613 if let Some(mode) = parse_mode_keyword(&state_raw) {
614 with_manager(|mgr| mgr.set_global_mode(mode));
615 } else {
616 return Err(format!("warning: unknown state '{}'", state_raw));
617 }
618 } else if identifier_trimmed.eq_ignore_ascii_case("backtrace") {
619 let state = state_raw.trim().to_ascii_lowercase();
620 match state.as_str() {
621 "on" => with_manager(|mgr| mgr.backtrace_enabled = true),
622 "off" | "default" => with_manager(|mgr| mgr.backtrace_enabled = false),
623 other => return Err(format!("warning: unknown backtrace state '{}'", other)),
624 }
625 } else if identifier_trimmed.eq_ignore_ascii_case("verbose") {
626 let state = state_raw.trim().to_ascii_lowercase();
627 match state.as_str() {
628 "on" => with_manager(|mgr| mgr.verbose_enabled = true),
629 "off" | "default" => with_manager(|mgr| mgr.verbose_enabled = false),
630 other => return Err(format!("warning: unknown verbose state '{}'", other)),
631 }
632 } else if identifier_trimmed.eq_ignore_ascii_case("last") {
633 let last_identifier = with_manager(|mgr| mgr.last_warning.clone());
634 let Some((identifier, _)) = last_identifier else {
635 return Err("warning: there is no last warning identifier to apply state".to_string());
636 };
637 if state_raw.trim().eq_ignore_ascii_case("default") {
638 with_manager(|mgr| mgr.clear_identifier(&identifier));
639 } else if let Some(mode) = parse_mode_keyword(&state_raw) {
640 with_manager(|mgr| mgr.set_identifier_mode(&identifier, mode));
641 } else {
642 return Err(format!("warning: unknown state '{}'", state_raw));
643 }
644 } else if state_raw.trim().eq_ignore_ascii_case("default") {
645 let normalized = normalize_identifier(identifier_trimmed);
646 with_manager(|mgr| mgr.clear_identifier(&normalized));
647 } else if let Some(mode) = parse_mode_keyword(&state_raw) {
648 let normalized = normalize_identifier(identifier_trimmed);
649 with_manager(|mgr| mgr.set_identifier_mode(&normalized, mode));
650 } else {
651 return Err(format!("warning: unknown state '{}'", state_raw));
652 }
653 Ok(())
654}
655
656#[derive(Clone, Copy, Debug, PartialEq, Eq)]
657enum WarningMode {
658 On,
659 Off,
660 Once,
661 Error,
662}
663
664impl WarningMode {
665 fn keyword(self) -> &'static str {
666 match self {
667 WarningMode::On => "on",
668 WarningMode::Off => "off",
669 WarningMode::Once => "once",
670 WarningMode::Error => "error",
671 }
672 }
673}
674
675#[derive(Clone, Copy)]
676enum Command {
677 SetMode(WarningMode),
678 Default,
679 Reset,
680 Query,
681 Status,
682 Backtrace,
683}
684
685fn parse_command(text: &str) -> Option<Command> {
686 match text.trim().to_ascii_lowercase().as_str() {
687 "on" => Some(Command::SetMode(WarningMode::On)),
688 "off" => Some(Command::SetMode(WarningMode::Off)),
689 "once" => Some(Command::SetMode(WarningMode::Once)),
690 "error" => Some(Command::SetMode(WarningMode::Error)),
691 "default" => Some(Command::Default),
692 "reset" => Some(Command::Reset),
693 "query" => Some(Command::Query),
694 "status" => Some(Command::Status),
695 "backtrace" => Some(Command::Backtrace),
696 _ => None,
697 }
698}
699
700fn parse_mode_keyword(text: &str) -> Option<WarningMode> {
701 match text.trim().to_ascii_lowercase().as_str() {
702 "on" => Some(WarningMode::On),
703 "off" => Some(WarningMode::Off),
704 "once" => Some(WarningMode::Once),
705 "error" => Some(WarningMode::Error),
706 _ => None,
707 }
708}
709
710#[derive(Clone, Copy)]
711struct WarningRule {
712 mode: WarningMode,
713 triggered: bool,
714}
715
716impl WarningRule {
717 fn new(mode: WarningMode) -> Self {
718 Self {
719 mode,
720 triggered: false,
721 }
722 }
723}
724
725enum WarningAction {
726 Suppress,
727 Display,
728 AsError,
729}
730
731struct WarningManager {
732 default_mode: WarningMode,
733 rules: HashMap<String, WarningRule>,
734 once_seen_default: HashSet<String>,
735 backtrace_enabled: bool,
736 verbose_enabled: bool,
737 last_warning: Option<(String, String)>,
738}
739
740impl Default for WarningManager {
741 fn default() -> Self {
742 Self {
743 default_mode: WarningMode::On,
744 rules: HashMap::new(),
745 once_seen_default: HashSet::new(),
746 backtrace_enabled: false,
747 verbose_enabled: false,
748 last_warning: None,
749 }
750 }
751}
752
753impl WarningManager {
754 fn set_global_mode(&mut self, mode: WarningMode) {
755 self.once_seen_default.clear();
756 self.default_mode = mode;
757 }
758
759 fn set_identifier_mode(&mut self, identifier: &str, mode: WarningMode) {
760 if mode == self.default_mode && !matches!(mode, WarningMode::Once) {
761 self.rules.remove(identifier);
762 } else {
763 self.rules
764 .insert(identifier.to_string(), WarningRule::new(mode));
765 }
766 if matches!(mode, WarningMode::Once) {
767 self.once_seen_default.remove(identifier);
768 }
769 }
770
771 fn clear_identifier(&mut self, identifier: &str) {
772 self.rules.remove(identifier);
773 self.once_seen_default.remove(identifier);
774 }
775
776 fn reset(&mut self) {
777 self.default_mode = WarningMode::On;
778 self.rules.clear();
779 self.once_seen_default.clear();
780 self.backtrace_enabled = false;
781 self.verbose_enabled = false;
782 self.last_warning = None;
783 }
784
785 fn reset_defaults_only(&mut self) {
786 self.default_mode = WarningMode::On;
787 self.once_seen_default.clear();
788 self.rules.clear();
789 self.backtrace_enabled = false;
790 self.verbose_enabled = false;
791 }
792
793 fn action_for(&mut self, identifier: &str) -> WarningAction {
794 if let Some(rule) = self.rules.get_mut(identifier) {
795 return match rule.mode {
796 WarningMode::On => WarningAction::Display,
797 WarningMode::Off => WarningAction::Suppress,
798 WarningMode::Error => WarningAction::AsError,
799 WarningMode::Once => {
800 if rule.triggered {
801 WarningAction::Suppress
802 } else {
803 rule.triggered = true;
804 WarningAction::Display
805 }
806 }
807 };
808 }
809
810 match self.default_mode {
811 WarningMode::On => WarningAction::Display,
812 WarningMode::Off => WarningAction::Suppress,
813 WarningMode::Error => WarningAction::AsError,
814 WarningMode::Once => {
815 if self.once_seen_default.contains(identifier) {
816 WarningAction::Suppress
817 } else {
818 self.once_seen_default.insert(identifier.to_string());
819 WarningAction::Display
820 }
821 }
822 }
823 }
824
825 fn record_last(&mut self, identifier: &str, message: &str) {
826 self.last_warning = Some((identifier.to_string(), message.to_string()));
827 }
828
829 fn default_state_struct(&self) -> StructValue {
830 let mut st = StructValue::new();
831 st.fields
832 .insert("identifier".to_string(), Value::from("all".to_string()));
833 st.fields.insert(
834 "state".to_string(),
835 Value::from(self.default_mode.keyword()),
836 );
837 st
838 }
839
840 fn state_struct_for(&self, identifier: &str, rule: WarningRule) -> StructValue {
841 let mut st = StructValue::new();
842 st.fields.insert(
843 "identifier".to_string(),
844 Value::from(identifier.to_string()),
845 );
846 st.fields
847 .insert("state".to_string(), Value::from(rule.mode.keyword()));
848 st
849 }
850
851 fn lookup_mode(&self, identifier: &str) -> WarningRule {
852 self.rules
853 .get(identifier)
854 .copied()
855 .unwrap_or_else(|| WarningRule::new(self.default_mode))
856 }
857
858 fn snapshot(&self) -> Vec<StructValue> {
859 let mut entries = Vec::new();
860 entries.push(self.default_state_struct());
861 for (id, rule) in self.rules.iter() {
862 entries.push(self.state_struct_for(id, *rule));
863 }
864 entries.push(state_struct(
865 "backtrace",
866 if self.backtrace_enabled { "on" } else { "off" },
867 ));
868 entries.push(state_struct(
869 "verbose",
870 if self.verbose_enabled { "on" } else { "off" },
871 ));
872 entries
873 }
874}
875
876fn value_to_string(context: &str, value: &Value) -> Result<String, String> {
877 match value {
878 Value::String(s) => Ok(s.clone()),
879 Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
880 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
881 Value::CharArray(_) => Err(format!("{context}: expected scalar char array")),
882 Value::StringArray(_) => Err(format!("{context}: expected scalar string")),
883 other => String::try_from(other)
884 .map_err(|_| format!("{context}: expected string-like argument, got {other:?}")),
885 }
886}
887
888fn is_message_identifier(text: &str) -> bool {
889 let trimmed = text.trim();
890 if trimmed.is_empty() || !trimmed.contains(':') {
891 return false;
892 }
893 trimmed
894 .chars()
895 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ':' | '_' | '.'))
896}
897
898fn normalize_identifier(raw: &str) -> String {
899 let trimmed = raw.trim();
900 if trimmed.is_empty() {
901 DEFAULT_IDENTIFIER.to_string()
902 } else if trimmed.contains(':') {
903 trimmed.to_string()
904 } else {
905 format!("MATLAB:{trimmed}")
906 }
907}
908
909fn build_error(identifier: &str, message: &str) -> String {
910 format!("{}: {}", identifier, message)
911}
912
913fn state_struct(identifier: &str, state: &str) -> StructValue {
914 let mut st = StructValue::new();
915 st.fields.insert(
916 "identifier".to_string(),
917 Value::from(identifier.to_string()),
918 );
919 st.fields
920 .insert("state".to_string(), Value::from(state.to_string()));
921 st
922}
923
924fn state_struct_value(identifier: &str, state: &str) -> Value {
925 Value::Struct(state_struct(identifier, state))
926}
927
928fn structs_to_cell(structs: Vec<StructValue>) -> Result<Value, String> {
929 let rows = structs.len();
930 let values: Vec<Value> = structs.into_iter().map(Value::Struct).collect();
931 CellArray::new(values, rows, 1)
932 .map(Value::Cell)
933 .map_err(|e| format!("warning: failed to assemble state cell: {e}"))
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939
940 static TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
941
942 fn reset_manager() {
943 with_manager(WarningManager::reset);
944 }
945
946 fn assert_state_struct(value: &Value, identifier: &str, state: &str) {
947 match value {
948 Value::Struct(st) => {
949 let id = st
950 .fields
951 .get("identifier")
952 .and_then(|v| String::try_from(v).ok())
953 .unwrap_or_default();
954 let st_state = st
955 .fields
956 .get("state")
957 .and_then(|v| String::try_from(v).ok())
958 .unwrap_or_default();
959 assert_eq!(id, identifier);
960 assert_eq!(st_state, state);
961 }
962 other => panic!("expected state struct, got {other:?}"),
963 }
964 }
965
966 fn structs_from_value(value: Value) -> Vec<StructValue> {
967 match value {
968 Value::Cell(cell) => cell
969 .data
970 .iter()
971 .map(|ptr| unsafe { &*ptr.as_raw() }.clone())
972 .map(|value| match value {
973 Value::Struct(st) => st,
974 other => panic!("expected struct entry, got {other:?}"),
975 })
976 .collect(),
977 Value::Struct(st) => vec![st],
978 other => panic!("expected struct array, got {other:?}"),
979 }
980 }
981
982 fn field_str(struct_value: &StructValue, field: &str) -> Option<String> {
983 struct_value
984 .fields
985 .get(field)
986 .and_then(|value| String::try_from(value).ok())
987 }
988
989 #[test]
990 fn emits_basic_warning() {
991 let _guard = TEST_LOCK.lock().unwrap();
992 reset_manager();
993 let result = warning_builtin(vec![Value::from("Hello world!")]).expect("warning ok");
994 assert!(matches!(result, Value::Num(_)));
995 let last = with_manager(|mgr| mgr.last_warning.clone());
996 assert_eq!(
997 last,
998 Some((DEFAULT_IDENTIFIER.to_string(), "Hello world!".to_string()))
999 );
1000 }
1001
1002 #[test]
1003 fn emits_warning_with_identifier_and_format() {
1004 let _guard = TEST_LOCK.lock().unwrap();
1005 reset_manager();
1006 let args = vec![
1007 Value::from("runmat:demo:test"),
1008 Value::from("value is %d"),
1009 Value::Int(runmat_builtins::IntValue::I32(7)),
1010 ];
1011 warning_builtin(args).expect("warning ok");
1012 let last = with_manager(|mgr| mgr.last_warning.clone());
1013 assert_eq!(
1014 last,
1015 Some(("runmat:demo:test".to_string(), "value is 7".to_string()))
1016 );
1017 }
1018
1019 #[test]
1020 fn off_suppresses_warning() {
1021 let _guard = TEST_LOCK.lock().unwrap();
1022 reset_manager();
1023 let state =
1024 warning_builtin(vec![Value::from("off"), Value::from("all")]).expect("state change");
1025 assert_state_struct(&state, "all", "on");
1026 warning_builtin(vec![Value::from("Should suppress")]).expect("warning ok");
1027 let last = with_manager(|mgr| mgr.last_warning.clone());
1028 assert!(last.is_none());
1029 }
1030
1031 #[test]
1032 fn once_only_emits_first_warning() {
1033 let _guard = TEST_LOCK.lock().unwrap();
1034 reset_manager();
1035 let state =
1036 warning_builtin(vec![Value::from("once"), Value::from("all")]).expect("state change");
1037 assert_state_struct(&state, "all", "on");
1038 warning_builtin(vec![Value::from("First")]).expect("warning ok");
1039 warning_builtin(vec![Value::from("Second")]).expect("warning ok");
1040 let last = with_manager(|mgr| mgr.last_warning.clone());
1041 assert_eq!(
1042 last,
1043 Some((DEFAULT_IDENTIFIER.to_string(), "First".to_string()))
1044 );
1045 }
1046
1047 #[test]
1048 fn error_mode_promotes_to_error() {
1049 let _guard = TEST_LOCK.lock().unwrap();
1050 reset_manager();
1051 let previous =
1052 warning_builtin(vec![Value::from("error"), Value::from("all")]).expect("state change");
1053 assert_state_struct(&previous, "all", "on");
1054 let err = warning_builtin(vec![Value::from("Promoted")]).expect_err("should error");
1055 assert_eq!(err, "MATLAB:warning: Promoted");
1056 }
1057
1058 #[test]
1059 fn query_returns_state_struct() {
1060 let _guard = TEST_LOCK.lock().unwrap();
1061 reset_manager();
1062 warning_builtin(vec![Value::from("off"), Value::from("runmat:demo:test")])
1063 .expect("state change");
1064 let value = warning_builtin(vec![Value::from("query"), Value::from("runmat:demo:test")])
1065 .expect("query ok");
1066 match value {
1067 Value::Struct(st) => {
1068 let state = st.fields.get("state").unwrap();
1069 assert_eq!(String::try_from(state).unwrap(), "off".to_string());
1070 }
1071 other => panic!("expected struct, got {other:?}"),
1072 }
1073 }
1074
1075 #[test]
1076 fn state_struct_restores_mode() {
1077 let _guard = TEST_LOCK.lock().unwrap();
1078 reset_manager();
1079 let snapshot =
1080 warning_builtin(vec![Value::from("query"), Value::from("all")]).expect("query all");
1081 warning_builtin(vec![Value::from("off"), Value::from("all")]).expect("off all");
1082 warning_builtin(vec![snapshot]).expect("restore");
1083 let state = warning_builtin(vec![
1084 Value::from("query"),
1085 Value::from("runmat:demo:restored"),
1086 ])
1087 .expect("query")
1088 .expect_struct();
1089 assert_eq!(
1090 String::try_from(state.fields.get("state").unwrap()).unwrap(),
1091 "on".to_string()
1092 );
1093 }
1094
1095 #[test]
1096 fn set_mode_backtrace_via_state() {
1097 let _guard = TEST_LOCK.lock().unwrap();
1098 reset_manager();
1099 let prev = warning_builtin(vec![Value::from("on"), Value::from("backtrace")])
1100 .expect("enable backtrace");
1101 assert_state_struct(&prev, "backtrace", "off");
1102 assert!(with_manager(|mgr| mgr.backtrace_enabled));
1103 let prev = warning_builtin(vec![Value::from("off"), Value::from("backtrace")])
1104 .expect("disable backtrace");
1105 assert_state_struct(&prev, "backtrace", "on");
1106 assert!(!with_manager(|mgr| mgr.backtrace_enabled));
1107 }
1108
1109 #[test]
1110 fn set_mode_verbose_via_state() {
1111 let _guard = TEST_LOCK.lock().unwrap();
1112 reset_manager();
1113 let prev = warning_builtin(vec![Value::from("on"), Value::from("verbose")])
1114 .expect("enable verbose");
1115 assert_state_struct(&prev, "verbose", "off");
1116 assert!(with_manager(|mgr| mgr.verbose_enabled));
1117 let prev = warning_builtin(vec![Value::from("off"), Value::from("verbose")])
1118 .expect("disable verbose");
1119 assert_state_struct(&prev, "verbose", "on");
1120 assert!(!with_manager(|mgr| mgr.verbose_enabled));
1121 }
1122
1123 #[test]
1124 fn special_mode_rejects_invalid_state() {
1125 let _guard = TEST_LOCK.lock().unwrap();
1126 reset_manager();
1127 let err = warning_builtin(vec![Value::from("once"), Value::from("backtrace")])
1128 .expect_err("invalid state");
1129 assert!(
1130 err.contains("only 'on' or 'off'"),
1131 "unexpected error message: {err}"
1132 );
1133 }
1134
1135 #[test]
1136 fn set_mode_last_requires_identifier() {
1137 let _guard = TEST_LOCK.lock().unwrap();
1138 reset_manager();
1139 let err = warning_builtin(vec![Value::from("off"), Value::from("last")])
1140 .expect_err("missing last");
1141 assert!(
1142 err.contains("no last warning identifier"),
1143 "unexpected error: {err}"
1144 );
1145 warning_builtin(vec![Value::from("Hello!")]).expect("emit warning");
1146 let previous =
1147 warning_builtin(vec![Value::from("off"), Value::from("last")]).expect("disable last");
1148 assert_state_struct(&previous, DEFAULT_IDENTIFIER, "on");
1149 let last_mode = with_manager(|mgr| mgr.lookup_mode(DEFAULT_IDENTIFIER).mode);
1150 assert!(matches!(last_mode, WarningMode::Off));
1151 }
1152
1153 #[test]
1154 fn default_returns_snapshot() {
1155 let _guard = TEST_LOCK.lock().unwrap();
1156 reset_manager();
1157 warning_builtin(vec![
1158 Value::from("off"),
1159 Value::from("runmat:demo:snapshot"),
1160 ])
1161 .expect("state change");
1162 let snapshot = warning_builtin(vec![Value::from("default")]).expect("default");
1163 let structs = structs_from_value(snapshot);
1164 assert!(structs.iter().any(|st| {
1165 field_str(st, "identifier").as_deref() == Some("all")
1166 && field_str(st, "state").as_deref() == Some("on")
1167 }));
1168 assert!(structs.iter().any(|st| {
1169 field_str(st, "identifier").as_deref() == Some("runmat:demo:snapshot")
1170 && field_str(st, "state").as_deref() == Some("off")
1171 }));
1172 assert!(structs
1173 .iter()
1174 .any(|st| { field_str(st, "identifier").as_deref() == Some("backtrace") }));
1175 assert!(structs
1176 .iter()
1177 .any(|st| { field_str(st, "identifier").as_deref() == Some("verbose") }));
1178 assert!(with_manager(|mgr| mgr.rules.is_empty()));
1179 }
1180
1181 #[test]
1182 fn default_special_modes_reset() {
1183 let _guard = TEST_LOCK.lock().unwrap();
1184 reset_manager();
1185 warning_builtin(vec![Value::from("on"), Value::from("verbose")]).expect("enable verbose");
1186 warning_builtin(vec![Value::from("on"), Value::from("backtrace")])
1187 .expect("enable backtrace");
1188 let verbose_prev =
1189 warning_builtin(vec![Value::from("default"), Value::from("verbose")]).expect("default");
1190 assert_state_struct(&verbose_prev, "verbose", "on");
1191 assert!(!with_manager(|mgr| mgr.verbose_enabled));
1192 let backtrace_prev =
1193 warning_builtin(vec![Value::from("default"), Value::from("backtrace")])
1194 .expect("default");
1195 assert_state_struct(&backtrace_prev, "backtrace", "on");
1196 assert!(!with_manager(|mgr| mgr.backtrace_enabled));
1197 }
1198
1199 #[test]
1200 fn query_backtrace_and_verbose() {
1201 let _guard = TEST_LOCK.lock().unwrap();
1202 reset_manager();
1203 warning_builtin(vec![Value::from("on"), Value::from("verbose")]).expect("enable verbose");
1204 let verbose = warning_builtin(vec![Value::from("query"), Value::from("verbose")])
1205 .expect("query verbose");
1206 assert_state_struct(&verbose, "verbose", "on");
1207 let backtrace = warning_builtin(vec![Value::from("query"), Value::from("backtrace")])
1208 .expect("query backtrace");
1209 assert_state_struct(&backtrace, "backtrace", "off");
1210 }
1211
1212 #[test]
1213 fn apply_state_struct_special_modes() {
1214 let _guard = TEST_LOCK.lock().unwrap();
1215 reset_manager();
1216 let mut backtrace = StructValue::new();
1217 backtrace
1218 .fields
1219 .insert("identifier".to_string(), Value::from("backtrace"));
1220 backtrace
1221 .fields
1222 .insert("state".to_string(), Value::from("on"));
1223 warning_builtin(vec![Value::Struct(backtrace)]).expect("apply backtrace");
1224 assert!(with_manager(|mgr| mgr.backtrace_enabled));
1225
1226 let mut verbose = StructValue::new();
1227 verbose
1228 .fields
1229 .insert("identifier".to_string(), Value::from("verbose"));
1230 verbose
1231 .fields
1232 .insert("state".to_string(), Value::from("default"));
1233 warning_builtin(vec![Value::Struct(verbose)]).expect("apply verbose");
1234 assert!(!with_manager(|mgr| mgr.verbose_enabled));
1235 }
1236
1237 trait ExpectStruct {
1238 fn expect_struct(self) -> StructValue;
1239 }
1240
1241 impl ExpectStruct for Value {
1242 fn expect_struct(self) -> StructValue {
1243 match self {
1244 Value::Struct(st) => st,
1245 other => panic!("expected struct, got {other:?}"),
1246 }
1247 }
1248 }
1249
1250 #[cfg(feature = "doc_export")]
1251 mod doc_tests {
1252 use super::*;
1253 use crate::builtins::common::test_support;
1254
1255 #[test]
1256 fn doc_examples_present() {
1257 let blocks = test_support::doc_examples(DOC_MD);
1258 assert!(!blocks.is_empty());
1259 }
1260 }
1261}