forked from AbleOS/holey-bytes
504 lines
17 KiB
Rust
504 lines
17 KiB
Rust
use {
|
|
crate::{
|
|
lexer::{self, TokenKind},
|
|
parser::{self, CommentOr, CtorField, Expr, Poser, Radix, StructField},
|
|
},
|
|
core::fmt,
|
|
};
|
|
|
|
pub fn minify(source: &mut str) -> usize {
|
|
fn needs_space(c: u8) -> bool {
|
|
matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | 127..)
|
|
}
|
|
|
|
let mut writer = source.as_mut_ptr();
|
|
let mut reader = &source[..];
|
|
let mut prev_needs_whitecpace = false;
|
|
let mut prev_needs_newline = false;
|
|
loop {
|
|
let mut token = lexer::Lexer::new(reader).next();
|
|
match token.kind {
|
|
TokenKind::Eof => break,
|
|
TokenKind::CtIdent | TokenKind::Directive => token.start -= 1,
|
|
_ => {}
|
|
}
|
|
|
|
let cpy_len = token.range().len();
|
|
|
|
let mut prefix = 0;
|
|
if prev_needs_whitecpace && needs_space(reader.as_bytes()[token.start as usize]) {
|
|
prefix = b' ';
|
|
debug_assert!(token.start != 0, "{reader}");
|
|
}
|
|
prev_needs_whitecpace = needs_space(reader.as_bytes()[token.end as usize - 1]);
|
|
|
|
let inbetween_new_lines =
|
|
reader[..token.start as usize].bytes().filter(|&b| b == b'\n').count()
|
|
+ token.kind.precedence().is_some() as usize;
|
|
let extra_prefix_new_lines = if inbetween_new_lines > 1 {
|
|
1 + token.kind.precedence().is_none() as usize
|
|
} else {
|
|
prev_needs_newline as usize
|
|
};
|
|
|
|
if token.kind == TokenKind::Comment && reader.as_bytes()[token.end as usize - 1] != b'/' {
|
|
prev_needs_newline = true;
|
|
prev_needs_whitecpace = false;
|
|
} else {
|
|
prev_needs_newline = false;
|
|
}
|
|
|
|
let sstr = reader[token.start as usize..].as_ptr();
|
|
reader = &reader[token.end as usize..];
|
|
unsafe {
|
|
if extra_prefix_new_lines != 0 {
|
|
for _ in 0..extra_prefix_new_lines {
|
|
writer.write(b'\n');
|
|
writer = writer.add(1);
|
|
}
|
|
} else if prefix != 0 {
|
|
writer.write(prefix);
|
|
writer = writer.add(1);
|
|
}
|
|
writer.copy_from(sstr, cpy_len);
|
|
writer = writer.add(cpy_len);
|
|
}
|
|
}
|
|
|
|
unsafe { writer.sub_ptr(source.as_mut_ptr()) }
|
|
}
|
|
|
|
pub struct Formatter<'a> {
|
|
source: &'a str,
|
|
depth: usize,
|
|
}
|
|
|
|
// we exclusively use `write_str` to reduce bloat
|
|
impl<'a> Formatter<'a> {
|
|
pub fn new(source: &'a str) -> Self {
|
|
Self { source, depth: 0 }
|
|
}
|
|
|
|
fn fmt_list<T: Poser, F: core::fmt::Write>(
|
|
&mut self,
|
|
f: &mut F,
|
|
trailing: bool,
|
|
end: &str,
|
|
sep: &str,
|
|
list: &[T],
|
|
fmt: impl Fn(&mut Self, &T, &mut F) -> fmt::Result,
|
|
) -> fmt::Result {
|
|
self.fmt_list_low(f, trailing, end, sep, list, |s, v, f| {
|
|
fmt(s, v, f)?;
|
|
Ok(true)
|
|
})
|
|
}
|
|
|
|
fn fmt_list_low<T: Poser, F: core::fmt::Write>(
|
|
&mut self,
|
|
f: &mut F,
|
|
trailing: bool,
|
|
end: &str,
|
|
sep: &str,
|
|
list: &[T],
|
|
fmt: impl Fn(&mut Self, &T, &mut F) -> Result<bool, fmt::Error>,
|
|
) -> fmt::Result {
|
|
if !trailing {
|
|
let mut first = true;
|
|
for expr in list {
|
|
if !core::mem::take(&mut first) {
|
|
f.write_str(sep)?;
|
|
f.write_str(" ")?;
|
|
}
|
|
first = !fmt(self, expr, f)?;
|
|
}
|
|
return f.write_str(end);
|
|
}
|
|
|
|
writeln!(f)?;
|
|
self.depth += 1;
|
|
let res = (|| {
|
|
for (i, stmt) in list.iter().enumerate() {
|
|
for _ in 0..self.depth {
|
|
f.write_str("\t")?;
|
|
}
|
|
let add_sep = fmt(self, stmt, f)?;
|
|
if add_sep {
|
|
f.write_str(sep)?;
|
|
}
|
|
if let Some(expr) = list.get(i + 1)
|
|
&& let Some(rest) = self.source.get(expr.posi() as usize..)
|
|
{
|
|
if insert_needed_semicolon(rest) {
|
|
f.write_str(";")?;
|
|
}
|
|
if preserve_newlines(&self.source[..expr.posi() as usize]) > 1 {
|
|
f.write_str("\n")?;
|
|
}
|
|
}
|
|
if add_sep {
|
|
f.write_str("\n")?;
|
|
}
|
|
}
|
|
Ok(())
|
|
})();
|
|
self.depth -= 1;
|
|
|
|
for _ in 0..self.depth {
|
|
f.write_str("\t")?;
|
|
}
|
|
f.write_str(end)?;
|
|
res
|
|
}
|
|
|
|
fn fmt_paren<F: core::fmt::Write>(
|
|
&mut self,
|
|
expr: &Expr,
|
|
f: &mut F,
|
|
cond: impl FnOnce(&Expr) -> bool,
|
|
) -> fmt::Result {
|
|
if cond(expr) {
|
|
f.write_str("(")?;
|
|
self.fmt(expr, f)?;
|
|
f.write_str(")")
|
|
} else {
|
|
self.fmt(expr, f)
|
|
}
|
|
}
|
|
|
|
pub fn fmt<F: core::fmt::Write>(&mut self, expr: &Expr, f: &mut F) -> fmt::Result {
|
|
macro_rules! impl_parenter {
|
|
($($name:ident => $pat:pat,)*) => {
|
|
$(
|
|
let $name = |e: &Expr| matches!(e, $pat);
|
|
)*
|
|
};
|
|
}
|
|
|
|
impl_parenter! {
|
|
unary => Expr::BinOp { .. },
|
|
postfix => Expr::UnOp { .. } | Expr::BinOp { .. },
|
|
consecutive => Expr::UnOp { .. },
|
|
}
|
|
|
|
match *expr {
|
|
Expr::Ct { value, .. } => {
|
|
f.write_str("$: ")?;
|
|
self.fmt(value, f)
|
|
}
|
|
Expr::String { literal, .. } => f.write_str(literal),
|
|
Expr::Comment { literal, .. } => f.write_str(literal),
|
|
Expr::Mod { path, .. } => write!(f, "@use(\"{path}\")"),
|
|
Expr::Field { target, name: field, .. } => {
|
|
self.fmt_paren(target, f, postfix)?;
|
|
f.write_str(".")?;
|
|
f.write_str(field)
|
|
}
|
|
Expr::Directive { name, args, .. } => {
|
|
f.write_str("@")?;
|
|
f.write_str(name)?;
|
|
f.write_str("(")?;
|
|
self.fmt_list(f, false, ")", ",", args, Self::fmt)
|
|
}
|
|
Expr::Struct { fields, trailing_comma, packed, .. } => {
|
|
if packed {
|
|
f.write_str("packed ")?;
|
|
}
|
|
|
|
write!(f, "struct {{")?;
|
|
self.fmt_list_low(f, trailing_comma, "}", ",", fields, |s, field, f| {
|
|
match field {
|
|
CommentOr::Or(StructField { name, ty, .. }) => {
|
|
f.write_str(name)?;
|
|
f.write_str(": ")?;
|
|
s.fmt(ty, f)?
|
|
}
|
|
CommentOr::Comment { literal, .. } => {
|
|
f.write_str(literal)?;
|
|
f.write_str("\n")?;
|
|
}
|
|
}
|
|
Ok(field.or().is_some())
|
|
})
|
|
}
|
|
Expr::Ctor { ty, fields, trailing_comma, .. } => {
|
|
if let Some(ty) = ty {
|
|
self.fmt_paren(ty, f, unary)?;
|
|
}
|
|
f.write_str(".{")?;
|
|
self.fmt_list(
|
|
f,
|
|
trailing_comma,
|
|
"}",
|
|
",",
|
|
fields,
|
|
|s: &mut Self, CtorField { name, value, .. }: &_, f| {
|
|
f.write_str(name)?;
|
|
if !matches!(value, Expr::Ident { name: n, .. } if name == n) {
|
|
f.write_str(": ")?;
|
|
s.fmt(value, f)?;
|
|
}
|
|
Ok(())
|
|
},
|
|
)
|
|
}
|
|
Expr::Tupl { ty, fields, trailing_comma, .. } => {
|
|
if let Some(ty) = ty {
|
|
self.fmt_paren(ty, f, unary)?;
|
|
}
|
|
f.write_str(".(")?;
|
|
self.fmt_list(f, trailing_comma, ")", ",", fields, Self::fmt)
|
|
}
|
|
Expr::Slice { item, size, .. } => {
|
|
f.write_str("[")?;
|
|
self.fmt(item, f)?;
|
|
if let Some(size) = size {
|
|
f.write_str("; ")?;
|
|
self.fmt(size, f)?;
|
|
}
|
|
f.write_str("]")
|
|
}
|
|
Expr::Index { base, index } => {
|
|
self.fmt(base, f)?;
|
|
f.write_str("[")?;
|
|
self.fmt(index, f)?;
|
|
f.write_str("]")
|
|
}
|
|
Expr::UnOp { op, val, .. } => {
|
|
f.write_str(op.name())?;
|
|
self.fmt_paren(val, f, unary)
|
|
}
|
|
Expr::Break { .. } => f.write_str("break"),
|
|
Expr::Continue { .. } => f.write_str("continue"),
|
|
Expr::If { cond, then, else_, .. } => {
|
|
f.write_str("if ")?;
|
|
self.fmt(cond, f)?;
|
|
f.write_str(" ")?;
|
|
self.fmt_paren(then, f, consecutive)?;
|
|
if let Some(e) = else_ {
|
|
f.write_str(" else ")?;
|
|
self.fmt(e, f)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Expr::Loop { body, .. } => {
|
|
f.write_str("loop ")?;
|
|
self.fmt(body, f)
|
|
}
|
|
Expr::Closure { ret, body, args, .. } => {
|
|
f.write_str("fn(")?;
|
|
self.fmt_list(f, false, "", ",", args, |s, arg, f| {
|
|
if arg.is_ct {
|
|
f.write_str("$")?;
|
|
}
|
|
f.write_str(arg.name)?;
|
|
f.write_str(": ")?;
|
|
s.fmt(&arg.ty, f)
|
|
})?;
|
|
f.write_str("): ")?;
|
|
self.fmt(ret, f)?;
|
|
f.write_str(" ")?;
|
|
self.fmt_paren(body, f, consecutive)?;
|
|
Ok(())
|
|
}
|
|
Expr::Call { func, args, trailing_comma } => {
|
|
self.fmt_paren(func, f, postfix)?;
|
|
f.write_str("(")?;
|
|
self.fmt_list(f, trailing_comma, ")", ",", args, Self::fmt)
|
|
}
|
|
Expr::Return { val: Some(val), .. } => {
|
|
f.write_str("return ")?;
|
|
self.fmt(val, f)
|
|
}
|
|
Expr::Return { val: None, .. } => f.write_str("return"),
|
|
Expr::Ident { name, is_ct: true, .. } => {
|
|
f.write_str("$")?;
|
|
f.write_str(name)
|
|
}
|
|
Expr::Ident { name, is_ct: false, .. } => f.write_str(name),
|
|
Expr::Block { stmts, .. } => {
|
|
f.write_str("{")?;
|
|
self.fmt_list(f, true, "}", "", stmts, Self::fmt)
|
|
}
|
|
Expr::Number { value, radix, .. } => {
|
|
fn display_radix(radix: Radix, mut value: u64, buf: &mut [u8; 64]) -> &str {
|
|
fn conv_radix(d: u8) -> u8 {
|
|
match d {
|
|
0..=9 => d + b'0',
|
|
_ => d - 10 + b'A',
|
|
}
|
|
}
|
|
|
|
for (i, b) in buf.iter_mut().enumerate().rev() {
|
|
let d = (value % radix as u64) as u8;
|
|
value /= radix as u64;
|
|
*b = conv_radix(d);
|
|
if value == 0 {
|
|
return unsafe { core::str::from_utf8_unchecked(&buf[i..]) };
|
|
}
|
|
}
|
|
|
|
unreachable!()
|
|
}
|
|
f.write_str(match radix {
|
|
Radix::Decimal => "",
|
|
Radix::Hex => "0x",
|
|
Radix::Octal => "0o",
|
|
Radix::Binary => "0b",
|
|
})?;
|
|
let mut buf = [0u8; 64];
|
|
f.write_str(display_radix(radix, value as u64, &mut buf))
|
|
}
|
|
Expr::Bool { value, .. } => f.write_str(if value { "true" } else { "false" }),
|
|
Expr::Idk { .. } => f.write_str("idk"),
|
|
Expr::BinOp {
|
|
left,
|
|
op: TokenKind::Assign,
|
|
right: &Expr::BinOp { left: lleft, op, right },
|
|
} if left.pos() == lleft.pos() => {
|
|
self.fmt(left, f)?;
|
|
f.write_str(" ")?;
|
|
f.write_str(op.name())?;
|
|
f.write_str("= ")?;
|
|
self.fmt(right, f)
|
|
}
|
|
Expr::BinOp { right, op, left } => {
|
|
let pec_miss = |e: &Expr| {
|
|
matches!(
|
|
e, Expr::BinOp { op: lop, .. } if op.precedence() > lop.precedence()
|
|
)
|
|
};
|
|
|
|
self.fmt_paren(left, f, pec_miss)?;
|
|
if let Some(mut prev) = self.source.get(..right.pos() as usize) {
|
|
prev = prev.trim_end();
|
|
let estimate_bound =
|
|
prev.rfind(|c: char| c.is_ascii_whitespace()).map_or(prev.len(), |i| i + 1);
|
|
let exact_bound = lexer::Lexer::new(&prev[estimate_bound..]).last().start;
|
|
prev = &prev[..exact_bound as usize + estimate_bound];
|
|
if preserve_newlines(prev) > 0 {
|
|
f.write_str("\n")?;
|
|
for _ in 0..self.depth + 1 {
|
|
f.write_str("\t")?;
|
|
}
|
|
f.write_str(op.name())?;
|
|
f.write_str(" ")?;
|
|
} else {
|
|
f.write_str(" ")?;
|
|
f.write_str(op.name())?;
|
|
f.write_str(" ")?;
|
|
}
|
|
} else {
|
|
f.write_str(" ")?;
|
|
f.write_str(op.name())?;
|
|
f.write_str(" ")?;
|
|
}
|
|
self.fmt_paren(right, f, pec_miss)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn preserve_newlines(source: &str) -> usize {
|
|
source[source.trim_end().len()..].bytes().filter(|&c| c == b'\n').count()
|
|
}
|
|
|
|
pub fn insert_needed_semicolon(source: &str) -> bool {
|
|
let kind = lexer::Lexer::new(source).next().kind;
|
|
kind.precedence().is_some() || matches!(kind, TokenKind::Ctor | TokenKind::Tupl)
|
|
}
|
|
|
|
impl core::fmt::Display for parser::Ast {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
fmt_file(self.exprs(), &self.file, f)
|
|
}
|
|
}
|
|
|
|
pub fn fmt_file(exprs: &[Expr], file: &str, f: &mut impl fmt::Write) -> fmt::Result {
|
|
for (i, expr) in exprs.iter().enumerate() {
|
|
Formatter::new(file).fmt(expr, f)?;
|
|
if let Some(expr) = exprs.get(i + 1)
|
|
&& let Some(rest) = file.get(expr.pos() as usize..)
|
|
{
|
|
if insert_needed_semicolon(rest) {
|
|
write!(f, ";")?;
|
|
}
|
|
|
|
if preserve_newlines(&file[..expr.pos() as usize]) > 1 {
|
|
writeln!(f)?;
|
|
}
|
|
}
|
|
|
|
if i + 1 != exprs.len() {
|
|
writeln!(f)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod test {
|
|
use {
|
|
crate::parser::{self, ParserCtx},
|
|
alloc::borrow::ToOwned,
|
|
std::{fmt::Write, string::String},
|
|
};
|
|
|
|
pub fn format(ident: &str, input: &str) {
|
|
let mut minned = input.to_owned();
|
|
let len = crate::fmt::minify(&mut minned);
|
|
minned.truncate(len);
|
|
|
|
let ast = parser::Ast::new(ident, minned, &mut ParserCtx::default(), &mut |_, _| Ok(0));
|
|
log::error!(
|
|
"{} / {} = {} | {} / {} = {}",
|
|
ast.mem.size(),
|
|
input.len(),
|
|
ast.mem.size() as f32 / input.len() as f32,
|
|
ast.mem.size(),
|
|
ast.file.len(),
|
|
ast.mem.size() as f32 / ast.file.len() as f32
|
|
);
|
|
let mut output = String::new();
|
|
write!(output, "{ast}").unwrap();
|
|
|
|
let input_path = format!("formatter_{ident}.expected");
|
|
let output_path = format!("formatter_{ident}.actual");
|
|
std::fs::write(&input_path, input).unwrap();
|
|
std::fs::write(&output_path, output).unwrap();
|
|
|
|
let success = std::process::Command::new("diff")
|
|
.arg("-u")
|
|
.arg("--color")
|
|
.arg(&input_path)
|
|
.arg(&output_path)
|
|
.status()
|
|
.unwrap()
|
|
.success();
|
|
std::fs::remove_file(&input_path).unwrap();
|
|
std::fs::remove_file(&output_path).unwrap();
|
|
assert!(success, "test failed");
|
|
}
|
|
|
|
macro_rules! test {
|
|
($($name:ident => $input:expr;)*) => {$(
|
|
#[test]
|
|
fn $name() {
|
|
format(stringify!($name), $input);
|
|
}
|
|
)*};
|
|
}
|
|
|
|
test! {
|
|
comments => "// comment\n// comment\n\n// comment\n\n\
|
|
/* comment */\n/* comment */\n\n/* comment */";
|
|
some_ordinary_code => "loft := fn(): int return loft(1, 2, 3)";
|
|
some_arg_per_line_code => "loft := fn(): int return loft(\
|
|
\n\t1,\n\t2,\n\t3,\n)";
|
|
some_ordinary_struct => "loft := fn(): int return loft.{a: 1, b: 2}";
|
|
some_ordinary_fild_per_lin_struct => "loft := fn(): int return loft.{\
|
|
\n\ta: 1,\n\tb: 2,\n}";
|
|
code_block => "loft := fn(): int {\n\tloft()\n\treturn 1\n}";
|
|
}
|
|
}
|