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