use std::{
ffi::OsString,
iter,
{collections::HashSet, fmt::Debug},
};
use super::error::ParseError;
use crate::{
app,
ast::{
Number,
combine::merge_doc,
pattern::bindings::Bindings as _,
record::{FieldDef, FieldMetadata, Include},
*,
},
combine::CombineAlloc,
files::FileId,
fun,
identifier::LocIdent,
position::{RawSpan, TermPos},
primop_app,
};
use malachite::{
Integer,
base::num::conversion::traits::{FromSciString, FromStringBase},
};
pub struct ParseNumberError;
pub fn parse_number_sci(slice: &str) -> Result<Number, ParseNumberError> {
Number::from_sci_string(slice).ok_or(ParseNumberError)
}
pub fn parse_number_base(base: u8, slice: &str) -> Result<Number, ParseNumberError> {
Ok(Number::from(
Integer::from_string_base(base, slice).ok_or(ParseNumberError)?,
))
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum StringStartDelimiter<'input> {
Standard,
Multiline,
Symbolic(&'input str),
}
impl StringStartDelimiter<'_> {
pub fn is_closed_by(&self, close: &StringEndDelimiter) -> bool {
matches!(
(self, close),
(StringStartDelimiter::Standard, StringEndDelimiter::Standard)
| (StringStartDelimiter::Multiline, StringEndDelimiter::Special)
| (
StringStartDelimiter::Symbolic(_),
StringEndDelimiter::Special
)
)
}
pub fn needs_strip_indent(&self) -> bool {
match self {
StringStartDelimiter::Standard => false,
StringStartDelimiter::Multiline | StringStartDelimiter::Symbolic(_) => true,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum StringEndDelimiter {
Standard,
Special,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ChunkLiteralPart {
Str(String),
Char(char),
}
#[allow(
clippy::large_enum_variant,
reason = "the large variant is the common one"
)]
#[derive(Clone, Debug)]
pub enum LastField<'ast> {
FieldDecl(FieldDecl<'ast>),
Ellipsis,
}
#[derive(Clone, Debug)]
pub enum FieldDecl<'ast> {
Def(FieldDef<'ast>),
Include(Include<'ast>),
IncludeList(Vec<Include<'ast>>),
}
#[derive(Debug, PartialEq, Clone)]
pub enum LastPattern<P> {
Normal(P),
Ellipsis(Option<LocIdent>),
}
pub(super) trait EtaExpand {
fn eta_expand(self, alloc: &AstAlloc, pos: TermPos) -> Node<'_>;
}
pub(super) struct InfixOp(pub(super) primop::PrimOp);
impl EtaExpand for InfixOp {
fn eta_expand(self, alloc: &AstAlloc, pos: TermPos) -> Node<'_> {
match self {
InfixOp(op @ primop::PrimOp::BoolAnd) | InfixOp(op @ primop::PrimOp::BoolOr) => {
let fst_arg = LocIdent::from("x");
let snd_arg = LocIdent::from("y");
fun!(
alloc,
fst_arg,
snd_arg,
app!(
alloc,
primop_app!(alloc, op, builder::var(fst_arg)),
builder::var(snd_arg),
)
.with_pos(pos),
)
.node
}
InfixOp(op @ primop::PrimOp::RecordGet) => {
let fst_arg = LocIdent::new("x");
let snd_arg = LocIdent::new("y");
fun!(
alloc,
fst_arg,
snd_arg,
primop_app!(alloc, op, builder::var(snd_arg), builder::var(fst_arg))
.with_pos(pos),
)
.node
}
InfixOp(op) => {
let vars: Vec<_> = (0..op.arity())
.map(|i| LocIdent::from(format!("x{i}")))
.collect();
let fun_args: Vec<_> = vars.iter().map(|arg| pattern::Pattern::any(*arg)).collect();
let args: Vec<_> = vars.into_iter().map(builder::var).collect();
alloc.fun(fun_args, alloc.prim_op(op, args).spanned(pos))
}
}
}
}
pub(super) enum ExtendedInfixOp {
ReverseApp,
NotEqual,
}
impl EtaExpand for ExtendedInfixOp {
fn eta_expand(self, alloc: &AstAlloc, pos: TermPos) -> Node<'_> {
match self {
ExtendedInfixOp::ReverseApp => {
let fst_arg = LocIdent::from("x");
let snd_arg = LocIdent::from("y");
fun!(
alloc,
fst_arg,
snd_arg,
app!(alloc, builder::var(snd_arg), builder::var(fst_arg)).with_pos(pos),
)
.node
}
ExtendedInfixOp::NotEqual => {
let fst_arg = LocIdent::from("x");
let snd_arg = LocIdent::from("y");
fun!(
alloc,
fst_arg,
snd_arg,
primop_app!(
alloc,
primop::PrimOp::BoolNot,
primop_app!(
alloc,
primop::PrimOp::Eq,
builder::var(fst_arg),
builder::var(snd_arg),
)
.with_pos(pos),
)
.with_pos(pos),
)
.node
}
}
}
}
pub trait AttachToAst<'ast, T> {
fn attach_to_ast(self, alloc: &'ast AstAlloc, ast: Ast<'ast>) -> T;
}
impl<'ast> CombineAlloc<'ast> for FieldMetadata<'ast> {
fn combine(alloc: &'ast AstAlloc, left: Self, right: Self) -> Self {
let priority = match (left.priority, right.priority) {
(MergePriority::Neutral, p) | (p, MergePriority::Neutral) => p,
(p1, p2) => std::cmp::max(p1, p2),
};
FieldMetadata {
doc: merge_doc(left.doc, right.doc),
annotation: CombineAlloc::combine(alloc, left.annotation, right.annotation),
opt: left.opt || right.opt,
not_exported: left.not_exported || right.not_exported,
priority,
}
}
}
impl<'ast> CombineAlloc<'ast> for LetMetadata<'ast> {
fn combine(alloc: &'ast AstAlloc, left: Self, right: Self) -> Self {
LetMetadata {
doc: merge_doc(left.doc, right.doc),
annotation: CombineAlloc::combine(alloc, left.annotation, right.annotation),
}
}
}
impl<'ast> CombineAlloc<'ast> for Annotation<'ast> {
fn combine(alloc: &'ast AstAlloc, left: Self, right: Self) -> Self {
let (typ, leftover) = match (left.typ, right.typ) {
(left_ty @ Some(_), right_ty @ Some(_)) => (left_ty, right_ty),
(left_ty, right_ty) => (left_ty.or(right_ty), None),
};
let contracts: Vec<_> = left
.contracts
.iter()
.cloned()
.chain(leftover)
.chain(right.contracts.iter().cloned())
.collect();
alloc.annotation(typ, contracts)
}
}
impl<'ast> AttachToAst<'ast, Ast<'ast>> for Annotation<'ast> {
fn attach_to_ast(self, alloc: &'ast AstAlloc, ast: Ast<'ast>) -> Ast<'ast> {
if self.is_empty() {
return ast;
}
let pos = ast.pos;
Ast {
node: alloc.annotated(self, ast),
pos,
}
}
}
pub fn mk_access<'ast>(alloc: &'ast AstAlloc, access: Ast<'ast>, root: Ast<'ast>) -> Node<'ast> {
if let Some(label) = access.node.try_str_chunk_as_static_str() {
alloc.prim_op(
primop::PrimOp::RecordStatAccess(LocIdent::new_with_pos(label, access.pos)),
iter::once(root),
)
} else {
alloc.prim_op(primop::PrimOp::RecordGet, [access, root])
}
}
pub fn mk_span(src_id: FileId, l: usize, r: usize) -> RawSpan {
RawSpan {
src_id,
start: (l as u32).into(),
end: (r as u32).into(),
}
}
pub fn mk_pos(src_id: FileId, l: usize, r: usize) -> TermPos {
TermPos::Original(mk_span(src_id, l, r))
}
pub fn mk_let<'ast>(
alloc: &'ast AstAlloc,
rec: bool,
bindings: Vec<LetBinding<'ast>>,
body: Ast<'ast>,
) -> Result<Node<'ast>, ParseError> {
let mut seen_bindings: HashSet<LocIdent> = HashSet::new();
for b in &bindings {
let new_bindings = b.pattern.bindings();
for binding in &new_bindings {
if let Some(old) = seen_bindings.get(&binding.id) {
return Err(ParseError::DuplicateIdentInLetBlock {
ident: binding.id,
prev_ident: *old,
});
}
}
seen_bindings.extend(new_bindings.into_iter().map(|binding| binding.id));
}
Ok(alloc.let_block(bindings, body, rec))
}
pub fn mk_import_based_on_filename(
alloc: &AstAlloc,
path: String,
_span: RawSpan,
) -> Result<Node<'_>, ParseError> {
let path = OsString::from(path);
let format: Option<InputFormat> = InputFormat::from_path(&path);
let format = format.unwrap_or_default();
Ok(alloc.import_path(path, format))
}
pub fn mk_import_explicit(
alloc: &AstAlloc,
path: String,
format: LocIdent,
span: RawSpan,
) -> Result<Node<'_>, ParseError> {
let path = OsString::from(path);
let Ok(format) = format.label().parse::<InputFormat>() else {
return Err(ParseError::InvalidImportFormat { span });
};
Ok(alloc.import_path(path, format))
}
pub fn min_indent(chunks: &[StringChunk<Ast<'_>>]) -> usize {
let mut min: usize = usize::MAX;
let mut current = 0;
let mut start_line = true;
for chunk in chunks.iter() {
match chunk {
StringChunk::Expr(_, _) if start_line => {
if current < min {
min = current;
}
start_line = false;
}
StringChunk::Expr(_, _) => (),
StringChunk::Literal(s) => {
for c in s.chars() {
match c {
' ' | '\t' if start_line => current += 1,
'\n' => {
current = 0;
start_line = true;
}
_ if start_line => {
if current < min {
min = current;
}
start_line = false;
}
_ => (),
}
}
}
}
}
min
}
pub fn strip_indent(chunks: &mut [StringChunk<Ast<'_>>]) {
if chunks.is_empty() {
return;
}
let min = min_indent(chunks);
let mut current = 0;
let mut start_line = true;
let chunks_len = chunks.len();
let mut unindent: Vec<usize> = Vec::new();
let mut expr_on_line: Option<usize> = None;
for (index, chunk) in chunks.iter_mut().enumerate() {
match chunk {
StringChunk::Literal(s) => {
let mut buffer = String::new();
for c in s.chars() {
match c {
' ' | '\t' if start_line && current < min => current += 1,
' ' | '\t' if start_line => {
current += 1;
buffer.push(c);
}
'\n' => {
current = 0;
start_line = true;
expr_on_line = None;
buffer.push(c);
}
c if start_line => {
start_line = false;
buffer.push(c);
}
c => buffer.push(c),
}
}
if index == 0
&& let Some(first_index) = buffer.find('\n')
&& (first_index == 0
|| buffer.as_bytes()[..first_index]
.iter()
.all(|c| *c == b' ' || *c == b'\t'))
{
buffer = String::from(&buffer[(first_index + 1)..]);
}
if index == chunks_len - 1
&& let Some(last_index) = buffer.rfind('\n')
&& (last_index == buffer.len() - 1
|| buffer.as_bytes()[(last_index + 1)..]
.iter()
.all(|c| *c == b' ' || *c == b'\t'))
{
buffer.truncate(last_index);
}
*s = buffer;
}
StringChunk::Expr(_, indent) => {
if start_line {
debug_assert!(current >= min);
debug_assert!(expr_on_line.is_none());
*indent = current - min;
start_line = false;
expr_on_line = Some(index);
} else if let Some(expr_index) = expr_on_line.take() {
unindent.push(expr_index);
}
}
}
}
for index in unindent.into_iter() {
match chunks.get_mut(index) {
Some(StringChunk::Expr(_, indent)) => *indent = 0,
_ => unreachable!(
"all elements in `unindent` should be expressions, but found a literal"
),
}
}
}