Error handling and small editor

- Error handling to signal what is preventing evaluation instead of
  program crash
- Small editor feature (allow edit new
line of code to complete the previous ones instead of destroying the
input, if an empty line inserted return the error preventing evaluation
- Auto indentation on multiline input based on depth

Still does not parse multi-statement code
- This is a problem when dealing with macros: does not allow
  expressions like `'()` since the atomic `'` hides the list ().
  Need to chose between:
  - Replace `'...` with `(quote ... )` during tokenization (may be
    hard to implement macros later)
  - Allows multi-statement code (this also allows to execute multiple
    statements when reading a file)

Will probably delete auto-indentation since it breaks code's uniformity
too much
This commit is contained in:
teo3300
2023-06-07 00:50:06 +02:00
parent 703b6888b5
commit 9d35d24cd4
5 changed files with 213 additions and 103 deletions

View File

@ -15,9 +15,34 @@ fn main() -> io::Result<()> {
let _ = io::stdout().flush(); let _ = io::stdout().flush();
let mut input = String::new(); let mut input = String::new();
loop {
// Read line to compose program inpug
let mut line = String::new();
io::stdin().read_line(&mut line)?;
io::stdin().read_line(&mut input)?; // Append line to input
input.push_str(&line);
println!("{}", rep(&input.replace("\n", " "))); // If there is nothing to evaluate skip rep
if input == "\n" {
continue;
}
// Perform rep on whole available input
match rep(&input) {
Ok(output) => println!("{}", output),
Err((err, depth)) => {
if line == "\n" {
println!("ERROR: {}", err);
} else {
print!("user> {}", " ".repeat(depth));
// Flush the prompt to appear before command
let _ = io::stdout().flush();
continue;
}
}
}
break;
}
} }
} }

View File

@ -1,30 +1,39 @@
use crate::types::escape_str;
use crate::types::MalType; use crate::types::MalType;
use crate::types::MalType::*;
pub fn pr_str(ast: &MalType) -> String { pub fn pr_str(ast: &MalType, print_readably: bool) -> String {
match ast { match ast {
MalType::Nil => "nil".to_string(), Nil => "nil".to_string(),
MalType::Symbol(sym) => sym.to_string(), Symbol(sym) | Keyword(sym) => sym.to_string(),
MalType::Integer(val) => val.to_string(), Int(val) => val.to_string(),
MalType::Bool(val) => val.to_string(), Bool(val) => val.to_string(),
MalType::List(el) => format!( Str(str) => {
if print_readably {
escape_str(str)
} else {
str.to_string()
}
}
List(el) => format!(
"({})", "({})",
el.iter() el.iter()
.map(|sub| pr_str(sub)) .map(|e| pr_str(e, print_readably))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(" ") .join(" ")
), ),
// This is truly horrible // This is truly horrible
MalType::Vector(el) => format!( Vector(el) => format!(
"[{}]", "[{}]",
el.iter() el.iter()
.map(|sub| pr_str(sub)) .map(|e| pr_str(e, print_readably))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(" ") .join(" ")
), ),
MalType::Map(el) => format!( Map(el) => format!(
"{{{}}}", "{{{}}}",
el.iter() el.iter()
.map(|sub| vec![pr_str(sub.0), pr_str(sub.1)].join(" ")) .map(|sub| vec![sub.0.to_string(), pr_str(sub.1, print_readably)].join(" "))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(" ") .join(" ")
), ),

View File

@ -1,38 +1,43 @@
use std::collections::{BTreeMap, VecDeque}; // Specyfy components in "types"
use crate::types::*;
use crate::types::MalType; // By specifying enum variants it's possible to omit namespace
use crate::types::MalType::*;
use regex::Regex; use regex::Regex;
pub struct Reader { pub struct Reader {
tokens: VecDeque<String>, tokens: Vec<String>,
ptr: usize,
depth: usize,
} }
// TODO: instead of panic on missing ")" try implementing a multi line parsing // TODO: instead of panic on missing ")" try implementing a multi line parsing
// Status on return should always be The last element of the last opened lists // Status on return should always be The last element of the last opened lists
// (append to the "last" list) while traversing // (append to the "last" list) while traversing
const PAREN_ERROR: &str =
"Looks like you reached a dead end, did you perhaps miss any \")\" or left some extra \"(\"?";
impl Reader { impl Reader {
fn new(tokens: VecDeque<String>) -> Reader { fn new(tokens: Vec<String>) -> Reader {
Reader { tokens } Reader {
tokens,
ptr: 0,
depth: 0,
}
} }
/// Returns the token at the current positioni /// Returns the token at the current position
fn peek(&self) -> &str { fn peek(&self) -> Result<String, MalErr> {
match self.tokens.get(0) { match self.tokens.get(self.ptr) {
Some(token) => token, Some(token) => Ok(token.to_string()),
None => panic!("{}", PAREN_ERROR), None => Err("Unexpected EOF, Missing parenthesis?".to_string()),
} }
} }
/// Returns the token at current position and increment current position /// Returns the token at current position and increment current position
// TODO: PLEASE USE THE PEEK FUNCTION // TODO: PLEASE USE THE PEEK FUNCTION
fn next(&mut self) -> String { fn next(&mut self) -> Result<String, MalErr> {
match self.tokens.pop_front() { self.ptr += 1;
Some(token) => token, match self.tokens.get(self.ptr - 1) {
None => panic!("{}", PAREN_ERROR), Some(token) => Ok(token.to_string()),
None => Err("Unexpected EOF, Missing parenthesis?".to_string()),
} }
} }
@ -41,35 +46,54 @@ impl Reader {
/// Accumulates results into a MalList /// Accumulates results into a MalList
/// NOTE: `read_list` calls `read_form` -> enable recursion /// NOTE: `read_list` calls `read_form` -> enable recursion
/// (lists can contains other lists) /// (lists can contains other lists)
fn read_list(&mut self, terminator: &str) -> Vec<MalType> { fn read_list(&mut self, terminator: &str) -> MalRet {
std::iter::from_fn(|| match self.peek() { self.depth += 1;
")" | "]" | "}" => {
if terminator != self.peek() { self.next()?;
panic!("Unexpected token: {}", self.peek())
let mut vector = Vec::new();
loop {
let token = self.peek()?;
if token == terminator {
break;
} }
self.next(); vector.push(self.read_form()?)
None
} }
_ => Some(self.read_form()), self.next()?;
}) let ret = match terminator {
.collect() ")" => Ok(List(vector)),
"]" => Ok(Vector(vector)),
"}" => make_map(vector),
_ => Err(format!("Unknown collection terminator: {}", terminator)),
};
self.depth -= 1;
ret
} }
/// Read atomic token and return appropriate scalar () /// Read atomic token and return appropriate scalar ()
fn read_atom(&mut self) -> MalType { fn read_atom(&mut self) -> MalRet {
let token = self.next(); let token = self.next()?;
// parse the token as an integer let re_digits = Regex::new(r"^-?[0-9]+$").unwrap();
match token.parse::<i32>() { match token.as_str() {
// On success assign the value ")" | "]" | "}" => Err(format!("Lone parenthesis {}", token)),
Ok(value) => MalType::Integer(value), "false" => Ok(Bool(false)),
// Otherwise assign the symbol "true" => Ok(Bool(true)),
Err(_) => match token.as_str() { "nil" => Ok(Nil),
")" | "]" | "}" => panic!("Lone parenthesis {}", token), _ => {
"false" => MalType::Bool(false), if re_digits.is_match(&token) {
"true" => MalType::Bool(true), Ok(Int(token.parse::<isize>().unwrap()))
"nil" => MalType::Nil, } else if token.starts_with('\"') {
_ => MalType::Symbol(token), if token.ends_with('\"') {
}, Ok(Str(unescape_str(&token)))
} else {
Err("Unterminated string, expected \"".to_string())
}
} else if token.starts_with(':') {
Ok(Keyword(token))
} else {
Ok(Symbol(token))
}
}
} }
} }
@ -78,31 +102,14 @@ impl Reader {
/// Switch on the first character /// Switch on the first character
/// "(" -> call `read_list` /// "(" -> call `read_list`
/// otherwise -> call `read_atom` /// otherwise -> call `read_atom`
fn read_form(&mut self) -> MalType { fn read_form(&mut self) -> MalRet {
match self.peek() { let token = self.peek()?;
// String slice containing the whole string
match &token[..] {
// Consume "(" and parse list // Consume "(" and parse list
"(" => { "(" => self.read_list(")"),
self.next(); "[" => self.read_list("]"),
MalType::List(self.read_list(")")) "{" => self.read_list("}"),
}
"[" => {
self.next();
MalType::Vector(self.read_list("]"))
}
"{" => {
self.next();
// fallback to C mode for now 😎
let list = self.read_list("}");
if list.len() % 2 != 0 {
panic!("Missing Map element")
}
let mut map = BTreeMap::new();
for i in (0..list.len()).step_by(2) {
map.insert(list[i].clone(), list[i + 1].clone());
}
MalType::Map(map)
}
// read atomically
_ => self.read_atom(), _ => self.read_atom(),
} }
} }
@ -112,23 +119,36 @@ impl Reader {
/// Create anew Reader with the tokens /// Create anew Reader with the tokens
/// Call read_from with the reader instance /// Call read_from with the reader instance
/// TODO: catch errors /// TODO: catch errors
pub fn read_str(input: &str) -> MalType { pub fn read_str(input: &str) -> Result<MalType, (MalErr, usize)> {
let ast = Reader::new(tokenize(input)).read_form(); let tokens = tokenize(input);
// pretty_print(&ast, 0); match tokens.len() {
ast 0 => Ok(Nil),
_ => {
let mut reader = Reader::new(tokens);
match reader.read_form() {
Err(err) => Err((err, reader.depth)),
Ok(any) => Ok(any),
}
}
}
} }
/// Read a string and return a list of tokens in it (following regex in README) /// Read a string and return a list of tokens in it (following regex in README)
// Add error handling for strings that are not terminated // Add error handling for strings that are not terminated
fn tokenize(input: &str) -> VecDeque<String> { fn tokenize(input: &str) -> Vec<String> {
let mut tokens = VecDeque::new(); let mut tokens = Vec::new();
let re = let re = Regex::new(
Regex::new(r###"[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)"###) r###"[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*\n|[^\s\[\]{}('"`,;)]*)"###,
)
.unwrap(); .unwrap();
for match_str in re.captures_iter(input) { for match_str in re.captures_iter(input) {
if match_str[1].len() > 0 { if !match_str[1].is_empty() {
tokens.push_back(match_str[1].to_string()); // Drop comments
if match_str[1].starts_with(';') {
continue;
}
tokens.push(match_str[1].to_string());
} }
} }

View File

@ -6,12 +6,15 @@
use crate::printer::pr_str; use crate::printer::pr_str;
use crate::reader::read_str; use crate::reader::read_str;
use crate::types::MalType; use crate::types::{MalErr, MalType};
#[allow(non_snake_case)] #[allow(non_snake_case)]
/// Read input and generate an ast /// Read input and generate an ast
fn READ(input: &str) -> MalType { fn READ(input: &str) -> Result<MalType, (MalErr, usize)> {
read_str(input) match read_str(input) {
Ok(ast) => Ok(ast),
Err((err, depth)) => Err((format!("Unexpected error during READ: {}", err), depth)),
}
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
@ -24,11 +27,11 @@ fn EVAL(ast: MalType) -> MalType {
#[allow(non_snake_case)] #[allow(non_snake_case)]
/// Print out the result of the evaluation /// Print out the result of the evaluation
fn PRINT(input: MalType) -> String { fn PRINT(input: MalType) -> String {
pr_str(&input) pr_str(&input, true)
} }
pub fn rep(input: &str) -> String { pub fn rep(input: &str) -> Result<String, (MalErr, usize)> {
let ast = READ(input); let ast = READ(input)?;
let out = EVAL(ast); let out = EVAL(ast);
PRINT(out /*&result*/) Ok(PRINT(out))
} }

View File

@ -1,16 +1,69 @@
// TODO: use enums for MalTypes // TODO: use enums for MalTypes
use std::collections::BTreeMap; use std::collections::HashMap;
// All Mal types should inherit from this // All Mal types should inherit from this
#[derive(Debug, Ord, Eq, PartialEq, PartialOrd, Clone)] #[derive(Debug, Clone)]
pub enum MalType { pub enum MalType {
List(Vec<MalType>), List(Vec<MalType>),
Vector(Vec<MalType>), Vector(Vec<MalType>),
// HashMap cannot implement Hash Map(HashMap<String, MalType>),
Map(BTreeMap<MalType, MalType>),
Symbol(String), Symbol(String),
Integer(i32), Keyword(String),
Str(String),
Int(isize),
Bool(bool), Bool(bool),
Nil, Nil,
} }
// Stolen, but this way it's easier to handle errors
/*
#[derive(Debug)]
pub enum MalErr {
Str(String), // Messages to the user
// Val(MalType),
// Messages to the program
} TEMP TEMP */
pub type MalErr = String;
pub type MalArgs = Vec<MalType>;
pub type MalRet = Result<MalType, MalErr>;
use MalType::{Map, Symbol};
pub fn make_map(list: MalArgs) -> MalRet {
if list.len() % 2 != 0 {
return Err("Map length is odd: missing value".to_string());
}
let mut map = HashMap::new();
for i in (0..list.len()).step_by(2) {
match &list[i] {
Symbol(k) => {
let v = list[i + 1].clone();
map.insert(k.to_string(), v);
}
_ => return Err(format!("Map key not valid: {:?}", list[i])),
}
}
Ok(Map(map))
}
pub fn escape_str(s: &str) -> String {
format!(
"\"{}\"",
String::from(s)
.replace('\\', "\\\\")
.replace('\n', "\\n")
.replace('\"', "\\\"")
)
}
pub fn unescape_str(s: &str) -> String {
String::from(&s[1..s.len() - 1])
.replace("\\\\", "\\")
.replace("\\n", "\n")
.replace("\\\"", "\"")
}