runmat_runtime/builtins/diagnostics/
warning.rs

1//! MATLAB-compatible `warning` builtin with state management and formatting support.
2
3use 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}