use super::grammar_names::GrammarNames;
#[cfg(feature = "std")]
use super::line_index::LineIndex;
use crate::parse::string_table::SymbolId;
use crate::{parse::insn::LiteralTable, types::Pos, types::RuleId, types::Span};
#[cfg(not(feature = "std"))]
use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
#[cfg(feature = "std")]
use std::{
format,
string::{String, ToString},
vec::Vec,
};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Expected {
Byte(u8),
ByteRange(u8, u8),
ClassLabel(u32),
Literal(u32),
EndOfInput,
AnyChar,
Char(u32),
CharRange(u32, u32),
Flag { id: u16, required: bool },
Rule(RuleId),
Label(u32),
}
impl Expected {
#[must_use]
pub fn display(
&self,
literals: Option<&LiteralTable<'_>>,
names: Option<&dyn GrammarNames>,
) -> String {
match self {
Self::Byte(b) => {
if (*b).is_ascii_graphic() {
format!("'{}'", char::from(*b))
} else {
format!("0x{b:02X}")
}
}
Self::ByteRange(lo, hi) => {
format!("'{}'-'{}'", char::from(*lo), char::from(*hi))
}
Self::ClassLabel(label_id) => {
if let Some(n) = names {
if let Some(s) = n.class_label(*label_id) {
return s.to_string();
}
}
format!("class#{label_id}")
}
Self::Literal(lit_id) => {
if let Some(tbl) = literals {
let bytes = tbl.get(*lit_id);
if let Ok(s) = core::str::from_utf8(bytes) {
return format!("{s:?}");
}
format!("0x{bytes:X?}")
} else {
format!("literal#{lit_id}")
}
}
Self::EndOfInput => "end-of-input".to_string(),
Self::AnyChar => "any Unicode character".to_string(),
Self::Char(cp) => char::from_u32(*cp).map_or_else(
|| format!("U+{cp:04X}"),
|c| {
if c.is_alphanumeric() || c.is_ascii_punctuation() {
format!("'{c}'")
} else {
format!("U+{cp:04X}")
}
},
),
Self::CharRange(lo, hi) => {
let fmt = |cp: u32| -> String {
char::from_u32(cp)
.filter(|c| c.is_alphanumeric() || c.is_ascii_punctuation())
.map_or_else(|| format!("U+{cp:04X}"), |c| format!("'{c}'"))
};
format!("{}–{}", fmt(*lo), fmt(*hi))
}
Self::Flag { id, required } => {
let word = id >> 6;
let bit = id & 63;
if *required {
format!("flag {id} (word {word} bit {bit}) to be set")
} else {
format!("flag {id} (word {word} bit {bit}) to be clear")
}
}
Self::Rule(rule_id) => {
if let Some(n) = names {
if let Some(s) = n.rule_name(*rule_id) {
return s.to_string();
}
}
format!("rule#{rule_id}")
}
Self::Label(label_id) => {
if let Some(n) = names {
if let Some(s) = n.expected_label(*label_id) {
return s.to_string();
}
}
format!("expected#{label_id}")
}
}
}
}
pub(crate) fn format_expected_list(items: &[String]) -> String {
if items.is_empty() {
return "nothing".to_string();
}
let (display, more) = if items.len() <= MAX_EXPECTED_DISPLAY {
(items, 0)
} else {
(
&items[..MAX_EXPECTED_DISPLAY],
items.len() - MAX_EXPECTED_DISPLAY,
)
};
let list = if display.len() == 1 {
display[0].clone()
} else {
let (last, rest) = display.split_last().unwrap();
format!("{}, or {last}", rest.join(", "))
};
if more > 0 {
format!("{list} (and {more} more)")
} else {
list
}
}
#[derive(Clone, Debug)]
pub struct Diagnostic {
pub furthest: Pos,
pub expected: Vec<Expected>,
pub hints: Vec<SymbolId>,
pub context_chain: Vec<u32>,
}
const MAX_EXPECTED_DISPLAY: usize = 10;
impl Diagnostic {
#[must_use]
pub fn primary_span(&self, source_len: usize) -> Span {
let len_pos = Pos::try_from(source_len).unwrap_or(Pos::MAX);
let start = self.furthest.min(len_pos);
if start >= len_pos {
return Span::new(len_pos, len_pos);
}
Span::new(start, (start + 1).min(len_pos))
}
#[inline]
fn context_lines(&self, names: Option<&dyn GrammarNames>) -> Vec<String> {
self.context_chain
.iter()
.map(|&id| {
let label = names.and_then(|n| n.expected_label(id)).unwrap_or("?");
format!(" while parsing: {label}")
})
.collect()
}
#[inline]
fn hint_lines(&self, names: Option<&dyn GrammarNames>) -> Vec<String> {
self.hints
.iter()
.map(|&id| {
let s = names.and_then(|n| n.resolve_symbol(id)).unwrap_or("?");
format!(" hint: {s}")
})
.collect()
}
#[inline]
fn expected_string(
&self,
literals: Option<&LiteralTable<'_>>,
names: Option<&dyn GrammarNames>,
) -> String {
let items: Vec<String> = self
.expected
.iter()
.map(|e| e.display(literals, names))
.collect();
format_expected_list(&items)
}
#[must_use]
pub fn message(
&self,
literals: Option<&LiteralTable<'_>>,
names: Option<&dyn GrammarNames>,
) -> String {
let expected_str = self.expected_string(literals, names);
let mut out = format!(
"parse error at byte {}: expected {expected_str}",
self.furthest
);
for line in self.context_lines(names) {
out.push('\n');
out.push_str(&line);
}
for line in self.hint_lines(names) {
out.push('\n');
out.push_str(&line);
}
out
}
#[cfg(feature = "std")]
#[must_use]
pub fn format_with_source(
&self,
source: &[u8],
line_index: &LineIndex,
literals: Option<&LiteralTable<'_>>,
names: Option<&dyn GrammarNames>,
) -> String {
let expected_str = self.expected_string(literals, names);
let (line_1, col_1) = line_index.line_col_1based(self.furthest);
let header = format!(
"parse error at {line_1}:{col_1} (byte {}): expected {expected_str}",
self.furthest
);
if source.is_empty() || self.furthest as usize > source.len() {
let mut out = header;
for line in self.context_lines(names) {
out.push('\n');
out.push_str(&line);
}
for line in self.hint_lines(names) {
out.push('\n');
out.push_str(&line);
}
return out;
}
let mut out = header;
for line in self.context_lines(names) {
out.push('\n');
out.push_str(&line);
}
for line in self.hint_lines(names) {
out.push('\n');
out.push_str(&line);
}
out.push('\n');
out.push_str(&line_index.snippet_at(source, self.furthest));
out
}
}
impl core::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.message(None, None))
}
}
#[cfg(feature = "miette")]
impl Diagnostic {
#[must_use]
pub fn into_miette(
&self,
source: impl Into<String>,
name: impl Into<String>,
literals: Option<&LiteralTable<'_>>,
names: Option<&dyn GrammarNames>,
) -> super::miette_support::MietteParseDiagnostic {
super::miette_support::MietteParseDiagnostic::new(self, source, name, literals, names)
}
}
pub struct ErrorContext {
pub furthest: Pos,
pub expected: Vec<Expected>,
pub context_chain: Vec<u32>,
pub hints: Vec<SymbolId>,
#[cfg(feature = "std")]
seen: std::collections::HashSet<Expected>,
#[cfg(feature = "std")]
seen_hints: std::collections::HashSet<SymbolId>,
#[cfg(not(feature = "std"))]
seen: alloc::vec::Vec<Expected>,
#[cfg(not(feature = "std"))]
seen_hints: alloc::vec::Vec<SymbolId>,
}
impl ErrorContext {
#[must_use]
pub fn new() -> Self {
Self {
furthest: 0,
expected: Vec::with_capacity(8),
context_chain: Vec::new(),
hints: Vec::new(),
#[cfg(feature = "std")]
seen: std::collections::HashSet::with_capacity(8),
#[cfg(feature = "std")]
seen_hints: std::collections::HashSet::with_capacity(4),
#[cfg(not(feature = "std"))]
seen: alloc::vec::Vec::with_capacity(8),
#[cfg(not(feature = "std"))]
seen_hints: alloc::vec::Vec::with_capacity(4),
}
}
#[inline]
pub fn clear(&mut self) {
self.furthest = 0;
self.expected.clear();
self.context_chain.clear();
self.hints.clear();
self.seen.clear();
self.seen_hints.clear();
}
#[inline]
pub fn record(&mut self, pos: Pos, context_stack: &[u32], what: Expected) {
if pos > self.furthest {
self.furthest = pos;
self.context_chain.clear();
self.context_chain.extend_from_slice(context_stack);
self.hints.clear();
self.seen_hints.clear();
self.expected.clear();
self.seen.clear();
#[cfg(feature = "std")]
{
self.seen.insert(what.clone());
}
#[cfg(not(feature = "std"))]
{
self.seen.push(what.clone());
}
self.expected.push(what);
} else if pos == self.furthest {
#[cfg(feature = "std")]
let is_new = self.seen.insert(what.clone());
#[cfg(not(feature = "std"))]
let is_new = !self.seen.iter().any(|e| e == &what);
#[cfg(not(feature = "std"))]
if is_new {
self.seen.push(what.clone());
}
if is_new {
self.expected.push(what);
}
}
}
#[inline]
pub fn record_hint(&mut self, pos: Pos, context_stack: &[u32], hint_id: SymbolId) {
if pos > self.furthest {
self.furthest = pos;
self.context_chain.clear();
self.context_chain.extend_from_slice(context_stack);
self.expected.clear();
self.seen.clear();
self.hints.clear();
self.seen_hints.clear();
#[cfg(feature = "std")]
{
self.seen_hints.insert(hint_id);
}
#[cfg(not(feature = "std"))]
{
self.seen_hints.push(hint_id);
}
self.hints.push(hint_id);
} else if pos == self.furthest {
#[cfg(feature = "std")]
let is_new = self.seen_hints.insert(hint_id);
#[cfg(not(feature = "std"))]
let is_new = !self.seen_hints.iter().any(|h| h == &hint_id);
#[cfg(not(feature = "std"))]
if is_new {
self.seen_hints.push(hint_id);
}
if is_new {
self.hints.push(hint_id);
}
}
}
#[must_use]
pub fn to_diagnostic(&self) -> Diagnostic {
Diagnostic {
furthest: self.furthest,
expected: self.expected.clone(),
hints: self.hints.clone(),
context_chain: self.context_chain.clone(),
}
}
}
impl Default for ErrorContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Severity {
Error,
Warning,
Deprecation,
Note,
}
#[derive(Clone, Debug)]
pub struct RelatedLocation {
pub span: Span,
pub message: String,
}
#[derive(Clone, Debug)]
pub struct SemanticDiagnostic {
pub span: Span,
pub message: String,
pub severity: Severity,
pub code: Option<String>,
pub file_id: Option<String>,
pub related: Vec<RelatedLocation>,
}
impl SemanticDiagnostic {
#[cfg(feature = "std")]
#[must_use]
pub fn format_with_source(&self, source: &[u8], line_index: &LineIndex) -> String {
let (line_1, col_1) = line_index.line_col_1based(self.span.start);
let severity_label = match self.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Deprecation => "deprecation",
Severity::Note => "note",
};
let code_part = self
.code
.as_deref()
.map(|c| format!(" [{c}]"))
.unwrap_or_default();
let location = self.file_id.as_deref().map_or_else(
|| format!("{line_1}:{col_1}"),
|f| format!("{f}:{line_1}:{col_1}"),
);
let header = format!(
"{}: {}{}: {}",
location, severity_label, code_part, self.message
);
if source.is_empty() || self.span.start as usize >= source.len() {
return header;
}
let mut out = String::new();
out.push_str(&header);
out.push('\n');
out.push_str(&line_index.snippet_at(source, self.span.start));
if !self.related.is_empty() {
use core::fmt::Write as _;
for rel in &self.related {
let (rl, rc) = line_index.line_col_1based(rel.span.start);
out.push_str("\n\nrelated: ");
out.push_str(&rel.message);
out.push_str(" at ");
let _ = write!(out, "{rl}:{rc}");
if (rel.span.start as usize) < source.len() {
out.push('\n');
out.push_str(&line_index.snippet_at(source, rel.span.start));
}
}
}
out
}
pub fn error(span: Span, message: impl Into<String>) -> Self {
Self {
span,
message: message.into(),
severity: Severity::Error,
code: None,
file_id: None,
related: Vec::new(),
}
}
pub fn warning(span: Span, message: impl Into<String>) -> Self {
Self {
span,
message: message.into(),
severity: Severity::Warning,
code: None,
file_id: None,
related: Vec::new(),
}
}
pub fn deprecation(span: Span, message: impl Into<String>) -> Self {
Self {
span,
message: message.into(),
severity: Severity::Deprecation,
code: None,
file_id: None,
related: Vec::new(),
}
}
#[must_use]
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
#[must_use]
pub fn with_file_id(mut self, file_id: impl Into<String>) -> Self {
self.file_id = Some(file_id.into());
self
}
#[must_use]
pub fn with_related(mut self, related: Vec<RelatedLocation>) -> Self {
self.related = related;
self
}
}
#[cfg(feature = "miette")]
impl SemanticDiagnostic {
#[must_use]
pub fn into_miette(
&self,
source: impl Into<String>,
name: impl Into<String>,
) -> super::miette_support::MietteSemanticDiagnostic {
super::miette_support::MietteSemanticDiagnostic::new(self, source, name)
}
}
#[cfg(test)]
mod tests {
use super::*;
extern crate alloc;
use alloc::vec;
#[test]
fn error_context_record_furthest() {
let mut ctx = ErrorContext::new();
ctx.record(0, &[], Expected::Byte(b'a'));
ctx.record(1, &[], Expected::Byte(b'b'));
ctx.record(0, &[], Expected::Byte(b'c')); assert_eq!(ctx.furthest, 1);
assert_eq!(ctx.expected.len(), 1);
assert!(matches!(ctx.expected[0], Expected::Byte(b'b')));
}
#[test]
fn error_context_record_same_pos_dedup() {
let mut ctx = ErrorContext::new();
ctx.record(5, &[], Expected::Byte(b'x'));
ctx.record(5, &[], Expected::EndOfInput);
ctx.record(5, &[], Expected::Byte(b'x')); assert_eq!(ctx.furthest, 5);
assert_eq!(ctx.expected.len(), 2);
}
#[test]
fn error_context_to_diagnostic() {
let mut ctx = ErrorContext::new();
ctx.record(10, &[], Expected::Literal(0));
ctx.record(10, &[], Expected::EndOfInput);
let diag = ctx.to_diagnostic();
assert_eq!(diag.furthest, 10);
assert_eq!(diag.expected.len(), 2);
assert!(diag.hints.is_empty());
}
#[test]
fn expected_rule_display() {
let rule = Expected::Rule(0);
let tables = super::super::grammar_names::SliceGrammarNames {
rule_names: &["start"],
expected_labels: &[],
class_labels: &[],
};
assert_eq!(rule.display(None, Some(&tables)), "start");
assert_eq!(rule.display(None, None), "rule#0");
}
#[test]
fn expected_label_display() {
let label = Expected::Label(0);
let tables = super::super::grammar_names::SliceGrammarNames {
rule_names: &[],
expected_labels: &["statement"],
class_labels: &[],
};
assert_eq!(label.display(None, Some(&tables)), "statement");
assert_eq!(label.display(None, None), "expected#0");
}
#[test]
fn error_context_snapshots_context_chain_on_furthest_advance() {
let mut ctx = ErrorContext::new();
ctx.record(0, &[1, 2], Expected::Byte(b'a'));
assert_eq!(ctx.furthest, 0);
assert!(ctx.context_chain.is_empty());
ctx.record(1, &[1, 2], Expected::Byte(b'b'));
assert_eq!(ctx.furthest, 1);
assert_eq!(ctx.context_chain, vec![1, 2]);
ctx.record(1, &[9], Expected::EndOfInput);
assert_eq!(ctx.context_chain, vec![1, 2]);
ctx.record(5, &[7], Expected::Byte(b'x'));
assert_eq!(ctx.furthest, 5);
assert_eq!(ctx.context_chain, vec![7]);
}
#[test]
fn error_context_record_hint_advances_furthest_and_resets_expected() {
let mut ctx = ErrorContext::new();
ctx.record(1, &[], Expected::Byte(b'a'));
assert_eq!(ctx.furthest, 1);
assert_eq!(ctx.expected.len(), 1);
ctx.record_hint(3, &[9], SymbolId(7));
assert_eq!(ctx.furthest, 3);
assert!(ctx.expected.is_empty());
assert_eq!(ctx.context_chain, vec![9]);
assert_eq!(ctx.hints, vec![SymbolId(7)]);
ctx.record_hint(3, &[1], SymbolId(7));
ctx.record_hint(3, &[1], SymbolId(8));
assert_eq!(ctx.hints, vec![SymbolId(7), SymbolId(8)]);
}
}