use std::{
collections::HashSet,
fmt::{self, Display, Formatter}
};
use crate::{
CompilationError, Parser, Validator,
ast::{
ArithmeticExpression, DiceExpression, Expression, Function, Parameter
},
parser::ParseError,
span::SourceSpan
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticKind
{
UnclosedDelimiter
{
opener: char,
expected_closer: char
},
UnopenedDelimiter
{
closer: char
},
MissingRightOperand
{
operator: char
},
MissingLeftOperand
{
operator: char
},
BareIdentifier,
MissingDiceFaces,
IncompleteDropClause,
IncompleteParameterDefinition,
TrailingInput,
EmptyExpression,
UnexpectedToken,
UnexpectedEof,
DuplicateParameter
{
name: String
},
BindingCollidesWithParameter
{
name: String
},
DuplicateBinding
{
name: String
},
UseBeforeBind
{
name: String
}
}
impl Display for DiagnosticKind
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result
{
match self
{
Self::UnclosedDelimiter { opener, .. } =>
{
write!(f, "unclosed `{}`", opener)
},
Self::UnopenedDelimiter { closer } =>
{
write!(f, "unexpected `{}`", closer)
},
Self::MissingRightOperand { operator } =>
{
write!(f, "missing right operand of `{}`", operator)
},
Self::MissingLeftOperand { operator } =>
{
write!(f, "missing left operand of `{}`", operator)
},
Self::BareIdentifier => write!(f, "bare identifier"),
Self::MissingDiceFaces => write!(f, "missing dice faces"),
Self::IncompleteDropClause =>
{
write!(f, "incomplete drop clause")
},
Self::IncompleteParameterDefinition =>
{
write!(f, "incomplete parameter definition")
},
Self::TrailingInput => write!(f, "trailing input"),
Self::EmptyExpression => write!(f, "empty expression"),
Self::UnexpectedToken => write!(f, "unexpected token"),
Self::UnexpectedEof => write!(f, "unexpected end of input"),
Self::DuplicateParameter { name } =>
{
write!(f, "duplicate parameter `{}`", name)
},
Self::BindingCollidesWithParameter { name } =>
{
write!(
f,
"local binding `{}` collides with formal parameter",
name
)
},
Self::DuplicateBinding { name } =>
{
write!(f, "duplicate local binding `{}`", name)
},
Self::UseBeforeBind { name } =>
{
write!(f, "reference to `{}` precedes its binding", name)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic
{
pub kind: DiagnosticKind,
pub span: SourceSpan,
pub message: String,
pub related: Vec<RelatedLabel>,
pub suggestions: Vec<Suggestion>
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelatedLabel
{
pub span: SourceSpan,
pub message: String
}
impl Display for Diagnostic
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result
{
write!(f, "{} ({}): {}", self.kind, self.span, self.message)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Suggestion
{
pub description: String,
pub corrected_source: String,
pub placeholders: Vec<Placeholder>
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Placeholder
{
pub span: SourceSpan,
pub description: &'static str,
pub valid_kinds: &'static [&'static str]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnoseResult
{
pub diagnostics: Vec<Diagnostic>,
pub corrected_source: Option<String>
}
const OPERAND_KINDS: &[&str] =
&["integer", "{variable}", "(expression)", "dice expression"];
const FACE_COUNT_KINDS: &[&str] = &["integer", "{variable}", "(expression)"];
const DROP_DIRECTION_KINDS: &[&str] = &["lowest", "highest"];
fn analyze_error(
source: &str,
error: &ParseError<'_>,
offset_map: &OffsetMap
) -> Diagnostic
{
let rightmost = &error.errors[0];
let pos = rightmost.0.location_offset();
let at_eof =
pos >= source.len() || source[pos..].chars().all(char::is_whitespace);
let expectations = error
.errors
.iter()
.take_while(|(span, _)| span.location_offset() == pos)
.flat_map(|(_, kind)| kind.expectations())
.collect::<Vec<_>>();
let expects_expression = expectations.iter().any(|e| {
let s = e.as_ref();
s == "integer"
|| s == "dice expression"
|| s == "`(`"
|| s == "`{`"
|| s == "`[`"
|| s == "`-`"
});
let expects_delimiter =
expectations.iter().find_map(|e| match e.as_ref()
{
"`)`" => Some(('(', ')')),
"`]`" => Some(('[', ']')),
"`}`" => Some(('{', '}')),
_ => None
});
if at_eof && let Some((opener, closer)) = expects_delimiter
{
let opener_pos = find_unmatched_opener(source, opener, closer, pos);
return make_unclosed_delimiter(
source, opener, closer, opener_pos, pos, offset_map
);
}
if expects_expression
&& !at_eof
&& let Some(diag) =
detect_bare_identifier_at_pos(source, pos, offset_map)
{
return diag;
}
let expects_identifier =
expectations.iter().any(|e| e.as_ref() == "identifier");
if (expects_expression || expects_identifier)
&& let Some((prev, prev_pos)) = find_preceding_char(source, pos)
{
if prev == 'd' || prev == 'D'
{
return make_missing_dice_faces(source, prev_pos, pos, offset_map);
}
if "+-*×/÷%^".contains(prev)
{
return make_missing_right_operand(
source, prev, prev_pos, pos, offset_map
);
}
if prev == '(' || prev == '[' || prev == '{'
{
return make_incomplete_delimited(
source, prev, prev_pos, pos, offset_map
);
}
if prev == ':'
&& let Some(bracket_pos) = source[..prev_pos].rfind('[')
{
let orig_bracket = offset_map.to_original(bracket_pos);
let mut corrected = source[..pos].to_string();
let p_start = corrected.len();
corrected.push('0');
let p_end = corrected.len();
corrected.push(']');
corrected.push_str(&source[pos..]);
return Diagnostic {
kind: DiagnosticKind::UnclosedDelimiter {
opener: '[',
expected_closer: ']'
},
span: SourceSpan {
start: orig_bracket,
end: orig_bracket + 1
},
message: "expected `]` to close `[`".into(),
related: vec![],
suggestions: vec![Suggestion {
description: "insert range end \
and `]`"
.into(),
corrected_source: corrected,
placeholders: vec![Placeholder {
span: SourceSpan {
start: p_start,
end: p_end
},
description: "range end",
valid_kinds: OPERAND_KINDS
}]
}]
};
}
}
if expectations
.iter()
.any(|e| e.as_ref() == "`lowest`" || e.as_ref() == "`highest`")
{
let drop_pos =
source[..pos].rfind("drop").unwrap_or(pos.saturating_sub(5));
let orig_drop = offset_map.to_original(drop_pos);
let orig_end = offset_map.to_original((drop_pos + 4).min(source.len()));
return make_incomplete_drop(
source, orig_drop, orig_end, pos, offset_map
);
}
if !at_eof
{
let ch = source[pos..].chars().next().unwrap_or('\0');
if "+-*×/÷%^".contains(ch) && pos == 0
{
return make_missing_left_operand(source, ch, offset_map);
}
}
if source.trim().is_empty()
{
return make_empty_expression(source);
}
if let Some(diag) = detect_bare_identifier(source, pos, offset_map)
{
return diag;
}
if expectations
.iter()
.any(|e| e.as_ref() == "`,`" || e.as_ref() == "`:`")
&& let Some(diag) = detect_incomplete_parameter(source, pos, offset_map)
{
return diag;
}
if expects_expression && !at_eof
{
let ch = source[pos..].chars().next().unwrap_or('\0');
if ch == ')' || ch == ']' || ch == '}'
{
return make_unopened_delimiter(source, ch, pos, offset_map);
}
}
if expectations.iter().any(|e| e.as_ref() == "end of input") && !at_eof
{
let orig_pos = offset_map.to_original(pos);
let orig_end = offset_map.to_original(source.len());
return Diagnostic {
kind: DiagnosticKind::TrailingInput,
span: SourceSpan {
start: orig_pos,
end: orig_end
},
message: format!(
"unexpected `{}` after expression",
&source[pos..].split_whitespace().next().unwrap_or("")
),
related: vec![],
suggestions: vec![Suggestion {
description: "remove trailing input".into(),
corrected_source: source[..pos].trim_end().to_string(),
placeholders: vec![]
}]
};
}
let orig_pos = offset_map.to_original(pos);
if at_eof
{
Diagnostic {
kind: DiagnosticKind::UnexpectedEof,
span: SourceSpan {
start: orig_pos,
end: orig_pos
},
message: "unexpected end of input".into(),
related: vec![],
suggestions: vec![]
}
}
else
{
let token_end = source[pos..]
.find(|c: char| c.is_whitespace())
.map_or(source.len(), |i| pos + i);
let orig_end = offset_map.to_original(token_end);
Diagnostic {
kind: DiagnosticKind::UnexpectedToken,
span: SourceSpan {
start: orig_pos,
end: orig_end
},
message: format!("unexpected `{}`", &source[pos..token_end]),
related: vec![],
suggestions: vec![]
}
}
}
fn find_preceding_char(source: &str, pos: usize) -> Option<(char, usize)>
{
let before = &source[..pos];
for (i, c) in before.char_indices().rev()
{
if !c.is_whitespace()
{
return Some((c, i));
}
}
None
}
fn find_unmatched_opener(
source: &str,
opener: char,
closer: char,
pos: usize
) -> Option<usize>
{
let mut depth = 0i32;
let mut last_opener = None;
for (i, c) in source[..pos].char_indices()
{
if c == opener
{
depth += 1;
last_opener = Some(i);
}
else if c == closer
{
depth -= 1;
}
}
if depth > 0 { last_opener } else { None }
}
fn detect_bare_identifier_at_pos(
source: &str,
pos: usize,
offset_map: &OffsetMap
) -> Option<Diagnostic>
{
let remaining = &source[pos..];
let first = remaining.chars().next()?;
if !first.is_alphabetic() && first != '_'
{
return None;
}
let ident_end = remaining
.find(|c: char| {
!(c.is_alphanumeric()
|| c == '_' || c == '-'
|| c == '.' || (c.is_whitespace() && !matches!(c, '\n' | '\r')))
})
.unwrap_or(remaining.len());
let name = remaining[..ident_end].trim_end();
if name.is_empty()
{
return None;
}
let orig_start = offset_map.to_original(pos);
let orig_end = offset_map.to_original(pos + name.len());
let after = &source[pos + ident_end..];
let mut suggestions = Vec::new();
let split_pos = name.char_indices().find(|&(i, c)| {
(c == 'd' || c == 'D')
&& i > 0 && name[..i]
.starts_with(|c: char| c.is_alphabetic() || c == '_')
});
if let Some((d_offset, _)) = split_pos
{
let prefix = &name[..d_offset];
let suffix = &name[d_offset..];
let mut split_fix = source[..pos].to_string();
split_fix.push('{');
split_fix.push_str(prefix);
split_fix.push('}');
split_fix.push_str(suffix);
split_fix.push_str(after);
suggestions.push(Suggestion {
description: format!("wrap `{}` in braces", prefix),
corrected_source: split_fix,
placeholders: vec![]
});
}
let mut whole_fix = source[..pos].to_string();
whole_fix.push('{');
whole_fix.push_str(name);
whole_fix.push('}');
whole_fix.push_str(after);
suggestions.push(Suggestion {
description: format!("use `{}` as a variable name", name),
corrected_source: whole_fix,
placeholders: vec![]
});
let bare_name = split_pos.map(|(i, _)| &name[..i]).unwrap_or(name);
Some(Diagnostic {
kind: DiagnosticKind::BareIdentifier,
span: SourceSpan {
start: orig_start,
end: orig_end
},
message: format!(
"bare identifier `{}` is not valid here; \
variables must be wrapped in `{{}}`",
bare_name
),
related: vec![],
suggestions
})
}
fn detect_incomplete_parameter(
source: &str,
pos: usize,
offset_map: &OffsetMap
) -> Option<Diagnostic>
{
let trimmed = source.trim();
if trimmed.is_empty()
{
return None;
}
let params_text = source[..pos].trim();
if params_text.is_empty()
{
return None;
}
let params: Vec<&str> = params_text
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if params.is_empty()
|| !params
.iter()
.all(|p| p.starts_with(|c: char| c.is_alphabetic() || c == '_'))
{
return None;
}
let orig_start = offset_map.to_original(0);
let orig_end = offset_map.to_original(pos);
let mut suggestions = Vec::new();
let trailing = source[pos..].trim_start();
let var_suggestion = if params.len() == 1
{
let name = params[0];
let mut var_fix = format!("{{{}}}", name);
if !trailing.is_empty()
{
var_fix.push(' ');
var_fix.push_str(trailing);
}
Some(Suggestion {
description: format!("use `{}` as a variable reference", name),
corrected_source: var_fix,
placeholders: vec![]
})
}
else
{
None
};
let param_suggestion = {
let mut fix = source[..pos].to_string();
fix.push_str(": ");
let trailing_works = if !trailing.is_empty()
{
let mut candidate = fix.clone();
candidate.push_str(trailing);
Parser::parse(&candidate).is_ok()
}
else
{
false
};
if trailing_works
{
fix.push_str(trailing);
Suggestion {
description: "complete parameter definition".into(),
corrected_source: fix,
placeholders: vec![]
}
}
else
{
let placeholder_start = fix.len();
fix.push('0');
let placeholder_end = fix.len();
Suggestion {
description: "complete parameter definition".into(),
corrected_source: fix,
placeholders: vec![Placeholder {
span: SourceSpan {
start: placeholder_start,
end: placeholder_end
},
description: "expression",
valid_kinds: OPERAND_KINDS
}]
}
}
};
if !trailing.is_empty()
{
if let Some(vs) = var_suggestion
{
suggestions.push(vs);
}
suggestions.push(param_suggestion);
}
else
{
suggestions.push(param_suggestion);
if let Some(vs) = var_suggestion
{
suggestions.push(vs);
}
}
Some(Diagnostic {
kind: DiagnosticKind::IncompleteParameterDefinition,
span: SourceSpan {
start: orig_start,
end: orig_end
},
message: format!(
"expected `:` and expression body after parameter{}",
if params.len() > 1 { "s" } else { "" }
),
related: vec![],
suggestions
})
}
fn detect_bare_identifier(
source: &str,
error_pos: usize,
offset_map: &OffsetMap
) -> Option<Diagnostic>
{
let before = &source[..error_pos];
let ident_start = before
.char_indices()
.rev()
.take_while(|(_, c)| {
c.is_alphanumeric() || *c == '_' || *c == '-' || *c == '.'
})
.last()
.map(|(i, _)| i)?;
let ident = &source[ident_start..error_pos];
for (i, c) in ident.char_indices()
{
if (c == 'd' || c == 'D') && i > 0
{
let after_d = &ident[i + 1..];
if after_d.is_empty()
|| after_d.starts_with(|c: char| c.is_ascii_digit())
|| after_d.starts_with('[')
|| after_d.starts_with('(')
|| after_d.starts_with('{')
{
let name = &ident[..i];
if name.starts_with(|c: char| c.is_alphabetic() || c == '_')
{
let orig_start = offset_map.to_original(ident_start);
let orig_end = offset_map.to_original(ident_start + i);
let after = &source[ident_start + i..];
let mut split_fix = source[..ident_start].to_string();
split_fix.push('{');
split_fix.push_str(name);
split_fix.push('}');
split_fix.push_str(after);
let mut whole_fix = source[..ident_start].to_string();
whole_fix.push('{');
whole_fix.push_str(ident);
whole_fix.push('}');
whole_fix.push_str(&source[error_pos..]);
return Some(Diagnostic {
kind: DiagnosticKind::BareIdentifier,
span: SourceSpan {
start: orig_start,
end: orig_end
},
message: format!(
"bare identifier `{}` is not valid here; \
variables must be wrapped in `{{}}`",
name
),
related: vec![],
suggestions: vec![
Suggestion {
description: format!(
"wrap `{}` in braces",
name
),
corrected_source: split_fix,
placeholders: vec![]
},
Suggestion {
description: format!(
"use `{}` as a variable \
name",
ident
),
corrected_source: whole_fix,
placeholders: vec![]
},
]
});
}
}
}
}
None
}
fn make_unclosed_delimiter(
source: &str,
opener: char,
closer: char,
opener_pos: Option<usize>,
error_pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let actual_opener = opener_pos.unwrap_or(0);
let orig_opener = offset_map.to_original(actual_opener);
let mut corrected = source[..error_pos].to_string();
corrected.push(closer);
corrected.push_str(&source[error_pos..]);
Diagnostic {
kind: DiagnosticKind::UnclosedDelimiter {
opener,
expected_closer: closer
},
span: SourceSpan {
start: orig_opener,
end: orig_opener + opener.len_utf8()
},
message: format!("expected `{}` to close `{}`", closer, opener),
related: vec![],
suggestions: vec![Suggestion {
description: format!("insert `{}`", closer),
corrected_source: corrected,
placeholders: vec![]
}]
}
}
fn make_missing_right_operand(
source: &str,
operator: char,
op_pos: usize,
error_pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let orig_error_pos = offset_map.to_original(error_pos);
let orig_op_pos = offset_map.to_original(op_pos);
let mut insert_fix = source[..error_pos].to_string();
if !insert_fix.is_empty() && !insert_fix.ends_with(' ')
{
insert_fix.push(' ');
}
let placeholder_start = insert_fix.len();
insert_fix.push('0');
let placeholder_end = insert_fix.len();
if !source[error_pos..].is_empty() && !source[error_pos..].starts_with(' ')
{
insert_fix.push(' ');
}
insert_fix.push_str(&source[error_pos..]);
let before_op = source[..op_pos].trim_end();
let mut drop_fix = before_op.to_string();
if !source[error_pos..].is_empty()
{
if !drop_fix.is_empty()
{
drop_fix.push(' ');
}
drop_fix.push_str(source[error_pos..].trim_start());
}
Diagnostic {
kind: DiagnosticKind::MissingRightOperand { operator },
span: SourceSpan {
start: orig_op_pos,
end: orig_error_pos
},
message: format!("expected operand after `{}`", operator),
related: vec![],
suggestions: vec![
Suggestion {
description: format!("insert operand after `{}`", operator),
corrected_source: insert_fix,
placeholders: vec![Placeholder {
span: SourceSpan {
start: placeholder_start,
end: placeholder_end
},
description: "operand",
valid_kinds: OPERAND_KINDS
}]
},
Suggestion {
description: format!("remove `{}`", operator),
corrected_source: drop_fix,
placeholders: vec![]
},
]
}
}
fn make_missing_left_operand(
source: &str,
operator: char,
offset_map: &OffsetMap
) -> Diagnostic
{
let orig_pos = offset_map.to_original(0);
let mut insert_fix = String::from("0 ");
insert_fix.push_str(source);
let after_op = source[operator.len_utf8()..].trim_start();
let drop_fix = after_op.to_string();
Diagnostic {
kind: DiagnosticKind::MissingLeftOperand { operator },
span: SourceSpan {
start: orig_pos,
end: orig_pos + operator.len_utf8()
},
message: format!("expected operand before `{}`", operator),
related: vec![],
suggestions: vec![
Suggestion {
description: format!("insert operand before `{}`", operator),
corrected_source: insert_fix,
placeholders: vec![Placeholder {
span: SourceSpan { start: 0, end: 1 },
description: "operand",
valid_kinds: OPERAND_KINDS
}]
},
Suggestion {
description: format!("remove `{}`", operator),
corrected_source: drop_fix,
placeholders: vec![]
},
]
}
}
fn make_incomplete_delimited(
source: &str,
opener: char,
opener_pos: usize,
error_pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let orig_opener = offset_map.to_original(opener_pos);
let closer = match opener
{
'(' => ')',
'[' => ']',
'{' => '}',
_ => ')'
};
let mut corrected = source[..error_pos].to_string();
let mut placeholders = Vec::new();
let is_custom_faces = opener == '['
&& opener_pos > 0
&& source[..opener_pos]
.chars()
.last()
.is_some_and(|c| c == 'd' || c == 'D');
match opener
{
'[' if is_custom_faces =>
{
let p_start = corrected.len();
corrected.push('0');
let p_end = corrected.len();
placeholders.push(Placeholder {
span: SourceSpan {
start: p_start,
end: p_end
},
description: "face value",
valid_kinds: &["integer"]
});
},
'[' =>
{
let p1_start = corrected.len();
corrected.push('0');
let p1_end = corrected.len();
corrected.push(':');
let p2_start = corrected.len();
corrected.push('0');
let p2_end = corrected.len();
placeholders.push(Placeholder {
span: SourceSpan {
start: p1_start,
end: p1_end
},
description: "range start",
valid_kinds: OPERAND_KINDS
});
placeholders.push(Placeholder {
span: SourceSpan {
start: p2_start,
end: p2_end
},
description: "range end",
valid_kinds: OPERAND_KINDS
});
},
'{' =>
{
let p_start = corrected.len();
corrected.push('x');
let p_end = corrected.len();
placeholders.push(Placeholder {
span: SourceSpan {
start: p_start,
end: p_end
},
description: "identifier",
valid_kinds: &["identifier"]
});
},
_ =>
{
let p_start = corrected.len();
corrected.push('0');
let p_end = corrected.len();
placeholders.push(Placeholder {
span: SourceSpan {
start: p_start,
end: p_end
},
description: "expression",
valid_kinds: OPERAND_KINDS
});
}
}
corrected.push(closer);
corrected.push_str(&source[error_pos..]);
Diagnostic {
kind: DiagnosticKind::UnclosedDelimiter {
opener,
expected_closer: closer
},
span: SourceSpan {
start: orig_opener,
end: orig_opener + opener.len_utf8()
},
message: format!("expected `{}` to close `{}`", closer, opener),
related: vec![],
suggestions: vec![Suggestion {
description: format!("insert expression and `{}`", closer),
corrected_source: corrected,
placeholders
}]
}
}
fn make_missing_dice_faces(
source: &str,
d_pos: usize,
_error_pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let orig_d_pos = offset_map.to_original(d_pos);
let insert_pos = d_pos + 1;
let mut corrected = source[..insert_pos].to_string();
let placeholder_start = corrected.len();
corrected.push('6');
let placeholder_end = corrected.len();
corrected.push_str(&source[insert_pos..]);
Diagnostic {
kind: DiagnosticKind::MissingDiceFaces,
span: SourceSpan {
start: orig_d_pos,
end: orig_d_pos + 1
},
message: format!(
"expected face count after `{}`",
&source[d_pos..d_pos + 1]
),
related: vec![],
suggestions: vec![Suggestion {
description: "insert face count".into(),
corrected_source: corrected,
placeholders: vec![Placeholder {
span: SourceSpan {
start: placeholder_start,
end: placeholder_end
},
description: "face count",
valid_kinds: FACE_COUNT_KINDS
}]
}]
}
}
fn make_incomplete_drop(
source: &str,
orig_drop_start: usize,
orig_drop_end: usize,
error_pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let _ = offset_map;
let insert_pos = source[..error_pos]
.rfind("drop")
.map_or(error_pos, |p| p + 4);
let mut corrected = source[..insert_pos].to_string();
corrected.push(' ');
let placeholder_start = corrected.len();
corrected.push_str("lowest");
let placeholder_end = corrected.len();
corrected.push_str(&source[insert_pos..]);
Diagnostic {
kind: DiagnosticKind::IncompleteDropClause,
span: SourceSpan {
start: orig_drop_start,
end: orig_drop_end
},
message: "expected `lowest` or `highest` after `drop`".into(),
related: vec![],
suggestions: vec![Suggestion {
description: "insert drop direction".into(),
corrected_source: corrected,
placeholders: vec![Placeholder {
span: SourceSpan {
start: placeholder_start,
end: placeholder_end
},
description: "direction",
valid_kinds: DROP_DIRECTION_KINDS
}]
}]
}
}
fn make_unopened_delimiter(
source: &str,
closer: char,
pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let orig_pos = offset_map.to_original(pos);
let mut corrected = source[..pos].to_string();
corrected.push_str(&source[pos + closer.len_utf8()..]);
Diagnostic {
kind: DiagnosticKind::UnopenedDelimiter { closer },
span: SourceSpan {
start: orig_pos,
end: orig_pos + closer.len_utf8()
},
message: format!("unexpected `{}` with no matching opener", closer),
related: vec![],
suggestions: vec![Suggestion {
description: format!("remove the unopened `{}`", closer),
corrected_source: corrected,
placeholders: vec![]
}]
}
}
fn make_empty_expression(source: &str) -> Diagnostic
{
let _ = source;
Diagnostic {
kind: DiagnosticKind::EmptyExpression,
span: SourceSpan { start: 0, end: 0 },
message: "expected expression".into(),
related: vec![],
suggestions: vec![Suggestion {
description: "insert expression".into(),
corrected_source: "0".into(),
placeholders: vec![Placeholder {
span: SourceSpan { start: 0, end: 1 },
description: "expression",
valid_kinds: OPERAND_KINDS
}]
}]
}
}
fn run_validator<'src>(
source: &'src str,
ast: &Function<'src>
) -> Vec<Diagnostic>
{
match Validator::validate(ast)
{
Ok(()) => Vec::new(),
Err(error) => vec![analyze_semantic_error(source, ast, error)]
}
}
fn analyze_semantic_error<'src>(
source: &'src str,
ast: &Function<'src>,
error: CompilationError<'src>
) -> Diagnostic
{
match error
{
CompilationError::DuplicateParameter {
name,
first,
duplicate
} => make_duplicate_parameter(source, ast, name, first, duplicate),
CompilationError::BindingCollidesWithParameter {
name,
parameter,
binding
} => make_binding_collides_with_parameter(
source, ast, name, parameter, binding
),
CompilationError::DuplicateBinding {
name,
first,
duplicate
} => make_duplicate_binding(source, ast, name, first, duplicate),
CompilationError::UseBeforeBind {
name,
reference,
binding
} => make_use_before_bind(source, ast, name, reference, binding),
CompilationError::ParseError(_)
| CompilationError::OptimizationFailed =>
{
unreachable!(
"Validator::validate produces only semantic errors; \
ParseError is produced by the parser and OptimizationFailed \
by the optimizer, neither of which is reached from \
diagnose() at this point"
)
}
}
}
fn make_binding_collides_with_parameter<'src>(
source: &'src str,
ast: &Function<'src>,
name: &'src str,
parameter: SourceSpan,
binding: SourceSpan
) -> Diagnostic
{
let used = collect_in_use_names(ast);
let fresh = suggest_rename_with_pool(name, &used);
let corrected = splice(source, binding, &fresh);
Diagnostic {
kind: DiagnosticKind::BindingCollidesWithParameter {
name: name.to_string()
},
span: binding,
message: format!(
"local binding `{}` collides with a formal parameter of the same \
name; bindings, parameters, and environment variables share one \
flat namespace per function",
name
),
related: vec![RelatedLabel {
span: parameter,
message: "declared as a parameter here".into()
}],
suggestions: vec![Suggestion {
description: format!("rename local binding to `{}`", fresh),
corrected_source: corrected,
placeholders: vec![]
}]
}
}
fn make_duplicate_binding<'src>(
source: &'src str,
ast: &Function<'src>,
name: &'src str,
first: SourceSpan,
duplicate: SourceSpan
) -> Diagnostic
{
let used = collect_in_use_names(ast);
let fresh = suggest_rename_with_pool(name, &used);
let corrected = splice(source, duplicate, &fresh);
Diagnostic {
kind: DiagnosticKind::DuplicateBinding {
name: name.to_string()
},
span: duplicate,
message: format!(
"local binding `{}` is bound more than once; a function body \
provides a single flat namespace, so rebinding is not permitted",
name
),
related: vec![RelatedLabel {
span: first,
message: "first bound here".into()
}],
suggestions: vec![Suggestion {
description: format!("rename duplicate binding to `{}`", fresh),
corrected_source: corrected,
placeholders: vec![]
}]
}
}
fn make_use_before_bind<'src>(
source: &'src str,
ast: &Function<'src>,
name: &'src str,
reference: SourceSpan,
binding: SourceSpan
) -> Diagnostic
{
let used = collect_in_use_names(ast);
let fresh = suggest_rename_with_pool(name, &used);
let corrected = splice(source, binding, &fresh);
Diagnostic {
kind: DiagnosticKind::UseBeforeBind {
name: name.to_string()
},
span: reference,
message: format!(
"reference to `{}` precedes its binding; references to a local \
binding must lexically follow the binding site, including \
references inside the bound expression itself (self-reference \
is not permitted)",
name
),
related: vec![RelatedLabel {
span: binding,
message: "bound here".into()
}],
suggestions: vec![Suggestion {
description: format!("rename local binding to `{}`", fresh),
corrected_source: corrected,
placeholders: vec![]
}]
}
}
fn splice(source: &str, span: SourceSpan, replacement: &str) -> String
{
let mut corrected = String::with_capacity(
source.len() + replacement.len() - (span.end - span.start)
);
corrected.push_str(&source[..span.start]);
corrected.push_str(replacement);
corrected.push_str(&source[span.end..]);
corrected
}
fn collect_in_use_names<'src>(ast: &Function<'src>) -> HashSet<&'src str>
{
let mut names: HashSet<&'src str> = HashSet::new();
if let Some(ref parameters) = ast.parameters
{
for param in parameters
{
names.insert(param.name);
}
}
gather_expression_names(&ast.body, &mut names);
names
}
fn gather_expression_names<'src>(
expr: &Expression<'src>,
out: &mut HashSet<&'src str>
)
{
match expr
{
Expression::Variable(v) =>
{
out.insert(v.name);
},
Expression::Binding(b) =>
{
out.insert(b.name);
gather_expression_names(&b.expression, out);
},
Expression::Group(g) => gather_expression_names(&g.expression, out),
Expression::Range(r) =>
{
gather_expression_names(&r.start, out);
gather_expression_names(&r.end, out);
},
Expression::Dice(d) => gather_dice_names(d, out),
Expression::Arithmetic(a) => gather_arithmetic_names(a, out),
Expression::Constant(_) =>
{}
}
}
fn gather_dice_names<'src>(
dice: &DiceExpression<'src>,
out: &mut HashSet<&'src str>
)
{
match dice
{
DiceExpression::Standard(d) =>
{
gather_expression_names(&d.count, out);
gather_expression_names(&d.faces, out);
},
DiceExpression::Custom(d) => gather_expression_names(&d.count, out),
DiceExpression::DropLowest(d) =>
{
gather_dice_names(&d.dice, out);
if let Some(ref drop) = d.drop
{
gather_expression_names(drop, out);
}
},
DiceExpression::DropHighest(d) =>
{
gather_dice_names(&d.dice, out);
if let Some(ref drop) = d.drop
{
gather_expression_names(drop, out);
}
}
}
}
fn gather_arithmetic_names<'src>(
arith: &ArithmeticExpression<'src>,
out: &mut HashSet<&'src str>
)
{
match arith
{
ArithmeticExpression::Add(a) =>
{
gather_expression_names(&a.left, out);
gather_expression_names(&a.right, out);
},
ArithmeticExpression::Sub(s) =>
{
gather_expression_names(&s.left, out);
gather_expression_names(&s.right, out);
},
ArithmeticExpression::Mul(m) =>
{
gather_expression_names(&m.left, out);
gather_expression_names(&m.right, out);
},
ArithmeticExpression::Div(d) =>
{
gather_expression_names(&d.left, out);
gather_expression_names(&d.right, out);
},
ArithmeticExpression::Mod(m) =>
{
gather_expression_names(&m.left, out);
gather_expression_names(&m.right, out);
},
ArithmeticExpression::Exp(e) =>
{
gather_expression_names(&e.left, out);
gather_expression_names(&e.right, out);
},
ArithmeticExpression::Neg(n) => gather_expression_names(&n.operand, out)
}
}
fn make_duplicate_parameter<'src>(
source: &'src str,
ast: &Function<'src>,
name: &'src str,
first: SourceSpan,
duplicate: SourceSpan
) -> Diagnostic
{
let parameters = ast.parameters.as_deref().unwrap_or(&[]);
let fresh = suggest_rename(name, parameters);
let corrected = splice(source, duplicate, &fresh);
Diagnostic {
kind: DiagnosticKind::DuplicateParameter {
name: name.to_string()
},
span: duplicate,
message: format!(
"parameter `{}` is declared more than once; review references \
to `{}` in the body — one may have meant a different parameter \
or an external variable",
name, name
),
related: vec![RelatedLabel {
span: first,
message: "first declared here".into()
}],
suggestions: vec![Suggestion {
description: format!("rename duplicate parameter to `{}`", fresh),
corrected_source: corrected,
placeholders: vec![]
}]
}
}
fn suggest_rename(duplicate: &str, parameters: &[Parameter<'_>]) -> String
{
let used: HashSet<&str> = parameters.iter().map(|p| p.name).collect();
suggest_rename_with_pool(duplicate, &used)
}
fn suggest_rename_with_pool(duplicate: &str, used: &HashSet<&str>) -> String
{
let mut chars = duplicate.chars();
if let (Some(c), None) = (chars.next(), chars.next())
&& c.is_ascii_alphabetic()
{
let start = if c.is_ascii_uppercase() { b'A' } else { b'a' };
let highest = used
.iter()
.filter_map(|n| {
let mut cs = n.chars();
match (cs.next(), cs.next())
{
(Some(ch), None)
if ch.is_ascii_alphabetic()
&& ch.is_ascii_uppercase()
== c.is_ascii_uppercase() =>
{
Some(ch as u8)
},
_ => None
}
})
.max()
.unwrap_or(c as u8);
for offset in 1u8..26
{
let code = start + ((highest - start + offset) % 26);
let candidate = (code as char).to_string();
if !used.contains(candidate.as_str())
{
return candidate;
}
}
}
for i in 0usize..
{
let candidate = format!("new{}", i);
if !used.contains(candidate.as_str())
{
return candidate;
}
}
unreachable!("cannot exhaust usize worth of `newN` candidates")
}
#[derive(Debug, Clone)]
struct OffsetMap
{
adjustments: Vec<(usize, isize)>
}
impl OffsetMap
{
fn new() -> Self
{
Self {
adjustments: Vec::new()
}
}
fn record(&mut self, modified_pos: usize, delta: isize)
{
self.adjustments.push((modified_pos, delta));
}
fn to_original(&self, modified_pos: usize) -> usize
{
let mut pos = modified_pos as isize;
for &(adj_pos, delta) in self.adjustments.iter().rev()
{
if modified_pos >= adj_pos
{
pos -= delta;
}
}
pos.max(0) as usize
}
}
#[cfg_attr(doc, aquamarine::aquamarine)]
pub fn diagnose(source: &str) -> DiagnoseResult
{
if let Ok(ast) = Parser::parse(source)
{
return DiagnoseResult {
diagnostics: run_validator(source, &ast),
corrected_source: Some(source.to_string())
};
}
let mut current_source = source.to_string();
let mut diagnostics = Vec::new();
let mut offset_map = OffsetMap::new();
let max_iterations = source.len() + 16;
for _ in 0..max_iterations
{
match Parser::parse(¤t_source)
{
Ok(_) =>
{
return DiagnoseResult {
diagnostics,
corrected_source: Some(current_source)
};
},
Err(error) =>
{
let diag = analyze_error(¤t_source, &error, &offset_map);
if let Some(suggestion) = diag.suggestions.first()
{
let old_len = current_source.len() as isize;
let new_source = suggestion.corrected_source.clone();
let new_len = new_source.len() as isize;
let delta = new_len - old_len;
let error_pos = error.errors[0].0.location_offset();
offset_map.record(error_pos, delta);
current_source = new_source;
diagnostics.push(diag);
}
else
{
diagnostics.push(diag);
return DiagnoseResult {
diagnostics,
corrected_source: None
};
}
}
}
}
DiagnoseResult {
diagnostics,
corrected_source: None
}
}
#[cfg(test)]
mod tests
{
use super::OffsetMap;
#[test]
fn test_offset_map_identity()
{
let map = OffsetMap::new();
assert_eq!(map.to_original(0), 0);
assert_eq!(map.to_original(5), 5);
assert_eq!(map.to_original(100), 100);
}
#[test]
fn test_offset_map_single_insertion()
{
let mut map = OffsetMap::new();
map.record(3, 2); assert_eq!(map.to_original(0), 0);
assert_eq!(map.to_original(2), 2);
assert_eq!(map.to_original(3), 1);
assert_eq!(map.to_original(5), 3);
assert_eq!(map.to_original(8), 6);
}
#[test]
fn test_offset_map_single_deletion()
{
let mut map = OffsetMap::new();
map.record(3, -2); assert_eq!(map.to_original(0), 0);
assert_eq!(map.to_original(2), 2);
assert_eq!(map.to_original(3), 5);
assert_eq!(map.to_original(5), 7);
}
#[test]
fn test_offset_map_multiple_insertions()
{
let mut map = OffsetMap::new();
map.record(4, 2); map.record(12, 2); assert_eq!(map.to_original(0), 0);
assert_eq!(map.to_original(3), 3);
assert_eq!(map.to_original(6), 4);
assert_eq!(map.to_original(13), 9);
}
#[test]
fn test_offset_map_position_before_adjustment()
{
let mut map = OffsetMap::new();
map.record(10, 5); for i in 0..10
{
assert_eq!(map.to_original(i), i);
}
}
#[test]
fn test_offset_map_clamp_to_zero()
{
let mut map = OffsetMap::new();
map.record(0, 10); assert_eq!(map.to_original(0), 0);
assert_eq!(map.to_original(5), 0);
assert_eq!(map.to_original(10), 0);
assert_eq!(map.to_original(11), 1);
}
}