pub fn format(src: &str) -> String {
let toks = tokenize(src);
render(&toks)
}
#[derive(Clone, Debug, PartialEq)]
enum Tok {
Open,
Close,
Prefix(String), Atom(String), Str(String), Comment(String), Newline,
}
fn tokenize(src: &str) -> Vec<Tok> {
let chars: Vec<char> = src.chars().collect();
let mut toks = Vec::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
match c {
'\n' => { toks.push(Tok::Newline); i += 1; }
' ' | '\t' | '\r' => { i += 1; }
';' => {
let start = i;
while i < chars.len() && chars[i] != '\n' { i += 1; }
toks.push(Tok::Comment(chars[start..i].iter().collect()));
}
'"' => {
let start = i;
i += 1;
while i < chars.len() {
match chars[i] {
'\\' => { i += 2; } '"' => { i += 1; break; }
_ => { i += 1; }
}
}
toks.push(Tok::Str(chars[start..i.min(chars.len())].iter().collect()));
}
'(' | '[' => { toks.push(Tok::Open); i += 1; }
')' | ']' => { toks.push(Tok::Close); i += 1; }
'\'' => { toks.push(Tok::Prefix("'".into())); i += 1; }
'`' => { toks.push(Tok::Prefix("`".into())); i += 1; }
',' => {
if i + 1 < chars.len() && chars[i + 1] == '@' {
toks.push(Tok::Prefix(",@".into())); i += 2;
} else {
toks.push(Tok::Prefix(",".into())); i += 1;
}
}
_ => {
let start = i;
while i < chars.len()
&& !matches!(chars[i],
' ' | '\t' | '\r' | '\n' | '(' | ')' | '[' | ']'
| '"' | ';' | '\'' | '`' | ',') {
i += 1;
}
toks.push(Tok::Atom(chars[start..i].iter().collect()));
}
}
}
toks
}
fn is_body_form(s: &str) -> bool {
matches!(s,
"define" | "def" | "define-macro" | "defmacro" | "deftool" | "deftool-spec"
| "defrust" | "defrust*" | "defun-constrained" | "define-typed" | "defproof"
| "deftest" | "defguard" | "lambda"
| "let" | "let*" | "letrec" | "letrec*" | "named-let" | "parameterize"
| "when" | "unless" | "begin" | "do" | "dotimes" | "while" | "for" | "for-each"
| "case" | "match" | "eval-when" | "module")
}
#[derive(Clone, Copy, PartialEq)]
enum Kind { Data, Body, Operator }
struct Ctx {
paren_col: usize,
base: usize, kind: Kind,
head_seen: bool,
awaiting_arg1: bool,
head_line: usize,
}
fn is_number(s: &str) -> bool { s.parse::<f64>().is_ok() }
fn render(toks: &[Tok]) -> String {
let mut lines: Vec<Vec<&Tok>> = vec![Vec::new()];
for t in toks {
if *t == Tok::Newline { lines.push(Vec::new()); }
else { lines.last_mut().unwrap().push(t); }
}
let mut stack: Vec<Ctx> = Vec::new();
let mut out: Vec<String> = Vec::new();
let mut line_no = 0usize;
for group in &lines {
if group.is_empty() {
if !out.is_empty() && !out.last().unwrap().is_empty() { out.push(String::new()); }
line_no += 1;
continue;
}
let indent = stack.last().map(|c| c.base).unwrap_or(0);
let mut line = " ".repeat(indent);
let mut col = indent;
let mut prev_prefix = false;
let mut prev_open = false;
for tok in group {
match tok {
Tok::Newline => unreachable!(),
Tok::Open => {
if col != indent && !prev_open && !prev_prefix { line.push(' '); col += 1; }
let paren_col = col;
line.push('('); col += 1;
consume_arg1(&mut stack, line_no, paren_col); stack.push(Ctx { paren_col, base: paren_col + 1, kind: Kind::Data,
head_seen: false, awaiting_arg1: false, head_line: line_no });
prev_open = true; prev_prefix = false;
}
Tok::Close => {
line.push(')'); col += 1;
stack.pop();
prev_open = false; prev_prefix = false;
}
Tok::Prefix(p) => {
if col != indent && !prev_open && !prev_prefix { line.push(' '); col += 1; }
let at = col;
line.push_str(p); col += p.chars().count();
set_head_or_arg1(&mut stack, line_no, at, HeadTok::Prefix);
prev_prefix = true; prev_open = false;
}
Tok::Atom(a) => {
if col != indent && !prev_open && !prev_prefix { line.push(' '); col += 1; }
let at = col;
line.push_str(a); col += a.chars().count();
let ht = if is_body_form(a) { HeadTok::BodySym }
else if is_number(a) { HeadTok::NonSym }
else { HeadTok::Sym };
set_head_or_arg1(&mut stack, line_no, at, ht);
prev_open = false; prev_prefix = false;
}
Tok::Str(s) => {
if col != indent && !prev_open && !prev_prefix { line.push(' '); col += 1; }
let at = col;
line.push_str(s);
col = match s.rsplit_once('\n') {
Some((_, tail)) => tail.chars().count(),
None => col + s.chars().count(),
};
set_head_or_arg1(&mut stack, line_no, at, HeadTok::NonSym);
prev_open = false; prev_prefix = false;
}
Tok::Comment(cm) => {
if col != indent && !prev_open && !prev_prefix { line.push(' '); col += 1; }
line.push_str(cm); col += cm.chars().count();
prev_open = false; prev_prefix = false;
}
}
}
while line.ends_with(' ') { line.pop(); }
out.push(line);
line_no += 1;
}
while out.last().map(|l| l.is_empty()).unwrap_or(false) { out.pop(); }
let mut s = out.join("\n");
s.push('\n');
s
}
enum HeadTok { Sym, BodySym, NonSym, Prefix }
fn set_head_or_arg1(stack: &mut [Ctx], line_no: usize, at: usize, ht: HeadTok) {
if let Some(top) = stack.last_mut() {
if !top.head_seen {
top.head_seen = true;
top.head_line = line_no;
match ht {
HeadTok::BodySym => { top.kind = Kind::Body; top.base = top.paren_col + 2; }
HeadTok::Sym => {
top.kind = Kind::Operator;
top.base = top.paren_col + 2; top.awaiting_arg1 = true;
}
HeadTok::NonSym | HeadTok::Prefix => {
top.kind = Kind::Data; top.base = top.paren_col + 1;
}
}
} else if top.awaiting_arg1 && top.kind == Kind::Operator {
if line_no == top.head_line { top.base = at; }
top.awaiting_arg1 = false;
}
}
}
fn consume_arg1(stack: &mut [Ctx], line_no: usize, at: usize) {
if let Some(top) = stack.last_mut() {
if !top.head_seen {
top.head_seen = true;
top.head_line = line_no;
top.kind = Kind::Data;
top.base = top.paren_col + 1;
} else if top.awaiting_arg1 && top.kind == Kind::Operator {
if line_no == top.head_line { top.base = at; }
top.awaiting_arg1 = false;
}
}
}