Browse Source

Merge pull request #5 from aisamanra/gdritter/error-handling

Improve error-handling across the board
G. D. Ritter 1 year ago
parent
commit
ca946788c1
8 changed files with 487 additions and 165 deletions
  1. 36 7
      examples/person.matzo
  2. 9 1
      src/ast.rs
  3. 135 64
      src/builtins.rs
  4. 24 0
      src/core.rs
  5. 113 0
      src/errors.rs
  6. 113 65
      src/interp.rs
  7. 55 28
      src/lexer.rs
  8. 2 0
      src/lib.rs

+ 36 - 7
examples/person.matzo

@@ -2,7 +2,7 @@ gender:= Male | Female | Unspec;
 byGender := { [m,f,n] => { [Male] => m; [Female] => f; [_] => n }};
 
 ending  := byGender["o","a","e"];
-pronoun := byGender["He","She","They"];
+pronoun := byGender["he","she","they"];
 noun    := byGender["man","woman","person"];
 are     := byGender["is","is","are"];
 have    := byGender["has", "has", "have"];
@@ -22,6 +22,7 @@ job := { [g] =>
   | case g in
       { Male   => "fisherman"
       ; Female => "fisherwoman"
+      ; _      => "fisher"
       } };
 tool := { ["stonemason"] => "chisel"
         ; ["baker"]      => "bowl"
@@ -34,10 +35,38 @@ adjective ::= happy cheerful focused quiet meek rash;
 person :=
   let fix my-gender := gender in {
   let fix my-job := job[my-gender] in {
-    "You come across " str/capitalize[name[my-gender]] ", a " noun[my-gender]
-      " from the city of " str/capitalize[name[Female]] ". "
-      pronoun[my-gender] " " are[my-gender] " a hardworking " my-job " with "
-      hairColor " hair and " eyeColor " eyes. "
-      str/capitalize[pronoun[my-gender]] " "
-      have[my-gender] " a " tool[my-job] " and " are[my-gender] " very " adjective "." } };
+    se[
+      "You come across",
+      str/capitalize[name[my-gender]],
+      ", a",
+      noun[my-gender],
+      "from the city of",
+      str/capitalize[name[Female]]
+      "."
+    ]
+    " "
+    se[
+      pronoun[my-gender],
+      are[my-gender],
+      "a hardworking",
+      my-job,
+      "with",
+      hairColor,
+      "hair and",
+      eyeColor,
+      "eyes."
+    ]
+    " "
+    se[
+      pronoun[my-gender],
+      have[my-gender],
+      "a",
+      tool[my-job],
+      "and",
+      are[my-gender],
+      "very",
+      adjective,
+      "."
+    ]
+  } };
 puts person;

+ 9 - 1
src/ast.rs

@@ -77,7 +77,15 @@ impl ASTArena {
         FileRef { idx }
     }
 
+    pub fn get_file(&self, file: FileRef) -> &str {
+        &self.files[file.idx]
+    }
+
     pub fn get_line(&self, file: FileRef, span: Span) -> String {
+        if !span.exists() {
+            return String::new();
+        }
+
         let mut line_number = 1;
         let mut start_of_line = 0;
         let mut end_of_line = None;
@@ -87,7 +95,7 @@ impl ASTArena {
             if ch == '\n' {
                 if i < span.start as usize {
                     line_number += 1;
-                    start_of_line = i;
+                    start_of_line = i + 1;
                 }
                 if i >= span.end as usize && end_of_line.is_none() {
                     end_of_line = Some(i);

+ 135 - 64
src/builtins.rs

@@ -1,7 +1,25 @@
 use crate::ast::*;
+use crate::errors::MatzoError;
 use crate::interp::*;
 
-use anyhow::{bail, Error};
+fn arity_error(func: &str, expected: usize, actual: &[ExprRef]) -> Result<Value, MatzoError> {
+    let msg = format!(
+        "`{}`: expected {} argument{}, got {}",
+        func,
+        expected,
+        if expected == 1 { "" } else { "s" },
+        actual.len()
+    );
+    if actual.is_empty() {
+        Err(MatzoError::new(Span::empty(), msg))
+    } else {
+        let span = Span {
+            start: actual[0].span.start,
+            end: actual[actual.len() - 1].span.end,
+        };
+        Err(MatzoError::new(span, msg))
+    }
+}
 
 /// The list of builtins provided at startup.
 pub fn builtins() -> Vec<BuiltinFunc> {
@@ -9,16 +27,22 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "rep",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
                     if let [rep, expr] = exprs {
                         let mut buf = String::new();
-                        let num = state.eval(*rep, env)?.as_num(&state.ast.borrow())?;
+                        let num = state
+                            .eval(*rep, env)?
+                            .as_num(&state.ast.borrow(), rep.span)?;
                         for _ in 0..num {
-                            buf.push_str(state.eval(*expr, env)?.as_str(&state.ast.borrow())?);
+                            buf.push_str(
+                                state
+                                    .eval(*expr, env)?
+                                    .as_str(&state.ast.borrow(), expr.span)?,
+                            );
                         }
                         Ok(Value::Lit(Literal::Str(buf)))
                     } else {
-                        bail!("`rep`: expected two arguments, got {}", exprs.len())
+                        arity_error("rep", 2, exprs)
                     }
                 },
             ),
@@ -26,17 +50,14 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "str/upper",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
                     if let [expr] = exprs {
                         let s = state.eval(*expr, env)?;
                         Ok(Value::Lit(Literal::Str(
-                            s.as_str(&state.ast.borrow())?.to_uppercase(),
+                            s.as_str(&state.ast.borrow(), expr.span)?.to_uppercase(),
                         )))
                     } else {
-                        bail!(
-                            "`str/capitalize`: expected 1 argument1, got {}",
-                            exprs.len()
-                        );
+                        arity_error("str/upper", 1, exprs)
                     }
                 },
             ),
@@ -44,17 +65,14 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "str/capitalize",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
                     if let [expr] = exprs {
                         let s = state.eval(*expr, env)?;
                         Ok(Value::Lit(Literal::Str(titlecase::titlecase(
-                            s.as_str(&state.ast.borrow())?,
+                            s.as_str(&state.ast.borrow(), expr.span)?,
                         ))))
                     } else {
-                        bail!(
-                            "`str/capitalize`: expected 1 argument1, got {}",
-                            exprs.len()
-                        );
+                        arity_error("str/capitalize", 1, exprs)
                     }
                 },
             ),
@@ -62,28 +80,76 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "str/lower",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
                     if let [expr] = exprs {
                         let s = state.eval(*expr, env)?;
                         Ok(Value::Lit(Literal::Str(
-                            s.as_str(&state.ast.borrow())?.to_lowercase(),
+                            s.as_str(&state.ast.borrow(), expr.span)?.to_lowercase(),
                         )))
                     } else {
-                        bail!("`str/lower`: expected 1 argument1, got {}", exprs.len());
+                        arity_error("str/lower", 1, exprs)
+                    }
+                },
+            ),
+        },
+        BuiltinFunc {
+            name: "wd",
+            callback: Box::new(
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
+                    let mut buf = String::new();
+                    for expr in exprs {
+                        let s = state.eval(*expr, env)?;
+                        buf.push_str(s.as_str(&state.ast.borrow(), expr.span)?);
+                    }
+                    Ok(Value::Lit(Literal::Str(buf)))
+                },
+            ),
+        },
+        BuiltinFunc {
+            name: "se",
+            callback: Box::new(
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
+                    let mut buf = String::new();
+                    let mut capitalized = false;
+                    let mut last_char = '\0';
+                    for expr in exprs.iter() {
+                        let s = state.eval(*expr, env)?;
+                        let s = s.as_str(&state.ast.borrow(), expr.span)?;
+                        if !capitalized && !s.trim().is_empty() {
+                            capitalized = true;
+                            let mut chars = s.chars();
+                            for c in chars.next().unwrap().to_uppercase() {
+                                buf.push(c);
+                            }
+                            for c in chars {
+                                buf.push(c);
+                            }
+                        } else if last_char.is_alphanumeric()
+                            && s.chars().next().map_or(false, |c| c.is_alphanumeric())
+                        {
+                            buf.push(' ');
+                            buf.push_str(s.trim());
+                        } else {
+                            buf.push_str(s.trim());
+                        }
+                        if buf.len() > 0 {
+                            last_char = buf.chars().last().unwrap();
+                        }
                     }
+                    Ok(Value::Lit(Literal::Str(buf)))
                 },
             ),
         },
         BuiltinFunc {
             name: "add",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
                     if let [x, y] = exprs {
-                        let x = state.eval(*x, env)?.as_num(&state.ast.borrow())?;
-                        let y = state.eval(*y, env)?.as_num(&state.ast.borrow())?;
+                        let x = state.eval(*x, env)?.as_num(&state.ast.borrow(), x.span)?;
+                        let y = state.eval(*y, env)?.as_num(&state.ast.borrow(), y.span)?;
                         Ok(Value::Lit(Literal::Num(x + y)))
                     } else {
-                        bail!("`add`: expected 2 arguments, got {}", exprs.len());
+                        arity_error("add", 2, exprs)
                     }
                 },
             ),
@@ -91,13 +157,13 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "sub",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
                     if let [x, y] = exprs {
-                        let x = state.eval(*x, env)?.as_num(&state.ast.borrow())?;
-                        let y = state.eval(*y, env)?.as_num(&state.ast.borrow())?;
+                        let x = state.eval(*x, env)?.as_num(&state.ast.borrow(), x.span)?;
+                        let y = state.eval(*y, env)?.as_num(&state.ast.borrow(), y.span)?;
                         Ok(Value::Lit(Literal::Num(x - y)))
                     } else {
-                        bail!("`sub`: expected 2 arguments, got {}", exprs.len());
+                        arity_error("sub", 2, exprs)
                     }
                 },
             ),
@@ -105,13 +171,13 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "mul",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
                     if let [x, y] = exprs {
-                        let x = state.eval(*x, env)?.as_num(&state.ast.borrow())?;
-                        let y = state.eval(*y, env)?.as_num(&state.ast.borrow())?;
+                        let x = state.eval(*x, env)?.as_num(&state.ast.borrow(), x.span)?;
+                        let y = state.eval(*y, env)?.as_num(&state.ast.borrow(), y.span)?;
                         Ok(Value::Lit(Literal::Num(x * y)))
                     } else {
-                        bail!("`mul`: expected 2 arguments, got {}", exprs.len());
+                        arity_error("mul", 2, exprs)
                     }
                 },
             ),
@@ -119,14 +185,14 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "tuple/len",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
                     if let [expr] = exprs {
                         let tup = state.eval(*expr, env)?;
                         Ok(Value::Lit(Literal::Num(
-                            tup.as_tup(&state.ast.borrow())?.len() as i64,
+                            tup.as_tup(&state.ast.borrow(), expr.span)?.len() as i64,
                         )))
                     } else {
-                        bail!("`tuple/len`: expected 1 argument, got {}", exprs.len())
+                        arity_error("tuple/len", 1, exprs)
                     }
                 },
             ),
@@ -134,19 +200,22 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "tuple/concat",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
                     if let [expr] = exprs {
                         let val = state.eval(*expr, env)?;
-                        let tup = val.as_tup(&state.ast.borrow())?;
+                        let tup = val.as_tup(&state.ast.borrow(), expr.span)?;
                         let mut contents = Vec::new();
                         for elem in tup {
-                            for th in state.hnf(elem)?.as_tup(&state.ast.borrow())? {
+                            for th in state
+                                .hnf(elem)?
+                                .as_tup(&state.ast.borrow(), Span::empty())?
+                            {
                                 contents.push(th.clone());
                             }
                         }
                         Ok(Value::Tup(contents))
                     } else {
-                        bail!("tuple/concat: expected 1 argument, got {}", exprs.len());
+                        arity_error("tuple/concat", 1, exprs)
                     }
                 },
             ),
@@ -154,16 +223,16 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "tuple/index",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
-                    if let [tup, idx] = exprs {
-                        let tup = state.eval(*tup, env)?;
-                        let idx = state.eval(*idx, env)?;
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
+                    if let [tup_e, idx_e] = exprs {
+                        let tup = state.eval(*tup_e, env)?;
+                        let idx = state.eval(*idx_e, env)?;
                         state.hnf(
-                            &tup.as_tup(&state.ast.borrow())?
-                                [idx.as_num(&state.ast.borrow())? as usize],
+                            &tup.as_tup(&state.ast.borrow(), tup_e.span)?
+                                [idx.as_num(&state.ast.borrow(), idx_e.span)? as usize],
                         )
                     } else {
-                        bail!("`tuple/index`: expected 2 arguments, got {}", exprs.len());
+                        arity_error("tuple/index", 1, exprs)
                     }
                 },
             ),
@@ -171,11 +240,13 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "tuple/replace",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
                     if let [tup, idx, new] = exprs {
                         let tup_val = state.eval(*tup, env)?;
-                        let tup = tup_val.as_tup(&state.ast.borrow())?;
-                        let idx = state.eval(*idx, env)?.as_num(&state.ast.borrow())?;
+                        let tup = tup_val.as_tup(&state.ast.borrow(), tup.span)?;
+                        let idx = state
+                            .eval(*idx, env)?
+                            .as_num(&state.ast.borrow(), idx.span)?;
 
                         let mut modified = Vec::with_capacity(tup.len());
                         for i in 0..idx {
@@ -187,7 +258,7 @@ pub fn builtins() -> Vec<BuiltinFunc> {
                         }
                         Ok(Value::Tup(modified))
                     } else {
-                        bail!("`tuple/replace`: expected 3 arguments, got {}", exprs.len());
+                        arity_error("tuple/replace", 3, exprs)
                     }
                 },
             ),
@@ -195,22 +266,22 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "tuple/fold",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
-                    if let [func, init, tup] = exprs {
-                        let func = state.eval(*func, env)?;
-                        let tup = state.eval(*tup, env)?;
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
+                    if let [func_e, init, tup_e] = exprs {
+                        let func = state.eval(*func_e, env)?;
+                        let tup = state.eval(*tup_e, env)?;
 
                         let mut result = Thunk::Expr(*init, env.clone());
-                        for t in tup.as_tup(&state.ast.borrow())? {
+                        for t in tup.as_tup(&state.ast.borrow(), tup_e.span)? {
                             result = Thunk::Value(state.eval_closure(
-                                func.as_closure(&state.ast.borrow())?,
+                                func.as_closure(&state.ast.borrow(), func_e.span)?,
                                 vec![result, t.clone()],
                             )?);
                         }
 
                         state.hnf(&result)
                     } else {
-                        bail!("`tuple/fold`: expected 3 arguments, got {}", exprs.len());
+                        arity_error("tuple/fold", 3, exprs)
                     }
                 },
             ),
@@ -218,21 +289,21 @@ pub fn builtins() -> Vec<BuiltinFunc> {
         BuiltinFunc {
             name: "tuple/map",
             callback: Box::new(
-                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, Error> {
-                    if let [func, tup] = exprs {
-                        let func = state.eval(*func, env)?;
-                        let tup = state.eval(*tup, env)?;
+                |state: &State, exprs: &[ExprRef], env: &Env| -> Result<Value, MatzoError> {
+                    if let [func_e, tup_e] = exprs {
+                        let func = state.eval(*func_e, env)?;
+                        let tup = state.eval(*tup_e, env)?;
 
                         let mut new_tup = Vec::new();
-                        let closure = func.as_closure(&state.ast.borrow())?;
-                        for t in tup.as_tup(&state.ast.borrow())? {
+                        let closure = func.as_closure(&state.ast.borrow(), func_e.span)?;
+                        for t in tup.as_tup(&state.ast.borrow(), tup_e.span)? {
                             new_tup
                                 .push(Thunk::Value(state.eval_closure(closure, vec![t.clone()])?));
                         }
 
                         Ok(Value::Tup(new_tup))
                     } else {
-                        bail!("`tuple/map`: expected 2 arguments, got {}", exprs.len());
+                        arity_error("tuple/map", 2, exprs)
                     }
                 },
             ),

+ 24 - 0
src/core.rs

@@ -0,0 +1,24 @@
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub struct FileRef {
+    pub idx: usize,
+}
+
+/// A location in a source file
+#[derive(Debug, Clone, Copy)]
+pub struct Span {
+    pub start: u32,
+    pub end: u32,
+}
+
+impl Span {
+    pub fn empty() -> Span {
+        Span {
+            start: u32::MAX,
+            end: u32::MAX,
+        }
+    }
+
+    pub fn exists(&self) -> bool {
+        self.start != u32::MAX && self.end != u32::MAX
+    }
+}

+ 113 - 0
src/errors.rs

@@ -0,0 +1,113 @@
+use crate::core::Span;
+use crate::lexer;
+
+#[derive(Debug)]
+pub struct MatzoError {
+    pub message: String,
+    pub span: Span,
+    pub context: Vec<ContextLine>,
+}
+
+#[derive(Debug)]
+pub struct ContextLine {
+    pub message: String,
+    pub span: Span,
+}
+
+impl MatzoError {
+    pub fn new(span: Span, message: String) -> MatzoError {
+        MatzoError {
+            message,
+            span,
+            context: Vec::new(),
+        }
+    }
+
+    pub fn no_loc(message: String) -> MatzoError {
+        MatzoError {
+            message,
+            span: Span::empty(),
+            context: Vec::new(),
+        }
+    }
+
+    pub fn reposition(mut self, span: Span) -> MatzoError {
+        self.span = span;
+        self
+    }
+
+    fn format_expected_list(expected: Vec<String>) -> String {
+        if expected.len() == 1 {
+            expected[0].to_string()
+        } else {
+            let mut expected_list = String::new();
+            let num = expected.len();
+            for (i, exp) in expected.iter().enumerate() {
+                if i > 0 {
+                    expected_list.push_str(", ");
+                }
+                if i + 1 == num {
+                    expected_list.push_str("or ");
+                }
+                expected_list.push_str(exp);
+            }
+            expected_list
+        }
+    }
+
+    pub fn from_parse_error(
+        err: lalrpop_util::ParseError<usize, lexer::Token, lexer::LexerError>,
+    ) -> Self {
+        match err {
+            lalrpop_util::ParseError::User { error } => {
+                MatzoError::new(error.range, "Unrecognized token".to_string())
+            }
+            lalrpop_util::ParseError::UnrecognizedToken {
+                token: (start, tok, end),
+                expected,
+            } => {
+                let span = Span {
+                    start: start as u32,
+                    end: end as u32,
+                };
+
+                let expected = MatzoError::format_expected_list(expected);
+                MatzoError::new(
+                    span,
+                    format!("Unexpected {}. Expected {}", tok.token_name(), expected),
+                )
+            }
+            lalrpop_util::ParseError::UnrecognizedEOF { location, expected } => {
+                let span = Span {
+                    start: location as u32 - 1,
+                    end: location as u32,
+                };
+                let expected = MatzoError::format_expected_list(expected);
+                MatzoError::new(
+                    span,
+                    format!("Unexpected end-of-file; expected {}", expected),
+                )
+            }
+            lalrpop_util::ParseError::InvalidToken { .. } => {
+                panic!("Unexpected `InvalidToken`")
+            }
+            lalrpop_util::ParseError::ExtraToken {
+                token: (start, tok, end),
+            } => {
+                let span = Span {
+                    start: start as u32,
+                    end: end as u32,
+                };
+                MatzoError::new(span, format!("Extra token {}", tok.token_name()))
+            }
+        }
+    }
+}
+
+impl std::fmt::Display for MatzoError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(f, "MatzoError {{ .. }}")
+    }
+}
+
+impl std::error::Error for MatzoError {}

+ 113 - 65
src/interp.rs

@@ -1,13 +1,13 @@
 use crate::ast::*;
+use crate::errors::MatzoError;
 use crate::lexer::Span;
 use crate::rand::*;
 
-use anyhow::{anyhow, bail, Error};
+use anyhow::{bail, Error};
 use std::cell::RefCell;
 use std::collections::HashMap;
 use std::fmt;
 use std::io;
-use std::io::Write;
 use std::rc::Rc;
 
 /// A `Value` is a representation of the result of evaluation. Note
@@ -38,34 +38,45 @@ impl Value {
 
 impl Value {
     /// Convert this value to a Rust integer, failing otherwise
-    pub fn as_num(&self, ast: &ASTArena) -> Result<i64, Error> {
+    pub fn as_num(&self, ast: &ASTArena, span: Span) -> Result<i64, MatzoError> {
         match self {
             Value::Lit(Literal::Num(n)) => Ok(*n),
-            _ => self.with_str(ast, |s| bail!("Expected number, got {}", s)),
+            _ => self.with_str(ast, |s| {
+                return Err(MatzoError::new(span, format!("Expected number, got {}", s)));
+            }),
         }
     }
 
     /// Convert this value to a Rust string, failing otherwise
-    pub fn as_str(&self, ast: &ASTArena) -> Result<&str, Error> {
+    pub fn as_str(&self, ast: &ASTArena, span: Span) -> Result<&str, MatzoError> {
         match self {
             Value::Lit(Literal::Str(s)) => Ok(s),
-            _ => self.with_str(ast, |s| bail!("Expected string, got {}", s)),
+            _ => self.with_str(ast, |s| {
+                return Err(MatzoError::new(span, format!("Expected string, got {}", s)));
+            }),
         }
     }
 
     /// Convert this value to a Rust slice, failing otherwise
-    pub fn as_tup(&self, ast: &ASTArena) -> Result<&[Thunk], Error> {
+    pub fn as_tup(&self, ast: &ASTArena, span: Span) -> Result<&[Thunk], MatzoError> {
         match self {
             Value::Tup(vals) => Ok(vals),
-            _ => self.with_str(ast, |s| bail!("Expected tuple, got {}", s)),
+            _ => self.with_str(ast, |s| {
+                return Err(MatzoError::new(span, format!("Expected tuple, got {}", s)));
+            }),
         }
     }
 
     /// Convert this value to a closure, failing otherwise
-    pub fn as_closure(&self, ast: &ASTArena) -> Result<&Closure, Error> {
+    pub fn as_closure(&self, ast: &ASTArena, span: Span) -> Result<&Closure, MatzoError> {
         match self {
             Value::Closure(closure) => Ok(closure),
-            _ => self.with_str(ast, |s| bail!("Expected closure, got {}", s)),
+            _ => self.with_str(ast, |s| {
+                return Err(MatzoError::new(
+                    span,
+                    format!("Expected closure, got {}", s),
+                ));
+            }),
         }
     }
 
@@ -102,7 +113,7 @@ impl Value {
     }
 }
 
-type Callback = Box<dyn Fn(&State, &[ExprRef], &Env) -> Result<Value, Error>>;
+type Callback = Box<dyn Fn(&State, &[ExprRef], &Env) -> Result<Value, MatzoError>>;
 
 /// A representation of a builtin function implemented in Rust. This
 /// will be inserted into the global scope under the name provided as
@@ -238,7 +249,7 @@ impl State {
     /// Look up a `Name` in the provided `Env`. This will result in
     /// either a `Thunk` (i.e. the named value) or an error that
     /// indicates the missing name.
-    fn lookup(&self, env: &Env, name: Name) -> Result<Thunk, Error> {
+    fn lookup(&self, env: &Env, name: Name) -> Result<Thunk, MatzoError> {
         if let Some(env) = env {
             if let Some(ne) = env.vars.get(&name.item) {
                 Ok(ne.clone())
@@ -247,10 +258,10 @@ impl State {
             }
         } else {
             match self.root_scope.borrow().get(&name.item) {
-                None => {
-                    let span = self.ast.borrow().get_line(name.file, name.span);
-                    bail!("no such thing: {}\n{}", &self.ast.borrow()[name.item], span)
-                }
+                None => Err(MatzoError::new(
+                    name.span,
+                    format!("Undefined name {}", &self.ast.borrow()[name.item]),
+                )),
                 Some(ne) => Ok(ne.clone()),
             }
         }
@@ -259,42 +270,48 @@ impl State {
     /// Evaluate this string as a standalone program, writing the
     /// results to stdout.
     pub fn run(&self, src: &str) -> Result<(), Error> {
-        let lexed = crate::lexer::tokens(src);
-        let file = self.ast.borrow_mut().add_file(src.to_string());
-        let stmts = self
-            .parser
-            .parse(&mut self.ast.borrow_mut(), file, lexed)
-            .map_err(|err| anyhow!("Got {:?}", err))?;
-        let mut stdout = io::stdout();
-        for stmt in stmts {
-            self.execute(&stmt, &mut stdout)?;
+        self.run_with_writer(src, &mut io::stdout())
+    }
+
+    fn print_error(&self, file: FileRef, mtz: MatzoError) -> String {
+        let mut buf = String::new();
+        buf.push_str(&mtz.message);
+        buf.push('\n');
+        buf.push_str(&self.ast.borrow().get_line(file, mtz.span));
+        for ctx in mtz.context {
+            buf.push('\n');
+            buf.push_str(&ctx.message);
+            buf.push_str(&self.ast.borrow().get_line(file, ctx.span));
         }
-        Ok(())
+        buf
     }
 
     /// Evaluate this string as a standalone program, writing the
     /// results to the provided writer.
     pub fn run_with_writer(&self, src: &str, w: &mut impl std::io::Write) -> Result<(), Error> {
-        let lexed = crate::lexer::tokens(src);
         let file = self.ast.borrow_mut().add_file(src.to_string());
-        let stmts = self
-            .parser
-            .parse(&mut self.ast.borrow_mut(), file, lexed)
-            .map_err(|err| anyhow!("Got {:?}", err))?;
+        if let Err(mtz) = self.run_file(src, file, w) {
+            bail!("{}", self.print_error(file, mtz));
+        }
+        Ok(())
+    }
+
+    fn run_file(
+        &self,
+        src: &str,
+        file: FileRef,
+        mut w: &mut impl std::io::Write,
+    ) -> Result<(), MatzoError> {
+        let lexed = crate::lexer::tokens(src);
+        let stmts = self.parser.parse(&mut self.ast.borrow_mut(), file, lexed);
+        let stmts = stmts.map_err(MatzoError::from_parse_error)?;
         for stmt in stmts {
-            self.execute(&stmt, &mut *w)?;
+            self.execute(&stmt, &mut w)?;
         }
         Ok(())
     }
 
-    /// Evaluate this string as a fragment in a REPL, writing the
-    /// results to stdout. One way this differs from the standalone
-    /// program is that it actually tries parsing twice: first it
-    /// tries parsing the fragment normally, and then if that doesn't
-    /// work it tries adding a `puts` ahead of it: this is hacky, but
-    /// it allows the REPL to respond by printing values when someone
-    /// simply types an expression.
-    pub fn run_repl(&self, src: &str) -> Result<(), Error> {
+    fn repl_parse(&self, src: &str) -> Result<Vec<Stmt>, MatzoError> {
         let lexed = crate::lexer::tokens(src);
         let file = self.ast.borrow_mut().add_file(src.to_string());
         let stmts = {
@@ -302,26 +319,45 @@ impl State {
             self.parser.parse(&mut ast, file, lexed)
         };
         match stmts {
-            Ok(stmts) => {
-                for stmt in stmts {
-                    self.execute(&stmt, io::stdout())?;
-                }
-            }
+            Ok(stmts) => Ok(stmts),
             Err(err) => {
+                // this might have just been an expression instead, so
+                // try parsing a single expression to see if that
+                // works
                 let lexed = crate::lexer::tokens(src);
                 let expr = {
                     let mut ast = self.ast.borrow_mut();
                     self.expr_parser.parse(&mut ast, file, lexed)
                 };
                 if let Ok(expr) = expr {
-                    let val = self.eval(expr, &None)?;
-                    let val = self.force(val)?;
-                    writeln!(io::stdout(), "{}", val.to_string(&self.ast.borrow()))?;
+                    Ok(vec![Stmt::Puts(expr)])
                 } else {
-                    bail!("{:?}", err);
+                    Err(MatzoError::from_parse_error(err))
                 }
             }
-        };
+        }
+    }
+
+    /// Evaluate this string as a fragment in a REPL, writing the
+    /// results to stdout. One way this differs from the standalone
+    /// program is that it actually tries parsing twice: first it
+    /// tries parsing the fragment normally, and then if that doesn't
+    /// work it tries adding a `puts` ahead of it: this is hacky, but
+    /// it allows the REPL to respond by printing values when someone
+    /// simply types an expression.
+    pub fn run_repl(&self, src: &str) -> Result<(), Error> {
+        let file = self.ast.borrow_mut().add_file(src.to_string());
+        if let Err(mtz) = self.repl_main(src) {
+            bail!("{}", self.print_error(file, mtz));
+        }
+        Ok(())
+    }
+
+    fn repl_main(&self, src: &str) -> Result<(), MatzoError> {
+        let stmts = self.repl_parse(src)?;
+        for stmt in stmts {
+            self.execute(&stmt, io::stdout())?;
+        }
         Ok(())
     }
 
@@ -345,7 +381,7 @@ impl State {
     /// Execute this statement, writing any output to the provided
     /// output writer. Right now, this will always start in root
     /// scope: there are no statements within functions.
-    pub fn execute(&self, stmt: &Stmt, mut output: impl io::Write) -> Result<(), Error> {
+    pub fn execute(&self, stmt: &Stmt, mut output: impl io::Write) -> Result<(), MatzoError> {
         match stmt {
             // Evaluate the provided expression _all the way_
             // (i.e. recurisvely, not to WHNF) and write its
@@ -432,7 +468,7 @@ impl State {
     }
 
     /// Given a value, force it recursively.
-    fn force(&self, val: Value) -> Result<Value, Error> {
+    fn force(&self, val: Value) -> Result<Value, MatzoError> {
         match val {
             Value::Tup(values) => Ok(Value::Tup(
                 values
@@ -442,14 +478,14 @@ impl State {
                         let v = self.force(v)?;
                         Ok(Thunk::Value(v))
                     })
-                    .collect::<Result<Vec<Thunk>, Error>>()?,
+                    .collect::<Result<Vec<Thunk>, MatzoError>>()?,
             )),
             _ => Ok(val),
         }
     }
 
     /// Given a thunk, force it to WHNF.
-    pub fn hnf(&self, thunk: &Thunk) -> Result<Value, Error> {
+    pub fn hnf(&self, thunk: &Thunk) -> Result<Value, MatzoError> {
         match thunk {
             Thunk::Expr(expr, env) => self.eval(*expr, env),
             Thunk::Value(val) => Ok(val.clone()),
@@ -459,7 +495,7 @@ impl State {
 
     /// Given an `ExprRef` and an environment, fetch that expression
     /// and then evalute it in that environment
-    pub fn eval(&self, expr_ref: ExprRef, env: &Env) -> Result<Value, Error> {
+    pub fn eval(&self, expr_ref: ExprRef, env: &Env) -> Result<Value, MatzoError> {
         let expr = &self.ast.borrow()[expr_ref.item];
         match expr {
             // literals should be mostly cheap-ish to copy, so a
@@ -517,8 +553,10 @@ impl State {
             // for a range, choose randomly between the start and end
             // expressions
             Expr::Range(from, to) => {
-                let from = self.eval(*from, env)?.as_num(&self.ast.borrow())?;
-                let to = self.eval(*to, env)?.as_num(&self.ast.borrow())?;
+                let from = self
+                    .eval(*from, env)?
+                    .as_num(&self.ast.borrow(), from.span)?;
+                let to = self.eval(*to, env)?.as_num(&self.ast.borrow(), to.span)?;
                 Ok(Value::Lit(Literal::Num(
                     self.rand.borrow_mut().gen_range_i64(from, to + 1),
                 )))
@@ -544,7 +582,10 @@ impl State {
                     let builtin = &self.builtins[b.idx];
                     (builtin.callback)(self, vals, env)
                 }
-                _ => bail!("Bad function: {:?}", func),
+                _ => Err(MatzoError::new(
+                    expr_ref.span,
+                    "Trying to call a non-function".to_string(),
+                )),
             },
 
             // for a let-expression, create a new scope, add the new
@@ -619,13 +660,17 @@ impl State {
     /// function to mutably replace it with progressively more
     /// evaluated versions of the same expression, and then that's the
     /// thing we put into scope in the body of the function.
-    pub fn eval_closure(&self, closure: &Closure, mut scruts: Vec<Thunk>) -> Result<Value, Error> {
+    pub fn eval_closure(
+        &self,
+        closure: &Closure,
+        mut scruts: Vec<Thunk>,
+    ) -> Result<Value, MatzoError> {
         let ast = self.ast.borrow();
         let cases = match &ast[closure.func] {
             Expr::Fun(cases) => cases,
             Expr::Case(_, cases) => cases,
             // see the note attached to the definition of `Closure`
-            _ => bail!("INVARIANT FAILED"),
+            other => panic!("Expected a `Fun` or `Case` in a closure, found {:?}", other),
         };
 
         // for each case
@@ -660,7 +705,10 @@ impl State {
         }
 
         // we couldn't find a matching pattern, so throw an error
-        bail!("No pattern in {:?} matched {:?}", cases, scruts);
+        Err(MatzoError::new(
+            Span::empty(),
+            format!("No pattern matched {:?}", scruts),
+        ))
     }
 
     /// attempt to match the thunk `scrut` against the pattern
@@ -678,7 +726,7 @@ impl State {
         pat: &Pat,
         scrut: &mut Thunk,
         bindings: &mut Vec<(Name, Thunk)>,
-    ) -> Result<bool, Error> {
+    ) -> Result<bool, MatzoError> {
         if let Pat::Var(v) = pat {
             bindings.push((*v, scrut.clone()));
             return Ok(true);
@@ -728,9 +776,9 @@ impl State {
         }
     }
 
-    // this chooses an expressino from a choice, taking into account
+    // this chooses an expression from a choice, taking into account
     // the weights
-    fn choose(&self, choices: &[Choice], env: &Env) -> Result<Value, Error> {
+    fn choose(&self, choices: &[Choice], env: &Env) -> Result<Value, MatzoError> {
         let max = choices.iter().map(Choice::weight).sum();
         let mut choice = self.rand.borrow_mut().gen_range_i64(0, max);
         for ch in choices {
@@ -741,6 +789,6 @@ impl State {
         }
 
         // if we got here, it means our math was wrong
-        bail!("unreachable")
+        panic!("unreachable (bad math in `choose`)")
     }
 }

+ 55 - 28
src/lexer.rs

@@ -1,30 +1,6 @@
+pub use crate::core::{FileRef, Span};
 use logos::{Lexer, Logos};
 
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-pub struct FileRef {
-    pub idx: usize,
-}
-
-/// A location in a source file
-#[derive(Debug, Clone, Copy)]
-pub struct Span {
-    pub start: u32,
-    pub end: u32,
-}
-
-impl Span {
-    pub fn empty() -> Span {
-        Span {
-            start: u32::MAX,
-            end: u32::MAX,
-        }
-    }
-
-    pub fn exists(&self) -> bool {
-        self.start != u32::MAX && self.end != u32::MAX
-    }
-}
-
 #[derive(Debug, Clone, Copy)]
 pub struct Located<T> {
     pub item: T,
@@ -159,12 +135,58 @@ pub enum Token<'a> {
     Error,
 }
 
+impl<'a> Token<'a> {
+    pub fn token_name(&self) -> String {
+        match self {
+            Token::Var(v) => format!("variable `{}`", v),
+            Token::Atom(a) => format!("atom `{}`", a),
+            Token::Num(n) => format!("number `{}`", n),
+            Token::Str(s) => format!("string `{}`", s),
+
+            Token::LAngle => "`<`".to_string(),
+            Token::RAngle => "`>`".to_string(),
+
+            Token::LPar => "`(`".to_string(),
+            Token::RPar => "`)`".to_string(),
+
+            Token::LCurl => "`{`".to_string(),
+            Token::RCurl => "`}`".to_string(),
+
+            Token::LBrac => "`[`".to_string(),
+            Token::RBrac => "`]`".to_string(),
+
+            Token::Pipe => "`|`".to_string(),
+
+            Token::Colon => "`:`".to_string(),
+            Token::Comma => "`,`".to_string(),
+            Token::Semi => "`;`".to_string(),
+
+            Token::Dot => "`.`".to_string(),
+            Token::Underscore => "`_`".to_string(),
+            Token::DotDot => "`..`".to_string(),
+            Token::Arrow => "`=>`".to_string(),
+            Token::Assn => "`:=`".to_string(),
+
+            Token::LitAssn => "`::=`".to_string(),
+            Token::Puts => "`puts`".to_string(),
+            Token::Case => "`case`".to_string(),
+            Token::Let => "`let`".to_string(),
+            Token::In => "`in`".to_string(),
+            Token::Fix => "`fix`".to_string(),
+
+            Token::Error => "error".to_string(),
+        }
+    }
+}
+
 #[derive(Debug)]
-pub struct LexerError;
+pub struct LexerError {
+    pub range: Span,
+}
 
 impl std::fmt::Display for LexerError {
     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        write!(f, "LexerError")
+        write!(f, "LexerError({}..{})", self.range.start, self.range.end)
     }
 }
 
@@ -174,7 +196,12 @@ pub fn tokens(source: &str) -> impl Iterator<Item = Spanned<Token<'_>, usize, Le
     Token::lexer(source)
         .spanned()
         .map(move |(token, range)| match token {
-            Token::Error => Err(LexerError),
+            Token::Error => Err(LexerError {
+                range: Span {
+                    start: range.start as u32,
+                    end: range.end as u32,
+                },
+            }),
             token => Ok((range.start, token, range.end)),
         })
 }

+ 2 - 0
src/lib.rs

@@ -3,6 +3,8 @@ extern crate lalrpop_util;
 
 pub mod ast;
 pub mod builtins;
+pub mod core;
+pub mod errors;
 pub mod interp;
 pub mod lexer;
 pub mod rand;