1use 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}