1use crate::instr::Instr;
5use crate::rt::{DataType, InstrSrc, Source, Span};
6use ariadne::FnCache;
7use ariadne::{Color, Label, Report, ReportKind};
8use smol_strc::{SmolStr, ToSmolStr};
9use std::cell::Cell;
10use std::cell::RefCell;
11use std::hint::unreachable_unchecked;
12use std::io::Write;
13use std::ops::Range;
14use std::sync::Arc;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Diagnostic {
24 pub filename: String,
25 pub span: Range<usize>,
26 pub message: String,
27 pub code: String,
28}
29
30struct FatalError;
33
34thread_local! {
35 static DIAGNOSTIC_SINK: RefCell<Option<Diagnostic>> = const { RefCell::new(None) };
36 static SINK_ACTIVE: Cell<bool> = const { Cell::new(false) };
37 static SUPPRESS_PANIC_HOOK: Cell<bool> = const { Cell::new(false) };
38}
39
40type BoxedHook = Box<dyn Fn(&std::panic::PanicHookInfo<'_>) + Sync + Send + 'static>;
41
42struct HookState {
55 depth: usize,
56 previous: Option<Arc<BoxedHook>>,
57}
58
59static HOOK_STATE: std::sync::Mutex<HookState> = std::sync::Mutex::new(HookState {
60 depth: 0,
61 previous: None,
62});
63
64fn lock_hook_state() -> std::sync::MutexGuard<'static, HookState> {
67 HOOK_STATE
68 .lock()
69 .unwrap_or_else(std::sync::PoisonError::into_inner)
70}
71
72fn enter_silencing_hook() {
79 let mut state = lock_hook_state();
80 if state.depth == 0 {
81 let previous = Arc::new(std::panic::take_hook());
82 let forward = Arc::clone(&previous);
83 std::panic::set_hook(Box::new(move |info| {
84 if SUPPRESS_PANIC_HOOK.with(Cell::get) && info.payload().is::<FatalError>() {
85 return;
86 }
87 forward(info);
88 }));
89 state.previous = Some(previous);
90 }
91 state.depth += 1;
92}
93
94fn leave_silencing_hook() {
98 let mut state = lock_hook_state();
99 state.depth -= 1;
100 if state.depth == 0
101 && let Some(previous) = state.previous.take()
102 {
103 drop(std::panic::take_hook());
107 if let Some(previous) = Arc::into_inner(previous) {
108 std::panic::set_hook(previous);
109 }
110 }
111}
112
113#[inline]
118pub fn diagnostics_enabled() -> bool {
119 SINK_ACTIVE.with(Cell::get)
120}
121
122#[cold]
128#[inline(never)]
129pub fn emit_diagnostic(filename: &str, span: Range<usize>, message: String, code: &str) -> ! {
130 DIAGNOSTIC_SINK.with(|sink| {
131 *sink.borrow_mut() = Some(Diagnostic {
132 filename: filename.to_owned(),
133 span,
134 message,
135 code: code.to_owned(),
136 });
137 });
138 std::panic::panic_any(FatalError);
139}
140
141pub fn collect_diagnostic<R>(f: impl FnOnce() -> R) -> Result<R, Diagnostic> {
168 enter_silencing_hook();
169 let previous_active = SINK_ACTIVE.replace(true);
170 let previous_sink = DIAGNOSTIC_SINK.with(|sink| sink.borrow_mut().take());
171 let previous_suppress = SUPPRESS_PANIC_HOOK.replace(true);
172 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
173 let recorded = DIAGNOSTIC_SINK.with(|sink| sink.borrow_mut().take());
174 DIAGNOSTIC_SINK.with(|sink| *sink.borrow_mut() = previous_sink);
175 SINK_ACTIVE.set(previous_active);
176 SUPPRESS_PANIC_HOOK.set(previous_suppress);
177 leave_silencing_hook();
178 match outcome {
179 Ok(value) => Ok(value),
180 Err(payload) => {
181 if payload.is::<FatalError>() {
182 Err(recorded.unwrap_or_else(|| Diagnostic {
183 filename: String::new(),
184 span: 0..0,
185 message: String::from("unknown error"),
186 code: String::from("unknown"),
187 }))
188 } else {
189 std::panic::resume_unwind(payload);
190 }
191 }
192 }
193}
194
195pub fn strip_ansi(s: &str) -> String {
198 let mut out = String::with_capacity(s.len());
199 let mut chars = s.chars();
200 while let Some(c) = chars.next() {
201 if c == '\x1B' {
202 if chars.next() == Some('[') {
204 for f in chars.by_ref() {
205 if ('@'..='~').contains(&f) {
206 break;
207 }
208 }
209 }
210 } else {
211 out.push(c);
212 }
213 }
214 out
215}
216
217pub const BLUE: &str = "\x1B[94m";
218pub fn blue<F: std::fmt::Display>(t: F) -> String {
219 format!("{BLUE}{t}{RESET}")
220}
221pub const RED: &str = "\x1B[31m";
222pub fn red<F: std::fmt::Display>(t: F) -> String {
223 format!("{RED}{t}{RESET}")
224}
225pub const BOLD: &str = "\x1B[1m";
226pub fn bold<F: std::fmt::Display>(t: F) -> String {
227 format!("{BOLD}{t}{RESET}")
228}
229pub const GREEN: &str = "\x1B[32m";
230pub fn green<F: std::fmt::Display>(t: F) -> String {
231 format!("{GREEN}{t}{RESET}")
232}
233pub const RESET: &str = "\x1B[0m\x1B[39m";
234
235#[derive(Clone, Copy)]
239pub struct ErrorCtx<'a> {
240 pub instr_src: &'a [InstrSrc],
241 pub sources: &'a [Source],
242}
243
244#[cold]
248#[inline(never)]
249#[must_use]
250pub fn call_site_name<'a>(ctx: &ErrorCtx<'a>, instr: Instr) -> &'a str {
251 let name = ctx
252 .instr_src
253 .iter()
254 .find(|s| s.instr == instr)
255 .and_then(|src| {
256 let contents = &ctx.sources.get(src.file_id as usize)?.contents;
257 contents.get((src.span.start as usize)..(src.span.end as usize))
258 })
259 .and_then(|text| text.split(['(', '<']).next())
260 .map(str::trim)
261 .unwrap_or_default();
262 if name.is_empty() { "a function" } else { name }
263}
264
265impl From<std::io::ErrorKind> for ErrType<'_> {
266 fn from(value: std::io::ErrorKind) -> Self {
267 match value {
268 std::io::ErrorKind::AlreadyExists => ErrType::FsAlreadyExists,
269 std::io::ErrorKind::Deadlock => ErrType::FsDeadlock,
270 std::io::ErrorKind::FileTooLarge => ErrType::FsFileTooLarge,
271 std::io::ErrorKind::Interrupted => ErrType::FsInterrupted,
272 std::io::ErrorKind::InvalidData => ErrType::FsInvalidData,
273 std::io::ErrorKind::InvalidFilename => ErrType::FsInvalidFilename,
274 std::io::ErrorKind::IsADirectory => ErrType::FsIsADirectory,
275 std::io::ErrorKind::NotADirectory => ErrType::FsNotADirectory,
276 std::io::ErrorKind::NotFound => ErrType::FsNotFound,
277 std::io::ErrorKind::PermissionDenied => ErrType::FsPermissionDenied,
278 std::io::ErrorKind::OutOfMemory => ErrType::FsOutOfMemory,
279 std::io::ErrorKind::ReadOnlyFilesystem => ErrType::FsReadOnlyFilesystem,
280 std::io::ErrorKind::StorageFull => ErrType::FsStorageFull,
281 std::io::ErrorKind::TimedOut => ErrType::FsTimedOut,
282 _ => unsafe { unreachable_unchecked() },
283 }
284 }
285}
286
287impl From<std::num::IntErrorKind> for ErrType<'_> {
288 fn from(value: std::num::IntErrorKind) -> Self {
289 match value {
290 std::num::IntErrorKind::Empty
291 | std::num::IntErrorKind::InvalidDigit
292 | std::num::IntErrorKind::NegOverflow
293 | std::num::IntErrorKind::PosOverflow => ErrType::InvalidInt,
294 _ => unsafe { unreachable_unchecked() },
295 }
296 }
297}
298
299pub enum ErrType<'a> {
301 Custom(&'a str),
302 FsAlreadyExists,
304 FsDeadlock,
305 FsFileTooLarge,
306 FsInterrupted,
307 FsInvalidData,
308 FsInvalidFilename,
309 FsIsADirectory,
311 FsNotADirectory,
313 FsNotFound,
314 FsPermissionDenied,
315 FsOutOfMemory,
316 FsReadOnlyFilesystem,
317 FsStorageFull,
318 FsTimedOut,
319 InvalidInt,
321 InvalidFloat,
322 InvalidBool,
324 IndexOutOfBounds(usize, i64),
326 SliceOutOfBounds(usize, i64, i64),
328 UnknownMapKey(&'a str),
329 NullByteInString,
330 CArrayReturnTypeNotSupported,
331 InvalidReturnType(&'a DataType),
332 DivisionByZero,
333 ModuloByZero,
334 NegativeExponent(i64),
337 ShiftCountOutOfRange(i64),
341 JsonParse(&'static str),
343 BadDowncast(&'a str, &'static str),
346 HostFn(&'a str, &'a str),
349 NotAString(&'static str),
353 CallDepthExceeded(&'a str, usize),
356}
357
358impl From<ErrType<'_>> for SmolStr {
359 fn from(value: ErrType) -> Self {
360 match value {
361 ErrType::Custom(m) => m.to_smolstr(),
362 ErrType::InvalidFloat => "Invalid float".into(),
363 ErrType::IndexOutOfBounds(length, index) => format_args!("Tried to get index {RED}{BOLD}{index}{RESET} but the length is {BLUE}{BOLD}{length}{RESET}").to_smolstr(),
364 ErrType::SliceOutOfBounds(length, idx_start, idx_end) => format_args!("Invalid range {RED}{BOLD}{idx_start}{RESET}..{RED}{BOLD}{idx_end}{RESET} for collection with length {BLUE}{BOLD}{length}{RESET}").to_smolstr(),
365 ErrType::InvalidBool => "The string could not be parsed into a boolean".into(),
366 ErrType::InvalidInt => "Invalid integer".into(),
367 ErrType::FsAlreadyExists => "The entity (directory, file, ...) already exists".into(),
368 ErrType::FsDeadlock => "This operation would result in a deadlock".into(),
369 ErrType::FsFileTooLarge => "The file is too large".into(),
370 ErrType::FsInterrupted => "This operation was interrupted".into(),
371 ErrType::FsInvalidData => "Malformed or invalid data were encountered".into(),
372 ErrType::FsInvalidFilename => "The filename is invalid or too long".into(),
373 ErrType::FsIsADirectory => "This operation encountered a directory, when a non-directory was expected".into(),
374 ErrType::FsNotADirectory => "This operation encountered a non-directory, when a directory was expected".into(),
375 ErrType::FsNotFound => "The entity (directory, file, ...) was not found".into(),
376 ErrType::FsPermissionDenied => "This operation lacked the necessary privileges to complete".into(),
377 ErrType::FsOutOfMemory => "This operation could not be completed, because it failed to allocate enough memory".into(),
378 ErrType::FsReadOnlyFilesystem => "The filesystem or storage medium is read-only, but a write operation was attempted".into(),
379 ErrType::FsStorageFull => "Storage is full".into(),
380 ErrType::FsTimedOut => "This operation timed out".into(),
381 ErrType::DivisionByZero => "Division by zero. I'm sorry Dave, I'm afraid I can't do that.".into(),
382 ErrType::ModuloByZero => "Modulo by zero. I'm sorry Dave, I'm afraid I can't do that.".into(),
383 ErrType::NegativeExponent(exp) => format_args!("An integer cannot be raised to the negative power {RED}{BOLD}{exp}{RESET}. Use floats for a fractional result").to_smolstr(),
384 ErrType::ShiftCountOutOfRange(count) => format_args!("An {BLUE}{BOLD}int{RESET} is 64 bits wide, so it cannot be shifted by {RED}{BOLD}{count}{RESET}. The count must be between 0 and 63").to_smolstr(),
385 ErrType::NullByteInString => "String passed to dynamic library function contains an interior null byte".into(),
386 ErrType::InvalidReturnType(t) => format_args!("Invalid return type: {RED}{BOLD}{t}{RESET}").to_smolstr(),
387 ErrType::CArrayReturnTypeNotSupported => "Array return types are not supported: C does not convey the length of a returned array".into(),
388 ErrType::UnknownMapKey(key) => format_args!("Unknown key {RED}{BOLD}{key}{RESET}").to_smolstr(),
389 ErrType::JsonParse(reason) => format_args!("Invalid JSON: {RED}{BOLD}{reason}{RESET}").to_smolstr(),
390 ErrType::BadDowncast(want, got) => format_args!("Cannot read this {RED}{BOLD}{got}{RESET} value as {BLUE}{BOLD}{want}{RESET}").to_smolstr(),
391 ErrType::HostFn(function, message) => format_args!("Host function {BLUE}{BOLD}{function}{RESET} failed: {RED}{BOLD}{message}{RESET}").to_smolstr(),
392 ErrType::NotAString(got) => format_args!("Cannot use this {RED}{BOLD}{got}{RESET} value as a {BLUE}{BOLD}string{RESET}").to_smolstr(),
393 ErrType::CallDepthExceeded(name, limit) => format_args!("The call to {RED}{BOLD}{name}{RESET} stands {BLUE}{BOLD}{limit}{RESET} calls deep, which is the call depth limit").to_smolstr(),
394 }
395 }
396}
397
398pub const BUILTIN_KINDS: &[&str] = &[
402 "fs_already_exists",
403 "fs_deadlock",
404 "fs_file_too_large",
405 "fs_interrupted",
406 "fs_invalid_data",
407 "fs_invalid_filename",
408 "fs_is_a_directory",
409 "fs_not_a_directory",
410 "fs_not_found",
411 "fs_permission_denied",
412 "fs_out_of_memory",
413 "fs_read_only_filesystem",
414 "fs_storage_full",
415 "fs_timed_out",
416 "invalid_int",
417 "invalid_float",
418 "invalid_bool",
419 "index_out_of_bounds",
420 "slice_out_of_bounds",
421 "division_by_zero",
422 "modulo_by_zero",
423 "negative_exponent",
424 "shift_count_out_of_range",
425 "null_byte_in_string",
426 "c_array_return_type_not_supported",
427 "invalid_return_type",
428 "unknown_map_key",
429 "json_parse_error",
430 "bad_downcast",
431 "host_fn_error",
432 "not_a_string",
433 "call_depth_exceeded",
434];
435
436impl ErrType<'_> {
437 pub const fn kind(&self) -> &str {
438 match self {
439 ErrType::Custom(e) => e,
440 ErrType::FsAlreadyExists => "fs_already_exists",
441 ErrType::FsDeadlock => "fs_deadlock",
442 ErrType::FsFileTooLarge => "fs_file_too_large",
443 ErrType::FsInterrupted => "fs_interrupted",
444 ErrType::FsInvalidData => "fs_invalid_data",
445 ErrType::FsInvalidFilename => "fs_invalid_filename",
446 ErrType::FsIsADirectory => "fs_is_a_directory",
447 ErrType::FsNotADirectory => "fs_not_a_directory",
448 ErrType::FsNotFound => "fs_not_found",
449 ErrType::FsPermissionDenied => "fs_permission_denied",
450 ErrType::FsOutOfMemory => "fs_out_of_memory",
451 ErrType::FsReadOnlyFilesystem => "fs_read_only_filesystem",
452 ErrType::FsStorageFull => "fs_storage_full",
453 ErrType::FsTimedOut => "fs_timed_out",
454 ErrType::InvalidInt => "invalid_int",
455 ErrType::InvalidFloat => "invalid_float",
456 ErrType::InvalidBool => "invalid_bool",
457 ErrType::IndexOutOfBounds(_, _) => "index_out_of_bounds",
458 ErrType::SliceOutOfBounds(_, _, _) => "slice_out_of_bounds",
459 ErrType::DivisionByZero => "division_by_zero",
460 ErrType::ModuloByZero => "modulo_by_zero",
461 ErrType::NegativeExponent(_) => "negative_exponent",
462 ErrType::ShiftCountOutOfRange(_) => "shift_count_out_of_range",
463 ErrType::NullByteInString => "null_byte_in_string",
464 ErrType::CArrayReturnTypeNotSupported => "c_array_return_type_not_supported",
465 ErrType::InvalidReturnType(_) => "invalid_return_type",
466 ErrType::UnknownMapKey(_) => "unknown_map_key",
467 ErrType::JsonParse(_) => "json_parse_error",
468 ErrType::BadDowncast(_, _) => "bad_downcast",
469 ErrType::HostFn(_, _) => "host_fn_error",
470 ErrType::NotAString(_) => "not_a_string",
471 ErrType::CallDepthExceeded(_, _) => "call_depth_exceeded",
472 }
473 }
474}
475
476#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct RuntimeError {
485 pub instr: Instr,
486 pub code: SmolStr,
489 pub message: SmolStr,
491}
492
493impl RuntimeError {
494 #[cold]
495 #[inline(never)]
496 #[must_use]
497 pub fn new(instr: Instr, t: ErrType) -> Self {
498 let code = SmolStr::from(t.kind());
499 Self {
500 instr,
501 code,
502 message: t.into(),
503 }
504 }
505
506 fn place<'a>(&self, ctx: &ErrorCtx<'a>) -> (&'a Source, Range<usize>) {
509 let InstrSrc {
510 instr: _,
511 span: Span { start, end },
512 file_id,
513 } = ctx
514 .instr_src
515 .iter()
516 .find(|s| s.instr == self.instr)
517 .unwrap_or(&InstrSrc {
518 instr: Instr::Halt(1),
519 span: Span { start: 0, end: 0 },
520 file_id: 0,
521 });
522 (
523 &ctx.sources[*file_id as usize],
524 (*start as usize)..(*end as usize),
525 )
526 }
527
528 #[cold]
530 #[inline(never)]
531 #[must_use]
532 pub fn diagnostic(&self, ctx: &ErrorCtx<'_>) -> Diagnostic {
533 let (src, span) = self.place(ctx);
534 Diagnostic {
535 filename: src.filename.to_string(),
536 span,
537 message: strip_ansi(self.message.as_str()),
538 code: self.code.to_string(),
539 }
540 }
541
542 #[cold]
546 #[inline(never)]
547 pub fn report(&self, ctx: &ErrorCtx<'_>) -> ! {
548 let (src, span) = self.place(ctx);
549 if diagnostics_enabled() {
550 emit_diagnostic(
551 src.filename.as_str(),
552 span,
553 strip_ansi(self.message.as_str()),
554 &self.code,
555 );
556 }
557 let mut out = crate::captured_output::stderr();
558 let _ = writeln!(out, "{RED}CANDELA ERROR{RESET}");
559 let report = Report::build(ReportKind::Error, (src.filename.as_str(), span.clone()))
560 .with_label(
561 Label::new((src.filename.as_str(), span))
562 .with_message(self.message.as_str())
563 .with_color(Color::Red),
564 )
565 .finish();
566
567 let _ = report.write(
571 (
572 src.filename.as_str(),
573 ariadne::Source::from(src.contents.as_str()),
574 ),
575 &mut out,
576 );
577
578 crash();
579 }
580}
581
582#[cold]
583#[inline(never)]
584#[cfg(target_arch = "wasm32")]
585pub fn wasm_error(msg: &str) -> ! {
586 crate::captured_output::print(&format!("CANDELA ERROR\n{msg}\n"));
587 wasm_bindgen::throw_str("candela error");
588}
589
590#[cold]
591#[inline(never)]
592pub fn throw_compiler_error<'a>(
593 report: &dyn Fn() -> Report<'a, (&'a str, core::ops::Range<usize>)>,
594 sources: &'a [Source],
595 file_idx: u16,
596 span: Span,
597 message: &str,
598 code: &'static str,
599) -> ! {
600 if diagnostics_enabled() {
601 emit_diagnostic(
602 sources[file_idx as usize].filename.as_str(),
603 (span.start as usize)..(span.end as usize),
604 strip_ansi(message),
605 code,
606 );
607 }
608 let report = report();
609
610 let _ = report.write(
611 FnCache::new((move |id| Err(format!("Failed to fetch source {id}"))) as fn(&_) -> _)
612 .with_sources(
613 sources
614 .iter()
615 .map(|Source { filename, contents }| {
616 (filename.as_str(), ariadne::Source::from(contents.as_str()))
617 })
618 .collect(),
619 ),
620 crate::captured_output::stderr(),
621 );
622
623 crash();
624}
625
626#[cold]
627#[inline(never)]
628pub fn crash() -> ! {
629 #[cfg(debug_assertions)]
630 panic!();
631
632 #[cfg(not(any(debug_assertions, target_arch = "wasm32", feature = "embed")))]
633 std::process::exit(1);
634
635 #[cfg(target_arch = "wasm32")]
636 wasm_bindgen::throw_str("candela_error");
637
638 #[cfg(all(feature = "embed", not(debug_assertions)))]
639 panic!();
640}