Skip to main content

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};
15use crate::builtins::diagnostics::type_resolvers::warning_type;
16use crate::console::{record_console_line, ConsoleStream};
17use crate::warning_store;
18use crate::{build_runtime_error, RuntimeError};
19use tracing;
20
21const DEFAULT_IDENTIFIER: &str = "RunMat:warning";
22
23#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::diagnostics::warning")]
24pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
25    name: "warning",
26    op_kind: GpuOpKind::Custom("control"),
27    supported_precisions: &[],
28    broadcast: BroadcastSemantics::None,
29    provider_hooks: &[],
30    constant_strategy: ConstantStrategy::InlineLiteral,
31    residency: ResidencyPolicy::GatherImmediately,
32    nan_mode: ReductionNaN::Include,
33    two_pass_threshold: None,
34    workgroup_size: None,
35    accepts_nan_mode: false,
36    notes: "Control-flow builtin; GPU backends are never invoked.",
37};
38
39#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::diagnostics::warning")]
40pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
41    name: "warning",
42    shape: ShapeRequirements::Any,
43    constant_strategy: ConstantStrategy::InlineLiteral,
44    elementwise: None,
45    reduction: None,
46    emits_nan: false,
47    notes: "Control-flow builtin; excluded from fusion planning.",
48};
49
50static MANAGER: Lazy<Mutex<WarningManager>> = Lazy::new(|| Mutex::new(WarningManager::default()));
51
52fn manager() -> &'static Mutex<WarningManager> {
53    &MANAGER
54}
55
56fn with_manager<F, R>(func: F) -> R
57where
58    F: FnOnce(&mut WarningManager) -> R,
59{
60    let mut guard = manager().lock().expect("warning manager mutex poisoned");
61    func(&mut guard)
62}
63
64fn warning_flow(identifier: &str, message: impl Into<String>) -> RuntimeError {
65    build_runtime_error(message)
66        .with_builtin("warning")
67        .with_identifier(normalize_identifier(identifier))
68        .build()
69}
70
71fn warning_default_error(message: impl Into<String>) -> RuntimeError {
72    warning_flow(DEFAULT_IDENTIFIER, message)
73}
74
75fn remap_warning_flow<F>(err: RuntimeError, identifier: &str, message: F) -> RuntimeError
76where
77    F: FnOnce(&crate::RuntimeError) -> String,
78{
79    build_runtime_error(message(&err))
80        .with_builtin("warning")
81        .with_identifier(normalize_identifier(identifier))
82        .with_source(err)
83        .build()
84}
85
86#[runtime_builtin(
87    name = "warning",
88    category = "diagnostics",
89    summary = "Display formatted warnings, control warning state, and query per-identifier settings.",
90    keywords = "warning,diagnostics,state,query,backtrace",
91    accel = "metadata",
92    sink = true,
93    suppress_auto_output = true,
94    type_resolver(warning_type),
95    builtin_path = "crate::builtins::diagnostics::warning"
96)]
97fn warning_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
98    if args.is_empty() {
99        return handle_query_default();
100    }
101
102    let first = &args[0];
103    let rest = &args[1..];
104
105    match first {
106        Value::Struct(_) | Value::Cell(_) => {
107            if !rest.is_empty() {
108                return Err(warning_default_error(
109                    "warning: state restoration accepts a single argument",
110                ));
111            }
112            apply_state_value(first)?;
113            Ok(Value::Num(0.0))
114        }
115        Value::MException(mex) => {
116            if !rest.is_empty() {
117                return Err(warning_default_error(
118                    "warning: additional arguments are not allowed when passing an MException",
119                ));
120            }
121            Ok(reissue_exception(mex)?)
122        }
123        _ => {
124            let first_string = value_to_string("warning", first)?;
125            if let Some(command) = parse_command(&first_string) {
126                return handle_command(command, rest);
127            }
128            Ok(handle_message_call(None, first_string, rest)?)
129        }
130    }
131}
132
133fn handle_message_call(
134    explicit_identifier: Option<String>,
135    first_string: String,
136    rest: &[Value],
137) -> crate::BuiltinResult<Value> {
138    if let Some(identifier) = explicit_identifier {
139        return emit_warning(&identifier, &first_string, rest);
140    }
141
142    if rest.is_empty() {
143        emit_warning(DEFAULT_IDENTIFIER, &first_string, rest)
144    } else if is_message_identifier(&first_string) {
145        let fmt = value_to_string("warning", &rest[0])?;
146        let args = &rest[1..];
147        emit_warning(&first_string, &fmt, args)
148    } else {
149        emit_warning(DEFAULT_IDENTIFIER, &first_string, rest)
150    }
151}
152
153fn emit_warning(identifier_raw: &str, fmt: &str, args: &[Value]) -> crate::BuiltinResult<Value> {
154    let identifier = normalize_identifier(identifier_raw);
155    let message = format_variadic(fmt, args).map_err(|flow| {
156        remap_warning_flow(flow, DEFAULT_IDENTIFIER, |err| err.message().to_string())
157    })?;
158
159    let action = with_manager(|mgr| {
160        let action = mgr.action_for(&identifier);
161        if matches!(action, WarningAction::Display | WarningAction::AsError) {
162            mgr.record_last(&identifier, &message);
163        }
164        action
165    });
166
167    match action {
168        WarningAction::Suppress => Ok(Value::Num(0.0)),
169        WarningAction::Display => {
170            print_warning(&identifier, &message);
171            warning_store::push(&identifier, &message);
172            Ok(Value::Num(0.0))
173        }
174        WarningAction::AsError => {
175            warning_store::push(&identifier, &message);
176            Err(warning_flow(&identifier, message))
177        }
178    }
179}
180
181fn print_warning(identifier: &str, message: &str) {
182    let (backtrace_enabled, verbose_enabled) =
183        with_manager(|mgr| (mgr.backtrace_enabled, mgr.verbose_enabled));
184
185    emit_stderr_line(format!("Warning: {message}"));
186    if identifier != DEFAULT_IDENTIFIER {
187        emit_stderr_line(format!("identifier: {identifier}"));
188    }
189
190    if verbose_enabled {
191        let suppression = if identifier == DEFAULT_IDENTIFIER {
192            DEFAULT_IDENTIFIER.to_string()
193        } else {
194            identifier.to_string()
195        };
196        emit_stderr_line(format!(
197            "(Type \"warning('off','{suppression}')\" to suppress this warning.)"
198        ));
199    }
200
201    if backtrace_enabled {
202        let bt = std::backtrace::Backtrace::force_capture();
203        emit_stderr_line(format!("{bt}"));
204    }
205}
206
207fn emit_stderr_line(line: String) {
208    tracing::warn!("{line}");
209    record_console_line(ConsoleStream::Stderr, line);
210}
211
212fn reissue_exception(mex: &runmat_builtins::MException) -> crate::BuiltinResult<Value> {
213    let identifier = normalize_identifier(&mex.identifier);
214    emit_warning(&identifier, &mex.message, &[])
215}
216
217fn handle_command(command: Command, rest: &[Value]) -> crate::BuiltinResult<Value> {
218    match command {
219        Command::SetMode(mode) => set_mode_command(mode, rest),
220        Command::Default => default_command(rest),
221        Command::Reset => {
222            if !rest.is_empty() {
223                return Err(warning_default_error(
224                    "warning: 'reset' does not accept additional arguments",
225                ));
226            }
227            with_manager(WarningManager::reset);
228            Ok(Value::Num(0.0))
229        }
230        Command::Query => query_command(rest),
231        Command::Status => status_command(rest),
232        Command::Backtrace => backtrace_command(rest),
233    }
234}
235
236fn set_mode_command(mode: WarningMode, rest: &[Value]) -> crate::BuiltinResult<Value> {
237    let identifier = if rest.is_empty() {
238        "all".to_string()
239    } else if rest.len() == 1 {
240        value_to_string("warning", &rest[0])?
241    } else {
242        return Err(warning_default_error(
243            "warning: too many input arguments for state change",
244        ));
245    };
246
247    let trimmed = identifier.trim();
248    if trimmed.eq_ignore_ascii_case("all") {
249        return with_manager(|mgr| {
250            let previous = mgr.default_mode;
251            let value = Value::Struct(mgr.state_struct_for("all", WarningRule::new(previous)));
252            mgr.set_global_mode(mode);
253            Ok(value)
254        });
255    }
256
257    if trimmed.eq_ignore_ascii_case("last") {
258        let last_identifier = with_manager(|mgr| mgr.last_warning.clone());
259        let Some((identifier, _)) = last_identifier else {
260            return Err(warning_default_error(
261                "warning: there is no last warning identifier to target",
262            ));
263        };
264        return set_mode_for_identifier(mode, &identifier);
265    }
266
267    if trimmed.eq_ignore_ascii_case("backtrace") || trimmed.eq_ignore_ascii_case("verbose") {
268        return set_mode_for_special_mode(mode, trimmed);
269    }
270
271    let normalized = normalize_identifier(trimmed);
272    set_mode_for_identifier(mode, &normalized)
273}
274
275fn set_mode_for_identifier(mode: WarningMode, identifier: &str) -> crate::BuiltinResult<Value> {
276    with_manager(|mgr| {
277        let previous = mgr.lookup_mode(identifier);
278        let value = Value::Struct(mgr.state_struct_for(identifier, previous));
279        mgr.set_identifier_mode(identifier, mode);
280        Ok(value)
281    })
282}
283
284fn reset_identifier_to_default(identifier: &str) -> crate::BuiltinResult<Value> {
285    with_manager(|mgr| {
286        let previous = mgr.lookup_mode(identifier);
287        let value = Value::Struct(mgr.state_struct_for(identifier, previous));
288        mgr.clear_identifier(identifier);
289        Ok(value)
290    })
291}
292
293fn set_mode_for_special_mode(mode: WarningMode, mode_name: &str) -> crate::BuiltinResult<Value> {
294    let mode_lower = mode_name.trim().to_ascii_lowercase();
295    if !matches!(mode, WarningMode::On | WarningMode::Off) {
296        return Err(warning_default_error(format!(
297            "warning: only 'on' or 'off' are valid states for '{mode_lower}'"
298        )));
299    }
300
301    with_manager(|mgr| {
302        let previous_enabled = if mode_lower == "backtrace" {
303            let prev = mgr.backtrace_enabled;
304            mgr.backtrace_enabled = matches!(mode, WarningMode::On);
305            prev
306        } else if mode_lower == "verbose" {
307            let prev = mgr.verbose_enabled;
308            mgr.verbose_enabled = matches!(mode, WarningMode::On);
309            prev
310        } else {
311            return Err(warning_default_error(format!(
312                "warning: unknown mode '{}'; expected 'backtrace' or 'verbose'",
313                mode_name
314            )));
315        };
316
317        let previous_state = if previous_enabled { "on" } else { "off" };
318        let value = state_struct_value(&mode_lower, previous_state);
319        Ok(value)
320    })
321}
322
323fn default_command(rest: &[Value]) -> crate::BuiltinResult<Value> {
324    match rest.len() {
325        0 => {
326            let snapshot = with_manager(|mgr| {
327                let snapshot = mgr.snapshot();
328                mgr.reset_defaults_only();
329                snapshot
330            });
331            structs_to_cell(snapshot)
332        }
333        1 => {
334            let identifier = value_to_string("warning", &rest[0])?;
335            let trimmed = identifier.trim();
336            if trimmed.eq_ignore_ascii_case("all") {
337                let snapshot = with_manager(|mgr| {
338                    let snapshot = mgr.snapshot();
339                    mgr.reset_defaults_only();
340                    snapshot
341                });
342                return structs_to_cell(snapshot);
343            }
344            if trimmed.eq_ignore_ascii_case("backtrace") {
345                return with_manager(|mgr| {
346                    let previous = if mgr.backtrace_enabled { "on" } else { "off" };
347                    mgr.backtrace_enabled = false;
348                    Ok(state_struct_value("backtrace", previous))
349                });
350            }
351            if trimmed.eq_ignore_ascii_case("verbose") {
352                return with_manager(|mgr| {
353                    let previous = if mgr.verbose_enabled { "on" } else { "off" };
354                    mgr.verbose_enabled = false;
355                    Ok(state_struct_value("verbose", previous))
356                });
357            }
358            if trimmed.eq_ignore_ascii_case("last") {
359                let last_identifier = with_manager(|mgr| mgr.last_warning.clone());
360                let Some((identifier, _)) = last_identifier else {
361                    return Err(warning_default_error(
362                        "warning: there is no last warning identifier to reset to default",
363                    ));
364                };
365                return reset_identifier_to_default(&identifier);
366            }
367            let normalized = normalize_identifier(trimmed);
368            reset_identifier_to_default(&normalized)
369        }
370        _ => Err(warning_default_error(
371            "warning: 'default' accepts zero or one identifier argument",
372        )),
373    }
374}
375
376fn query_command(rest: &[Value]) -> crate::BuiltinResult<Value> {
377    if rest.len() > 1 {
378        return Err(warning_default_error(
379            "warning: 'query' accepts at most one identifier argument",
380        ));
381    }
382
383    let target = if rest.is_empty() {
384        "all".to_string()
385    } else {
386        value_to_string("warning", &rest[0])?
387    };
388
389    with_manager(|mgr| {
390        if target.trim().eq_ignore_ascii_case("all") {
391            let snapshot = mgr.snapshot();
392            let rows = snapshot.len();
393            let entries: Vec<Value> = snapshot.into_iter().map(Value::Struct).collect();
394            let cell = CellArray::new(entries, rows, 1).map_err(|e| {
395                warning_default_error(format!("warning: failed to assemble query cell: {e}"))
396            })?;
397            Ok(Value::Cell(cell))
398        } else if target.trim().eq_ignore_ascii_case("last") {
399            if let Some((identifier, message)) = mgr.last_warning.clone() {
400                let mut st = StructValue::new();
401                st.fields
402                    .insert("identifier".to_string(), Value::from(identifier));
403                st.fields
404                    .insert("message".to_string(), Value::from(message));
405                st.fields.insert("state".to_string(), Value::from("last"));
406                Ok(Value::Struct(st))
407            } else {
408                let mut st = StructValue::new();
409                st.fields.insert("identifier".to_string(), Value::from(""));
410                st.fields.insert("message".to_string(), Value::from(""));
411                st.fields.insert("state".to_string(), Value::from("none"));
412                Ok(Value::Struct(st))
413            }
414        } else if target.trim().eq_ignore_ascii_case("backtrace") {
415            Ok(state_struct_value(
416                "backtrace",
417                if mgr.backtrace_enabled { "on" } else { "off" },
418            ))
419        } else if target.trim().eq_ignore_ascii_case("verbose") {
420            Ok(state_struct_value(
421                "verbose",
422                if mgr.verbose_enabled { "on" } else { "off" },
423            ))
424        } else {
425            let normalized = normalize_identifier(&target);
426            let state = mgr.lookup_mode(&normalized);
427            Ok(Value::Struct(mgr.state_struct_for(&normalized, state)))
428        }
429    })
430}
431
432fn status_command(rest: &[Value]) -> crate::BuiltinResult<Value> {
433    if !rest.is_empty() {
434        return Err(warning_default_error(
435            "warning: 'status' does not accept additional arguments",
436        ));
437    }
438    let value = query_command(&[])?;
439    match &value {
440        Value::Cell(cell) => {
441            emit_stderr_line("Warning status:".to_string());
442            for idx in 0..cell.data.len() {
443                let entry = (*cell.data[idx]).clone();
444                if let Value::Struct(st) = entry {
445                    let identifier = st
446                        .fields
447                        .get("identifier")
448                        .and_then(|v| value_to_string("warning", v).ok())
449                        .unwrap_or_default();
450                    let state = st
451                        .fields
452                        .get("state")
453                        .and_then(|v| value_to_string("warning", v).ok())
454                        .unwrap_or_default();
455                    emit_stderr_line(format!("  {identifier}: {state}"));
456                }
457            }
458        }
459        Value::Struct(st) => {
460            let identifier = st
461                .fields
462                .get("identifier")
463                .and_then(|v| value_to_string("warning", v).ok())
464                .unwrap_or_default();
465            let state = st
466                .fields
467                .get("state")
468                .and_then(|v| value_to_string("warning", v).ok())
469                .unwrap_or_default();
470            emit_stderr_line(format!("Warning status -> {identifier}: {state}"));
471        }
472        _ => {}
473    }
474    Ok(value)
475}
476
477fn backtrace_command(rest: &[Value]) -> crate::BuiltinResult<Value> {
478    match rest.len() {
479        0 => {
480            let state = with_manager(|mgr| if mgr.backtrace_enabled { "on" } else { "off" });
481            Ok(Value::from(state))
482        }
483        1 => {
484            let setting = value_to_string("warning", &rest[0])?;
485            match setting.trim().to_ascii_lowercase().as_str() {
486                "on" => with_manager(|mgr| mgr.backtrace_enabled = true),
487                "off" => with_manager(|mgr| mgr.backtrace_enabled = false),
488                other => {
489                    return Err(warning_default_error(format!(
490                        "warning: backtrace mode must be 'on' or 'off', got '{other}'"
491                    )))
492                }
493            }
494            Ok(Value::Num(0.0))
495        }
496        _ => Err(warning_default_error(
497            "warning: 'backtrace' accepts zero or one argument",
498        )),
499    }
500}
501
502fn handle_query_default() -> crate::BuiltinResult<Value> {
503    query_command(&[])
504}
505
506fn apply_state_value(value: &Value) -> crate::BuiltinResult<()> {
507    match value {
508        Value::Struct(st) => apply_state_struct(st),
509        Value::Cell(cell) => {
510            for idx in 0..cell.data.len() {
511                let entry = (*cell.data[idx]).clone();
512                apply_state_value(&entry)?;
513            }
514            Ok(())
515        }
516        other => Err(warning_default_error(format!(
517            "warning: expected a struct or cell array of structs, got {other:?}"
518        ))),
519    }
520}
521
522fn apply_state_struct(st: &StructValue) -> crate::BuiltinResult<()> {
523    let identifier_value = st.fields.get("identifier").ok_or_else(|| {
524        warning_default_error("warning: state struct must contain an 'identifier' field")
525    })?;
526    let state_value = st.fields.get("state").ok_or_else(|| {
527        warning_default_error("warning: state struct must contain a 'state' field")
528    })?;
529    let identifier_raw = value_to_string("warning", identifier_value)?;
530    let state_raw = value_to_string("warning", state_value)?;
531    let identifier_trimmed = identifier_raw.trim();
532    if identifier_trimmed.eq_ignore_ascii_case("all") {
533        if let Some(mode) = parse_mode_keyword(&state_raw) {
534            with_manager(|mgr| mgr.set_global_mode(mode));
535        } else {
536            return Err(warning_default_error(format!(
537                "warning: unknown state '{}'",
538                state_raw
539            )));
540        }
541    } else if identifier_trimmed.eq_ignore_ascii_case("backtrace") {
542        let state = state_raw.trim().to_ascii_lowercase();
543        match state.as_str() {
544            "on" => with_manager(|mgr| mgr.backtrace_enabled = true),
545            "off" | "default" => with_manager(|mgr| mgr.backtrace_enabled = false),
546            other => {
547                return Err(warning_default_error(format!(
548                    "warning: unknown backtrace state '{}'",
549                    other
550                )))
551            }
552        }
553    } else if identifier_trimmed.eq_ignore_ascii_case("verbose") {
554        let state = state_raw.trim().to_ascii_lowercase();
555        match state.as_str() {
556            "on" => with_manager(|mgr| mgr.verbose_enabled = true),
557            "off" | "default" => with_manager(|mgr| mgr.verbose_enabled = false),
558            other => {
559                return Err(warning_default_error(format!(
560                    "warning: unknown verbose state '{}'",
561                    other
562                )))
563            }
564        }
565    } else if identifier_trimmed.eq_ignore_ascii_case("last") {
566        let last_identifier = with_manager(|mgr| mgr.last_warning.clone());
567        let Some((identifier, _)) = last_identifier else {
568            return Err(warning_default_error(
569                "warning: there is no last warning identifier to apply state",
570            ));
571        };
572        if state_raw.trim().eq_ignore_ascii_case("default") {
573            with_manager(|mgr| mgr.clear_identifier(&identifier));
574        } else if let Some(mode) = parse_mode_keyword(&state_raw) {
575            with_manager(|mgr| mgr.set_identifier_mode(&identifier, mode));
576        } else {
577            return Err(warning_default_error(format!(
578                "warning: unknown state '{}'",
579                state_raw
580            )));
581        }
582    } else if state_raw.trim().eq_ignore_ascii_case("default") {
583        let normalized = normalize_identifier(identifier_trimmed);
584        with_manager(|mgr| mgr.clear_identifier(&normalized));
585    } else if let Some(mode) = parse_mode_keyword(&state_raw) {
586        let normalized = normalize_identifier(identifier_trimmed);
587        with_manager(|mgr| mgr.set_identifier_mode(&normalized, mode));
588    } else {
589        return Err(warning_default_error(format!(
590            "warning: unknown state '{}'",
591            state_raw
592        )));
593    }
594    Ok(())
595}
596
597#[derive(Clone, Copy, Debug, PartialEq, Eq)]
598enum WarningMode {
599    On,
600    Off,
601    Once,
602    Error,
603}
604
605impl WarningMode {
606    fn keyword(self) -> &'static str {
607        match self {
608            WarningMode::On => "on",
609            WarningMode::Off => "off",
610            WarningMode::Once => "once",
611            WarningMode::Error => "error",
612        }
613    }
614}
615
616#[derive(Clone, Copy)]
617enum Command {
618    SetMode(WarningMode),
619    Default,
620    Reset,
621    Query,
622    Status,
623    Backtrace,
624}
625
626fn parse_command(text: &str) -> Option<Command> {
627    match text.trim().to_ascii_lowercase().as_str() {
628        "on" => Some(Command::SetMode(WarningMode::On)),
629        "off" => Some(Command::SetMode(WarningMode::Off)),
630        "once" => Some(Command::SetMode(WarningMode::Once)),
631        "error" => Some(Command::SetMode(WarningMode::Error)),
632        "default" => Some(Command::Default),
633        "reset" => Some(Command::Reset),
634        "query" => Some(Command::Query),
635        "status" => Some(Command::Status),
636        "backtrace" => Some(Command::Backtrace),
637        _ => None,
638    }
639}
640
641fn parse_mode_keyword(text: &str) -> Option<WarningMode> {
642    match text.trim().to_ascii_lowercase().as_str() {
643        "on" => Some(WarningMode::On),
644        "off" => Some(WarningMode::Off),
645        "once" => Some(WarningMode::Once),
646        "error" => Some(WarningMode::Error),
647        _ => None,
648    }
649}
650
651#[derive(Clone, Copy)]
652struct WarningRule {
653    mode: WarningMode,
654    triggered: bool,
655}
656
657impl WarningRule {
658    fn new(mode: WarningMode) -> Self {
659        Self {
660            mode,
661            triggered: false,
662        }
663    }
664}
665
666enum WarningAction {
667    Suppress,
668    Display,
669    AsError,
670}
671
672struct WarningManager {
673    default_mode: WarningMode,
674    rules: HashMap<String, WarningRule>,
675    once_seen_default: HashSet<String>,
676    backtrace_enabled: bool,
677    verbose_enabled: bool,
678    last_warning: Option<(String, String)>,
679}
680
681impl Default for WarningManager {
682    fn default() -> Self {
683        Self {
684            default_mode: WarningMode::On,
685            rules: HashMap::new(),
686            once_seen_default: HashSet::new(),
687            backtrace_enabled: false,
688            verbose_enabled: false,
689            last_warning: None,
690        }
691    }
692}
693
694impl WarningManager {
695    fn set_global_mode(&mut self, mode: WarningMode) {
696        self.once_seen_default.clear();
697        self.default_mode = mode;
698    }
699
700    fn set_identifier_mode(&mut self, identifier: &str, mode: WarningMode) {
701        if mode == self.default_mode && !matches!(mode, WarningMode::Once) {
702            self.rules.remove(identifier);
703        } else {
704            self.rules
705                .insert(identifier.to_string(), WarningRule::new(mode));
706        }
707        if matches!(mode, WarningMode::Once) {
708            self.once_seen_default.remove(identifier);
709        }
710    }
711
712    fn clear_identifier(&mut self, identifier: &str) {
713        self.rules.remove(identifier);
714        self.once_seen_default.remove(identifier);
715    }
716
717    fn reset(&mut self) {
718        self.default_mode = WarningMode::On;
719        self.rules.clear();
720        self.once_seen_default.clear();
721        self.backtrace_enabled = false;
722        self.verbose_enabled = false;
723        self.last_warning = None;
724    }
725
726    fn reset_defaults_only(&mut self) {
727        self.default_mode = WarningMode::On;
728        self.once_seen_default.clear();
729        self.rules.clear();
730        self.backtrace_enabled = false;
731        self.verbose_enabled = false;
732    }
733
734    fn action_for(&mut self, identifier: &str) -> WarningAction {
735        if let Some(rule) = self.rules.get_mut(identifier) {
736            return match rule.mode {
737                WarningMode::On => WarningAction::Display,
738                WarningMode::Off => WarningAction::Suppress,
739                WarningMode::Error => WarningAction::AsError,
740                WarningMode::Once => {
741                    if rule.triggered {
742                        WarningAction::Suppress
743                    } else {
744                        rule.triggered = true;
745                        WarningAction::Display
746                    }
747                }
748            };
749        }
750
751        match self.default_mode {
752            WarningMode::On => WarningAction::Display,
753            WarningMode::Off => WarningAction::Suppress,
754            WarningMode::Error => WarningAction::AsError,
755            WarningMode::Once => {
756                if self.once_seen_default.contains(identifier) {
757                    WarningAction::Suppress
758                } else {
759                    self.once_seen_default.insert(identifier.to_string());
760                    WarningAction::Display
761                }
762            }
763        }
764    }
765
766    fn record_last(&mut self, identifier: &str, message: &str) {
767        self.last_warning = Some((identifier.to_string(), message.to_string()));
768    }
769
770    fn default_state_struct(&self) -> StructValue {
771        let mut st = StructValue::new();
772        st.fields
773            .insert("identifier".to_string(), Value::from("all".to_string()));
774        st.fields.insert(
775            "state".to_string(),
776            Value::from(self.default_mode.keyword()),
777        );
778        st
779    }
780
781    fn state_struct_for(&self, identifier: &str, rule: WarningRule) -> StructValue {
782        let mut st = StructValue::new();
783        st.fields.insert(
784            "identifier".to_string(),
785            Value::from(identifier.to_string()),
786        );
787        st.fields
788            .insert("state".to_string(), Value::from(rule.mode.keyword()));
789        st
790    }
791
792    fn lookup_mode(&self, identifier: &str) -> WarningRule {
793        self.rules
794            .get(identifier)
795            .copied()
796            .unwrap_or_else(|| WarningRule::new(self.default_mode))
797    }
798
799    fn snapshot(&self) -> Vec<StructValue> {
800        let mut entries = Vec::new();
801        entries.push(self.default_state_struct());
802        for (id, rule) in self.rules.iter() {
803            entries.push(self.state_struct_for(id, *rule));
804        }
805        entries.push(state_struct(
806            "backtrace",
807            if self.backtrace_enabled { "on" } else { "off" },
808        ));
809        entries.push(state_struct(
810            "verbose",
811            if self.verbose_enabled { "on" } else { "off" },
812        ));
813        entries
814    }
815}
816
817fn value_to_string(context: &str, value: &Value) -> crate::BuiltinResult<String> {
818    match value {
819        Value::String(s) => Ok(s.clone()),
820        Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
821        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
822        Value::CharArray(_) => Err(warning_default_error(format!(
823            "{context}: expected scalar char array"
824        ))),
825        Value::StringArray(_) => Err(warning_default_error(format!(
826            "{context}: expected scalar string"
827        ))),
828        other => String::try_from(other).map_err(|_| {
829            warning_default_error(format!(
830                "{context}: expected string-like argument, got {other:?}"
831            ))
832        }),
833    }
834}
835
836fn is_message_identifier(text: &str) -> bool {
837    let trimmed = text.trim();
838    if trimmed.is_empty() || !trimmed.contains(':') {
839        return false;
840    }
841    trimmed
842        .chars()
843        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ':' | '_' | '.'))
844}
845
846fn normalize_identifier(raw: &str) -> String {
847    let trimmed = raw.trim();
848    if trimmed.is_empty() {
849        DEFAULT_IDENTIFIER.to_string()
850    } else if trimmed.contains(':') {
851        trimmed.to_string()
852    } else {
853        format!("RunMat:{trimmed}")
854    }
855}
856
857fn state_struct(identifier: &str, state: &str) -> StructValue {
858    let mut st = StructValue::new();
859    st.fields.insert(
860        "identifier".to_string(),
861        Value::from(identifier.to_string()),
862    );
863    st.fields
864        .insert("state".to_string(), Value::from(state.to_string()));
865    st
866}
867
868fn state_struct_value(identifier: &str, state: &str) -> Value {
869    Value::Struct(state_struct(identifier, state))
870}
871
872fn structs_to_cell(structs: Vec<StructValue>) -> crate::BuiltinResult<Value> {
873    let rows = structs.len();
874    let values: Vec<Value> = structs.into_iter().map(Value::Struct).collect();
875    CellArray::new(values, rows, 1)
876        .map(Value::Cell)
877        .map_err(|e| warning_default_error(format!("warning: failed to assemble state cell: {e}")))
878}
879
880#[cfg(test)]
881pub(crate) mod tests {
882    use super::*;
883    use runmat_builtins::{ResolveContext, Type};
884
885    static TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
886
887    fn unwrap_error(err: crate::RuntimeError) -> crate::RuntimeError {
888        err
889    }
890
891    fn reset_manager() {
892        with_manager(WarningManager::reset);
893    }
894
895    fn assert_state_struct(value: &Value, identifier: &str, state: &str) {
896        match value {
897            Value::Struct(st) => {
898                let id = st
899                    .fields
900                    .get("identifier")
901                    .and_then(|v| String::try_from(v).ok())
902                    .unwrap_or_default();
903                let st_state = st
904                    .fields
905                    .get("state")
906                    .and_then(|v| String::try_from(v).ok())
907                    .unwrap_or_default();
908                assert_eq!(id, identifier);
909                assert_eq!(st_state, state);
910            }
911            other => panic!("expected state struct, got {other:?}"),
912        }
913    }
914
915    fn structs_from_value(value: Value) -> Vec<StructValue> {
916        match value {
917            Value::Cell(cell) => cell
918                .data
919                .iter()
920                .map(|ptr| unsafe { &*ptr.as_raw() }.clone())
921                .map(|value| match value {
922                    Value::Struct(st) => st,
923                    other => panic!("expected struct entry, got {other:?}"),
924                })
925                .collect(),
926            Value::Struct(st) => vec![st],
927            other => panic!("expected struct array, got {other:?}"),
928        }
929    }
930
931    fn field_str(struct_value: &StructValue, field: &str) -> Option<String> {
932        struct_value
933            .fields
934            .get(field)
935            .and_then(|value| String::try_from(value).ok())
936    }
937
938    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
939    #[test]
940    fn emits_basic_warning() {
941        let _guard = TEST_LOCK.lock().unwrap();
942        reset_manager();
943        let result = warning_builtin(vec![Value::from("Hello world!")]).expect("warning ok");
944        assert!(matches!(result, Value::Num(_)));
945        let last = with_manager(|mgr| mgr.last_warning.clone());
946        assert_eq!(
947            last,
948            Some((DEFAULT_IDENTIFIER.to_string(), "Hello world!".to_string()))
949        );
950    }
951
952    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
953    #[test]
954    fn emits_warning_with_identifier_and_format() {
955        let _guard = TEST_LOCK.lock().unwrap();
956        reset_manager();
957        let args = vec![
958            Value::from("runmat:demo:test"),
959            Value::from("value is %d"),
960            Value::Int(runmat_builtins::IntValue::I32(7)),
961        ];
962        warning_builtin(args).expect("warning ok");
963        let last = with_manager(|mgr| mgr.last_warning.clone());
964        assert_eq!(
965            last,
966            Some(("runmat:demo:test".to_string(), "value is 7".to_string()))
967        );
968    }
969
970    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
971    #[test]
972    fn off_suppresses_warning() {
973        let _guard = TEST_LOCK.lock().unwrap();
974        reset_manager();
975        let state =
976            warning_builtin(vec![Value::from("off"), Value::from("all")]).expect("state change");
977        assert_state_struct(&state, "all", "on");
978        warning_builtin(vec![Value::from("Should suppress")]).expect("warning ok");
979        let last = with_manager(|mgr| mgr.last_warning.clone());
980        assert!(last.is_none());
981    }
982
983    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
984    #[test]
985    fn once_only_emits_first_warning() {
986        let _guard = TEST_LOCK.lock().unwrap();
987        reset_manager();
988        let state =
989            warning_builtin(vec![Value::from("once"), Value::from("all")]).expect("state change");
990        assert_state_struct(&state, "all", "on");
991        warning_builtin(vec![Value::from("First")]).expect("warning ok");
992        warning_builtin(vec![Value::from("Second")]).expect("warning ok");
993        let last = with_manager(|mgr| mgr.last_warning.clone());
994        assert_eq!(
995            last,
996            Some((DEFAULT_IDENTIFIER.to_string(), "First".to_string()))
997        );
998    }
999
1000    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1001    #[test]
1002    fn error_mode_promotes_to_error() {
1003        let _guard = TEST_LOCK.lock().unwrap();
1004        reset_manager();
1005        let previous =
1006            warning_builtin(vec![Value::from("error"), Value::from("all")]).expect("state change");
1007        assert_state_struct(&previous, "all", "on");
1008        let err =
1009            unwrap_error(warning_builtin(vec![Value::from("Promoted")]).expect_err("should error"));
1010        assert_eq!(err.identifier(), Some(DEFAULT_IDENTIFIER));
1011        assert_eq!(err.message(), "Promoted");
1012    }
1013
1014    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1015    #[test]
1016    fn query_returns_state_struct() {
1017        let _guard = TEST_LOCK.lock().unwrap();
1018        reset_manager();
1019        warning_builtin(vec![Value::from("off"), Value::from("runmat:demo:test")])
1020            .expect("state change");
1021        let value = warning_builtin(vec![Value::from("query"), Value::from("runmat:demo:test")])
1022            .expect("query ok");
1023        match value {
1024            Value::Struct(st) => {
1025                let state = st.fields.get("state").unwrap();
1026                assert_eq!(String::try_from(state).unwrap(), "off".to_string());
1027            }
1028            other => panic!("expected struct, got {other:?}"),
1029        }
1030    }
1031
1032    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1033    #[test]
1034    fn state_struct_restores_mode() {
1035        let _guard = TEST_LOCK.lock().unwrap();
1036        reset_manager();
1037        let snapshot =
1038            warning_builtin(vec![Value::from("query"), Value::from("all")]).expect("query all");
1039        warning_builtin(vec![Value::from("off"), Value::from("all")]).expect("off all");
1040        warning_builtin(vec![snapshot]).expect("restore");
1041        let state = warning_builtin(vec![
1042            Value::from("query"),
1043            Value::from("runmat:demo:restored"),
1044        ])
1045        .expect("query")
1046        .expect_struct();
1047        assert_eq!(
1048            String::try_from(state.fields.get("state").unwrap()).unwrap(),
1049            "on".to_string()
1050        );
1051    }
1052
1053    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1054    #[test]
1055    fn set_mode_backtrace_via_state() {
1056        let _guard = TEST_LOCK.lock().unwrap();
1057        reset_manager();
1058        let prev = warning_builtin(vec![Value::from("on"), Value::from("backtrace")])
1059            .expect("enable backtrace");
1060        assert_state_struct(&prev, "backtrace", "off");
1061        assert!(with_manager(|mgr| mgr.backtrace_enabled));
1062        let prev = warning_builtin(vec![Value::from("off"), Value::from("backtrace")])
1063            .expect("disable backtrace");
1064        assert_state_struct(&prev, "backtrace", "on");
1065        assert!(!with_manager(|mgr| mgr.backtrace_enabled));
1066    }
1067
1068    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1069    #[test]
1070    fn set_mode_verbose_via_state() {
1071        let _guard = TEST_LOCK.lock().unwrap();
1072        reset_manager();
1073        let prev = warning_builtin(vec![Value::from("on"), Value::from("verbose")])
1074            .expect("enable verbose");
1075        assert_state_struct(&prev, "verbose", "off");
1076        assert!(with_manager(|mgr| mgr.verbose_enabled));
1077        let prev = warning_builtin(vec![Value::from("off"), Value::from("verbose")])
1078            .expect("disable verbose");
1079        assert_state_struct(&prev, "verbose", "on");
1080        assert!(!with_manager(|mgr| mgr.verbose_enabled));
1081    }
1082
1083    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1084    #[test]
1085    fn special_mode_rejects_invalid_state() {
1086        let _guard = TEST_LOCK.lock().unwrap();
1087        reset_manager();
1088        let err = unwrap_error(
1089            warning_builtin(vec![Value::from("once"), Value::from("backtrace")])
1090                .expect_err("invalid state"),
1091        );
1092        assert_eq!(err.identifier(), Some(DEFAULT_IDENTIFIER));
1093        assert!(
1094            err.message().contains("only 'on' or 'off'"),
1095            "unexpected error message: {}",
1096            err.message()
1097        );
1098    }
1099
1100    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1101    #[test]
1102    fn set_mode_last_requires_identifier() {
1103        let _guard = TEST_LOCK.lock().unwrap();
1104        reset_manager();
1105        let err = unwrap_error(
1106            warning_builtin(vec![Value::from("off"), Value::from("last")])
1107                .expect_err("missing last"),
1108        );
1109        assert_eq!(err.identifier(), Some(DEFAULT_IDENTIFIER));
1110        assert!(
1111            err.message().contains("no last warning identifier"),
1112            "unexpected error: {}",
1113            err.message()
1114        );
1115        warning_builtin(vec![Value::from("Hello!")]).expect("emit warning");
1116        let previous =
1117            warning_builtin(vec![Value::from("off"), Value::from("last")]).expect("disable last");
1118        assert_state_struct(&previous, DEFAULT_IDENTIFIER, "on");
1119        let last_mode = with_manager(|mgr| mgr.lookup_mode(DEFAULT_IDENTIFIER).mode);
1120        assert!(matches!(last_mode, WarningMode::Off));
1121    }
1122
1123    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1124    #[test]
1125    fn default_returns_snapshot() {
1126        let _guard = TEST_LOCK.lock().unwrap();
1127        reset_manager();
1128        warning_builtin(vec![
1129            Value::from("off"),
1130            Value::from("runmat:demo:snapshot"),
1131        ])
1132        .expect("state change");
1133        let snapshot = warning_builtin(vec![Value::from("default")]).expect("default");
1134        let structs = structs_from_value(snapshot);
1135        assert!(structs.iter().any(|st| {
1136            field_str(st, "identifier").as_deref() == Some("all")
1137                && field_str(st, "state").as_deref() == Some("on")
1138        }));
1139        assert!(structs.iter().any(|st| {
1140            field_str(st, "identifier").as_deref() == Some("runmat:demo:snapshot")
1141                && field_str(st, "state").as_deref() == Some("off")
1142        }));
1143        assert!(structs
1144            .iter()
1145            .any(|st| { field_str(st, "identifier").as_deref() == Some("backtrace") }));
1146        assert!(structs
1147            .iter()
1148            .any(|st| { field_str(st, "identifier").as_deref() == Some("verbose") }));
1149        assert!(with_manager(|mgr| mgr.rules.is_empty()));
1150    }
1151
1152    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1153    #[test]
1154    fn default_special_modes_reset() {
1155        let _guard = TEST_LOCK.lock().unwrap();
1156        reset_manager();
1157        warning_builtin(vec![Value::from("on"), Value::from("verbose")]).expect("enable verbose");
1158        warning_builtin(vec![Value::from("on"), Value::from("backtrace")])
1159            .expect("enable backtrace");
1160        let verbose_prev =
1161            warning_builtin(vec![Value::from("default"), Value::from("verbose")]).expect("default");
1162        assert_state_struct(&verbose_prev, "verbose", "on");
1163        assert!(!with_manager(|mgr| mgr.verbose_enabled));
1164        let backtrace_prev =
1165            warning_builtin(vec![Value::from("default"), Value::from("backtrace")])
1166                .expect("default");
1167        assert_state_struct(&backtrace_prev, "backtrace", "on");
1168        assert!(!with_manager(|mgr| mgr.backtrace_enabled));
1169    }
1170
1171    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1172    #[test]
1173    fn query_backtrace_and_verbose() {
1174        let _guard = TEST_LOCK.lock().unwrap();
1175        reset_manager();
1176        warning_builtin(vec![Value::from("on"), Value::from("verbose")]).expect("enable verbose");
1177        let verbose = warning_builtin(vec![Value::from("query"), Value::from("verbose")])
1178            .expect("query verbose");
1179        assert_state_struct(&verbose, "verbose", "on");
1180        let backtrace = warning_builtin(vec![Value::from("query"), Value::from("backtrace")])
1181            .expect("query backtrace");
1182        assert_state_struct(&backtrace, "backtrace", "off");
1183    }
1184
1185    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1186    #[test]
1187    fn apply_state_struct_special_modes() {
1188        let _guard = TEST_LOCK.lock().unwrap();
1189        reset_manager();
1190        let mut backtrace = StructValue::new();
1191        backtrace
1192            .fields
1193            .insert("identifier".to_string(), Value::from("backtrace"));
1194        backtrace
1195            .fields
1196            .insert("state".to_string(), Value::from("on"));
1197        warning_builtin(vec![Value::Struct(backtrace)]).expect("apply backtrace");
1198        assert!(with_manager(|mgr| mgr.backtrace_enabled));
1199
1200        let mut verbose = StructValue::new();
1201        verbose
1202            .fields
1203            .insert("identifier".to_string(), Value::from("verbose"));
1204        verbose
1205            .fields
1206            .insert("state".to_string(), Value::from("default"));
1207        warning_builtin(vec![Value::Struct(verbose)]).expect("apply verbose");
1208        assert!(!with_manager(|mgr| mgr.verbose_enabled));
1209    }
1210
1211    #[test]
1212    fn warning_type_is_unknown() {
1213        assert_eq!(
1214            warning_type(&[Type::String], &ResolveContext::new(Vec::new())),
1215            Type::Unknown
1216        );
1217    }
1218
1219    trait ExpectStruct {
1220        fn expect_struct(self) -> StructValue;
1221    }
1222
1223    impl ExpectStruct for Value {
1224        fn expect_struct(self) -> StructValue {
1225            match self {
1226                Value::Struct(st) => st,
1227                other => panic!("expected struct, got {other:?}"),
1228            }
1229        }
1230    }
1231}