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