use alloc::vec::Vec;
use core::fmt;
use crate::expr::ExprRef;
use crate::expr::capability::ExprDialect;
use vize_l0::Span;
use scan::{Tok, scan};
mod scan;
pub const DIALECT: &str = "moonbit";
#[derive(Debug, Clone, Copy, Default)]
pub struct MoonBitDialect;
const BINDERS: [&str; 12] = [
"catch", "fn", "for", "guard", "is", "let", "letrec", "lexmatch", "loop", "match", "try",
"while",
];
fn moonbit_source<'a>(expr: ExprRef<'a>) -> Option<&'a str> {
match expr {
ExprRef::Foreign(foreign) if foreign.dialect == DIALECT => Some(foreign.source),
_ => None,
}
}
pub fn free_names(source: &str) -> (Vec<&str>, bool) {
let Some(tokens) = scan(source) else {
return (Vec::new(), false);
};
let mut names: Vec<&str> = Vec::new();
let mut exact = true;
let mut parens = 0usize;
for (index, token) in tokens.iter().enumerate() {
let before = index.checked_sub(1).and_then(|at| tokens.get(at).copied());
let after = tokens.get(index + 1).copied();
match *token {
Tok::Keyword(word) if BINDERS.contains(&word) => exact = false,
Tok::Arrow | Tok::Punct(b'~') => exact = false,
Tok::Punct(b'(') => parens += 1,
Tok::Punct(b')') => parens = parens.saturating_sub(1),
Tok::Name(name) => {
let member = matches!(before, Some(Tok::Punct(b'.' | b'@') | Tok::PathSep));
let label = matches!(after, Some(Tok::Punct(b'~')))
|| (parens > 0 && matches!(after, Some(Tok::Punct(b'='))));
exact &= !label;
let local = name.starts_with(|c: char| c.is_ascii_lowercase() || c == '_');
if !member && !label && local && !names.contains(&name) {
names.push(name);
}
}
_ => {}
}
}
(names, exact)
}
#[must_use]
pub fn is_handler_path(source: &str) -> bool {
let Some(tokens) = scan(source) else {
return false;
};
let mut expect_name = true;
for token in &tokens {
match (expect_name, token) {
(true, Tok::Name(name)) if name.starts_with(|c: char| c.is_ascii_lowercase()) => {}
(false, Tok::Punct(b'.')) => {}
_ => return false,
}
expect_name = !expect_name;
}
!tokens.is_empty() && !expect_name
}
impl ExprDialect for MoonBitDialect {
fn enumerate_bindings(&self, expr: ExprRef<'_>, each: &mut dyn FnMut(&str)) {
if let Some(source) = moonbit_source(expr) {
free_names(source).0.into_iter().for_each(each);
}
}
fn bindings_are_exact(&self, expr: ExprRef<'_>) -> bool {
moonbit_source(expr).is_some_and(|source| free_names(source).1)
}
fn is_constant(&self, expr: ExprRef<'_>) -> bool {
moonbit_source(expr).and_then(scan).is_some_and(|tokens| {
tokens.contains(&Tok::Literal)
&& tokens.iter().all(|token| {
matches!(
token,
Tok::Literal | Tok::Punct(b'+' | b'-' | b'*' | b'/' | b'%' | b'(' | b')')
)
})
})
}
fn map_span(&self, expr: ExprRef<'_>, inner: Span) -> Span {
let span = expr.span();
let verbatim = u32::try_from(expr.source().len()).is_ok_and(|len| len == span.len());
if verbatim
&& expr
.source()
.get(inner.start as usize..inner.end as usize)
.is_some()
{
Span::new(span.start + inner.start, span.start + inner.end)
} else {
span
}
}
fn emit(&self, expr: ExprRef<'_>, out: &mut dyn fmt::Write) -> Result<(), fmt::Error> {
match expr {
ExprRef::Foreign(foreign) if foreign.dialect == DIALECT => {
out.write_str(foreign.source)
}
ExprRef::Opaque(opaque) => out.write_str(opaque.source),
ExprRef::Js(_) | ExprRef::Filter(_) | ExprRef::Foreign(_) => Err(fmt::Error),
}
}
}