use crate::form::{Form, List};
use crate::messages::{raise, Refusal};
#[derive(Clone, Debug, PartialEq)]
pub struct Token {
pub text: String,
pub line: u32,
}
pub fn tokenize(s: &str) -> Vec<Token> {
let chars: Vec<char> = s.chars().collect();
let n = chars.len();
let mut out = Vec::new();
let mut i = 0;
let mut line: u32 = 1;
while i < n {
let c = chars[i];
match c {
'\n' => {
line += 1;
i += 1;
}
' ' | '\t' | '\r' => {
i += 1;
}
'(' | ')' => {
out.push(Token {
text: c.to_string(),
line,
});
i += 1;
}
';' => {
while i < n && chars[i] != '\r' && chars[i] != '\n' {
i += 1;
}
}
'"' => {
let start_line = line;
i += 1;
let mut buf = String::from("\"");
while i < n {
let c = chars[i];
if c == '\\' {
match chars.get(i + 1) {
Some('"') => {
buf.push('"');
i += 2;
}
Some('\\') => {
buf.push('\\');
i += 2;
}
_ => {
buf.push(c);
i += 1;
}
}
} else if c == '"' {
i += 1;
break;
} else {
if c == '\n' {
line += 1;
}
buf.push(c);
i += 1;
}
}
out.push(Token {
text: buf,
line: start_line,
});
}
_ => {
let start = i;
while i < n {
let c = chars[i];
if matches!(c, '(' | ')' | ' ' | '\t' | '\r' | '\n' | ';' | '"') {
break;
}
i += 1;
}
out.push(Token {
text: chars[start..i].iter().collect(),
line,
});
}
}
}
out
}
pub fn count_lf(s: &str) -> usize {
s.matches('\n').count()
}
fn user_line(raw_line: u32, line_offset: u32) -> Option<u32> {
if raw_line > line_offset {
Some(raw_line - line_offset)
} else {
None
}
}
pub fn src_line_tag(raw_line: u32, line_offset: u32) -> String {
match user_line(raw_line, line_offset) {
Some(n) => format!(" (vla line {n})"),
None => String::new(),
}
}
pub fn opened_at_tag(raw_line: u32, line_offset: u32) -> String {
match user_line(raw_line, line_offset) {
Some(n) => format!(" for the list opened at vla line {n}"),
None => String::new(),
}
}
pub fn parse_all(toks: &[Token], line_offset: u32) -> Result<Vec<Form>, Refusal> {
let mut pos = 0;
let mut out = Vec::new();
while pos < toks.len() {
out.push(parse_form(toks, &mut pos, line_offset, 1)?);
}
Ok(out)
}
pub const FORM_DEPTH_LIMIT: u32 = 64;
fn parse_form(
toks: &[Token],
pos: &mut usize,
line_offset: u32,
depth: u32,
) -> Result<Form, Refusal> {
let tok = &toks[*pos];
let raw_line = tok.line;
*pos += 1;
if tok.text == "(" {
if depth > FORM_DEPTH_LIMIT {
return Err(raise(
"vla-form-too-deep",
&[
("limit", &FORM_DEPTH_LIMIT.to_string()),
("loc", &src_line_tag(raw_line, line_offset)),
],
));
}
let line = user_line(raw_line, line_offset).unwrap_or(0);
let mut items = Vec::new();
while *pos < toks.len() {
if toks[*pos].text == ")" {
*pos += 1;
return Ok(Form::List(List::new(items, line)));
}
items.push(parse_form(toks, pos, line_offset, depth + 1)?);
}
Err(raise(
"vla-unbalanced-parens",
&[("loc", &opened_at_tag(raw_line, line_offset))],
))
} else if tok.text == ")" {
Err(raise(
"vla-unexpected-close-paren",
&[("loc", &src_line_tag(raw_line, line_offset))],
))
} else if let Some(content) = tok.text.strip_prefix('"') {
Ok(Form::Str(content.to_string()))
} else {
Ok(Form::Sym(tok.text.clone()))
}
}
pub fn read_forms(text: &str) -> Result<Vec<Form>, Refusal> {
parse_all(&tokenize(text), 0)
}
pub fn read_forms_with_lines(text: &str) -> Result<Vec<(Form, u32)>, Refusal> {
Ok(read_forms(text)?
.into_iter()
.map(|f| {
let line = f.as_list().map(|l| l.line).unwrap_or(0);
(f, line)
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::printer::{write_datum, write_pretty};
fn texts(toks: &[Token]) -> Vec<&str> {
toks.iter().map(|t| t.text.as_str()).collect()
}
#[test]
fn tokenize_follows_the_vba_character_classes() {
let toks = tokenize("(set! x \"a \\\"q\\\" b\\n\") ; a comment\r\n(y)");
assert_eq!(
texts(&toks),
vec!["(", "set!", "x", "\"a \"q\" b\\n", ")", "(", "y", ")"]
);
assert_eq!(toks[0].line, 1);
assert_eq!(toks[5].line, 2);
assert_eq!(
texts(&tokenize("abc\"d\"\te\rf")),
vec!["abc", "\"d", "e", "f"]
);
let toks = tokenize("\"two\nlines\" after");
assert_eq!(toks[0].text, "\"two\nlines");
assert_eq!(toks[0].line, 1);
assert_eq!(toks[1].line, 2);
assert_eq!(texts(&tokenize("\"open")), vec!["\"open"]);
assert!(tokenize("").is_empty());
assert!(tokenize(" ; only a comment\n").is_empty());
}
#[test]
fn parse_tags_lists_with_their_lines_less_the_offset() {
let forms = read_forms("(a\n (b) c)\n\n(d)").unwrap();
assert_eq!(forms.len(), 2);
let a = forms[0].as_list().unwrap();
assert_eq!(a.line, 1);
assert_eq!(a.items[1].as_list().unwrap().line, 2);
assert_eq!(forms[1].as_list().unwrap().line, 4);
let forms = parse_all(&tokenize("(p)\n(q)\n(r)"), 2).unwrap();
assert_eq!(forms[0].as_list().unwrap().line, 0);
assert_eq!(forms[1].as_list().unwrap().line, 0);
assert_eq!(forms[2].as_list().unwrap().line, 1);
let with_lines = read_forms_with_lines("x\n(y)").unwrap();
assert_eq!(with_lines[0], (Form::sym("x"), 0));
assert_eq!(with_lines[1].1, 2);
}
#[test]
fn the_two_reader_refusals_name_their_lines() {
let r = read_forms("(a\n(b").unwrap_err();
assert_eq!(r.id, "vla-unbalanced-parens");
assert_eq!(
r.text,
"unbalanced parentheses: missing ')' for the list opened at vla line 2"
);
let r = read_forms("(a)\n)").unwrap_err();
assert_eq!(r.id, "vla-unexpected-close-paren");
assert_eq!(r.text, "unexpected ')' (vla line 2)");
let r = parse_all(&tokenize("(a"), 5).unwrap_err();
assert_eq!(r.text, "unbalanced parentheses: missing ')'");
}
#[test]
fn a_list_past_the_depth_limit_is_refused_by_name() {
let nest = |n: usize| format!("{}{}", "(".repeat(n), ")".repeat(n));
assert_eq!(read_forms(&nest(64)).unwrap().len(), 1);
let r = read_forms(&nest(65)).unwrap_err();
assert_eq!(r.id, "vla-form-too-deep");
assert_eq!(
r.text,
"lists nested more than 64 deep (vla line 1) - no program needs that many"
);
assert_eq!(
read_forms(&nest(100_000)).unwrap_err().id,
"vla-form-too-deep"
);
}
const GOLDEN: &str = include_str!("../testdata/instructions_golden.vla");
const PRELUDE: &str = include_str!("../testdata/prelude.vla");
#[test]
fn the_golden_and_the_prelude_read_print_and_read_again() {
let golden = read_forms(GOLDEN).unwrap();
assert_eq!(golden.len(), 248);
let prelude = read_forms(PRELUDE).unwrap();
assert_eq!(prelude.len(), 46);
for forms in [&golden, &prelude] {
let flat: String = forms.iter().map(write_datum).collect::<Vec<_>>().join("\n");
assert_eq!(&read_forms(&flat).unwrap(), forms);
let pretty: String = forms
.iter()
.map(|f| write_pretty(f, 0))
.collect::<Vec<_>>()
.join("\n");
assert_eq!(&read_forms(&pretty).unwrap(), forms);
}
assert!(golden[0].head_is("const"));
assert!(prelude.iter().all(|f| f.head_is("defmacro")));
}
#[test]
fn count_lf_counts_line_feeds_only() {
assert_eq!(count_lf("a\r\nb\nc"), 2);
assert_eq!(count_lf(""), 0);
}
}