Repository: abs-lang/abs
Pull request #125 (2019-01-22 01:22:02)

issue #38 v2: implementing token-based error location
Well, it turns out that any lexer.CurrentPosition() based approach is useless for error location because the lexer fast forward functions such as lexer.skipWhiteSpace(), lexer.readLine(), et al. change the current position before the interpreter reports the error. 

So, the new model does token-based position marking which is totally accurate (i.e. hack-free). In this model the lexer captures the current lexer.position in lexer.NextToken() (before it can move away) and saves it into the new Token.Position field. Since the parser already places the Token into each of the existing ast.Node.Token fields, we carry the token.Position along for the ride into the evaluator. And, as before, we pass the lexer pointer thru to the evaluator, but the REPL and testing modules now call a new BeginEval(node, *env, *lexer) function which, in turn, calls the recursive Eval(node, *env) function.

Thus, when we hit either a parser or evaluator error, their error location reporters pull the token.Location and pass it to the lexer.ErrorLine(pos) method which does the position look-up in the lexer.lineMap and returns the lineNum, column, and errorLine for formatting into the error location message.

Bottom line is that both the line numbers and columns are now accurate in both the parser and evaluator error messages without requiring any backtracking in the file.

I will withdraw the previous PR and, upon further review, we can go forward with this PR v2 instead.



Patch:
diff --git a/Makefile b/Makefile
index 0d95886..ab7c431 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,9 @@ build:
 test:
 	# The -vet=off is as YOLO as it gets
 	go test ./... -vet=off
+test_verbose:
+	# this will show successful error [line:col] tests per #38
+	CONTEXT='abs' go test ./... -v -vet=off
 repl:
 	go run main.go
 build_simple:
@@ -19,4 +22,4 @@ install:
 	go get -v github.com/mattn/go-colorable
 	go get -v github.com/mattn/go-tty
 	go get -d -v ./...
-travis: install test
\ No newline at end of file
+travis: install test
diff --git a/docs/misc/error.md b/docs/misc/error.md
index aafcb6f..ba56eba 100644
--- a/docs/misc/error.md
+++ b/docs/misc/error.md
@@ -1,16 +1,58 @@
 # Errors
 
-When using ABS, you might bump into errors within your code:
-when the interpreter finds an error, it will give up trying
-to evaluate the script and will exit with status code `99`:
+When using ABS, you might bump into errors within your code. When the interpreter finds an error, it will give up trying to execute the script and will exit with status code `99`.
 
+Note that there are 2 phases of the interpreter: parser and evaluator.
+
+When the parser phase encounters a syntax error it will continue to process the rest of the file and report all of the syntax errors it finds. 
+
+However, the evaluator phase will quit immediately when it encounters an evaluation error. Thus, you may need to run the ABS interpreter multiple times to find all the run-time errors.
+
+When you are running ABS interactively in the Run, Eval, Print Loop (REPL) the location of the error can only be the current line you just entered.
+
+However, when you are running ABS over a script file (even a small one) locating errors requires more help from the interpreter. ABS now provides `[line:column]` positions as well as the error line itself following the error message.
+
+For example, a file with syntax errors might look like this when the first syntax error is in line 4 somewhere around column 11.
+```
+$ cat examples/error-parse.abs
+# there are multiple parser errors in this file
+
+# this is a malformed identifier
+m.a = 'abc'
+
+# this is a command terminated with a semi
+d/d = $(command);
+
+# this is a command terminated with a LF
+c/c = $(command)
+
+# this is a bad infix operator
+b %% c
+
+$ abs examples/error-parse.abs
+ parser errors:
+	no prefix parse function for '=' found
+	[4:11]	m.a = 'abc'
+	no prefix parse function for '=' found
+	[7:16]	d/d = $(command);
+	no prefix parse function for '=' found
+	[10:16]	c/c = $(command)
+	no prefix parse function for '%' found
+	[13:6]	b %% c
+
+$ echo $?
+99
+```
+Furthermore, a file with evaluation errors might look like this when the first error encountered is in line 2 somewhere around column 11:
 ```
-$ cat examples/error.abs
+$ cat examples/error-eval.abs
+# there is an evaluation error on line 2
 1 + "hello"
 echo("should not reach here")
 
-$ abs examples/error.abs
+$ abs examples/error-eval.abs
 ERROR: type mismatch: NUMBER + STRING
+	[2:11]	1 + "hello"
 
 $ echo $?
 99
diff --git a/docs/misc/technical-details.md b/docs/misc/technical-details.md
index 9bdc056..83c8e8b 100644
--- a/docs/misc/technical-details.md
+++ b/docs/misc/technical-details.md
@@ -1,16 +1,12 @@
 # A few technical details...
 
-The ABS interpreter is built with Golang version `1.11`, and is mostly based
-on [the interpreter book](https://interpreterbook.com/) written by
-[Thorsten Ball](https://twitter.com/thorstenball).
+The ABS interpreter is built with Golang version `1.11`, and is mostly based on [the interpreter book](https://interpreterbook.com/) written by [Thorsten Ball](https://twitter.com/thorstenball).
 
-ABS is extremely different from Monkey, the "fictional" language the reader
-builds throughout the book, but the base structure (lexer, parser, evaluator)
-are still very much based on Thorsten's work.
+ABS is extremely different from Monkey, the "fictional" language the reader builds throughout the book, but the base structure (lexer, parser, evaluator) are still very much based on Thorsten's work.
 
 ## Why Go?
 
-There are multiple rasons Go's the ideal choice for ABS, in no
+There are multiple reasons Go is the ideal choice for ABS, in no
 particular order:
 
 * portability, as our goal is to be able to deliver ABS to
@@ -34,6 +30,71 @@ will probably work perfectly).
 With `make run` you can get inside a container built for ABS'
 development, and `make test` will run all tests.
 
+## Testing
+You can run the interpreter error location tests by invoking this bash script: `tests/test-abs.sh`.
+```
+$ tests/test-abs.sh
+
+=======================================
+Test Parser
+tests/test-parser.abs
+ parser errors:
+	no prefix parse function for '=' found
+	[4:11]	m.a = 'abc'
+	no prefix parse function for '=' found
+	[7:16]	d/d = $(command);
+	no prefix parse function for '=' found
+	[10:16]	c/c = $(command)
+	no prefix parse function for '%' found
+	[13:6]	b %% c
+	no prefix parse function for '&&' found
+	[22:4]	&&||!-/*5;
+	no prefix parse function for 'OR' found
+	[22:5]	&&||!-/*5;
+	no prefix parse function for '/' found
+	[22:8]	&&||!-/*5;
+	no prefix parse function for '<=>' found
+	[25:3]	<=>
+	expected next token to be ], got , instead
+	[44:3]	[1, 2];
+	no prefix parse function for ',' found
+	[44:5]	[1, 2];
+	no prefix parse function for ']' found
+	[44:7]	[1, 2];
+	no prefix parse function for '%' found
+	[68:2]	~%
+	no prefix parse function for '-=' found
+	[70:2]	-=
+	no prefix parse function for '/=' found
+	[72:2]	/=
+	no prefix parse function for '%=' found
+	[74:2]	%=
+	no prefix parse function for '^' found
+	[79:4]	&^>><<
+	no prefix parse function for '<<' found
+	[79:6]	&^>><<
+	Illegal token '$111'
+	[80:4]	$111
+	no prefix parse function for 'ILLEGAL' found
+	[76:7]	1.str()
+Exit code: 99
+
+=======================================
+Test Eval()
+tests/test-eval.abs
+ERROR: type mismatch: STRING + NUMBER
+	[8:35]	    s = s + 1   # this is a comment
+Exit code: 99
+
+ERROR: invalid property 'junk' on type ARRAY
+	[14:6]	    a.junk
+Exit code: 99
+
+ERROR: index operator not supported: f(x) {x} on HASH
+	[19:29]	    {"name": "Abs"}[f(x) {x}];  
+Exit code: 99
+
+```
 ## Roadmap
 
 We're currently working on the [preview-4](https://github.com/abs-lang/abs/milestone/7).
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index 2f70c0c..72a27fa 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -10,7 +10,9 @@ import (
 	"strings"
 
 	"github.com/abs-lang/abs/ast"
+	"github.com/abs-lang/abs/lexer"
 	"github.com/abs-lang/abs/object"
+	"github.com/abs-lang/abs/token"
 	"github.com/abs-lang/abs/util"
 )
 
@@ -22,15 +24,32 @@ var (
 	Fns   map[string]*object.Builtin
 )
 
+// This program's lexer used for error location in Eval(program)
+var lex *lexer.Lexer
+
 func init() {
 	Fns = getFns()
 }
 
-func newError(format string, a ...interface{}) *object.Error {
-	return &object.Error{Message: fmt.Sprintf(format, a...)}
+func newError(tok token.Token, format string, a ...interface{}) *object.Error {
+	// get the token position from the error node and append the offending line to the error message
+	lineNum, column, errorLine := lex.ErrorLine(tok.Position)
+	errorPosition := fmt.Sprintf("\n\t[%d:%d]\t%s", lineNum, column, errorLine)
+	return &object.Error{Message: fmt.Sprintf(format, a...) + errorPosition}
+}
+
+// BeginEval (program, env, lexer) object.Object
+// REPL and testing modules call this function to init the global lexer pointer for error location
+// NB. Eval(node, env) is recursive
+func BeginEval(program ast.Node, env *object.Environment, lexer *lexer.Lexer) object.Object {
+	// global lexer
+	lex = lexer
+	// run the evaluator
+	return Eval(program, env)
 }
 
 func Eval(node ast.Node, env *object.Environment) object.Object {
+
 	switch node := node.(type) {
 	// Statements
 	case *ast.Program:
@@ -59,13 +78,14 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 		return NULL
 	// Expressions
 	case *ast.NumberLiteral:
-		return &object.Number{Value: node.Value}
+		return &object.Number{Token: node.Token, Value: node.Value}
 
 	case *ast.NullLiteral:
-		return NULL
+		return &object.Null{Token: node.Token}
+		// return NULL
 
 	case *ast.StringLiteral:
-		return &object.String{Value: node.Value}
+		return &object.String{Token: node.Token, Value: node.Value}
 
 	case *ast.Boolean:
 		return nativeBoolToBooleanObject(node.Value)
@@ -75,7 +95,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 		if isError(right) {
 			return right
 		}
-		return evalPrefixExpression(node.Operator, right)
+		return evalPrefixExpression(node.Token, node.Operator, right)
 
 	case *ast.InfixExpression:
 		left := Eval(node.Left, env)
@@ -88,7 +108,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 			return right
 		}
 
-		return evalInfixExpression(node.Operator, left, right)
+		return evalInfixExpression(node.Token, node.Operator, left, right)
 
 	case *ast.CompoundAssignment:
 		left := Eval(node.Left, env)
@@ -101,7 +121,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 			return right
 		}
 
-		expr := evalInfixExpression(node.Operator[:len(node.Operator)-1], left, right)
+		expr := evalInfixExpression(node.Token, node.Operator[:len(node.Operator)-1], left, right)
 		env.Set(node.Left.String(), expr)
 		return NULL
 	case *ast.IfExpression:
@@ -122,7 +142,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 	case *ast.FunctionLiteral:
 		params := node.Parameters
 		body := node.Body
-		return &object.Function{Parameters: params, Env: env, Body: body}
+		return &object.Function{Token: node.Token, Parameters: params, Env: env, Body: body}
 
 	case *ast.CallExpression:
 		function := Eval(node.Function, env)
@@ -135,7 +155,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 			return args[0]
 		}
 
-		return applyFunction(function, args)
+		return applyFunction(node.Token, function, args)
 
 	case *ast.MethodExpression:
 		o := Eval(node.Object, env)
@@ -148,7 +168,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 			return args[0]
 		}
 
-		return applyMethod(o, node.Method.String(), args)
+		return applyMethod(node.Token, o, node.Method.String(), args)
 
 	case *ast.PropertyExpression:
 		return evalPropertyExpression(node, env)
@@ -157,7 +177,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 		if len(elements) == 1 && isError(elements[0]) {
 			return elements[0]
 		}
-		return &object.Array{Elements: elements}
+		return &object.Array{Token: node.Token, Elements: elements}
 
 	case *ast.IndexExpression:
 		left := Eval(node.Left, env)
@@ -168,13 +188,13 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 		if isError(index) {
 			return index
 		}
-		return evalIndexExpression(left, index)
+		return evalIndexExpression(node.Token, left, index)
 
 	case *ast.HashLiteral:
 		return evalHashLiteral(node, env)
 
 	case *ast.CommandExpression:
-		return evalCommandExpression(node.Value, env)
+		return evalCommandExpression(node.Token, node.Value, env)
 
 	}
 
@@ -230,7 +250,7 @@ func evalAssignment(as *ast.AssignStatement, env *object.Environment) object.Obj
 	// destructuring x, y = [1, 2]
 	if len(as.Names) > 0 {
 		if val.Type() != object.ARRAY_OBJ {
-			return newError("wrong assignment, expected identifier or array destructuring, got %s (%s)", val.Type(), val.Inspect())
+			return newError(as.Token, "wrong assignment, expected identifier or array destructuring, got %s (%s)", val.Type(), val.Inspect())
 		}
 
 		elements := val.(*object.Array).Elements
@@ -256,21 +276,21 @@ func nativeBoolToBooleanObject(input bool) *object.Boolean {
 	return FALSE
 }
 
-func evalPrefixExpression(operator string, right object.Object) object.Object {
+func evalPrefixExpression(tok token.Token, operator string, right object.Object) object.Object {
 	switch operator {
 	case "!":
 		return evalBangOperatorExpression(right)
 	case "-":
-		return evalMinusPrefixOperatorExpression(right)
+		return evalMinusPrefixOperatorExpression(tok, right)
 	case "~":
-		return evalTildePrefixOperatorExpression(right)
+		return evalTildePrefixOperatorExpression(tok, right)
 	default:
-		return newError("unknown operator: %s%s", operator, right.Type())
+		return newError(tok, "unknown operator: %s%s", operator, right.Type())
 	}
 }
 
 func evalInfixExpression(
-	operator string,
+	tok token.Token, operator string,
 	left, right object.Object,
 ) object.Object {
 	switch {
@@ -301,19 +321,19 @@ func evalInfixExpression(
 		fmt.Printf("%s %s %s", operator, left, right)
 		return nil
 	case left.Type() == object.NUMBER_OBJ && right.Type() == object.NUMBER_OBJ:
-		return evalNumberInfixExpression(operator, left, right)
+		return evalNumberInfixExpression(tok, operator, left, right)
 	case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ:
-		return evalStringInfixExpression(operator, left, right)
+		return evalStringInfixExpression(tok, operator, left, right)
 	case left.Type() == object.ARRAY_OBJ && right.Type() == object.ARRAY_OBJ:
-		return evalArrayInfixExpression(operator, left, right)
+		return evalArrayInfixExpression(tok, operator, left, right)
 	case operator == "==":
 		return nativeBoolToBooleanObject(left == right)
 	case operator == "!=":
 		return nativeBoolToBooleanObject(left != right)
 	case left.Type() != right.Type():
-		return newError("type mismatch: %s %s %s", left.Type(), operator, right.Type())
+		return newError(tok, "type mismatch: %s %s %s", left.Type(), operator, right.Type())
 	default:
-		return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
+		return newError(tok, "unknown operator: %s %s %s", left.Type(), operator, right.Type())
 	}
 }
 
@@ -345,18 +365,18 @@ func evalBangOperatorExpression(right object.Object) object.Object {
 	}
 }
 
-func evalTildePrefixOperatorExpression(right object.Object) object.Object {
+func evalTildePrefixOperatorExpression(tok token.Token, right object.Object) object.Object {
 	switch o := right.(type) {
 	case *object.Number:
 		return &object.Number{Value: float64(^int64(o.Value))}
 	default:
-		return newError("Bitwise not (~) can only be applied to numbers, got %s (%s)", o.Type(), o.Inspect())
+		return newError(tok, "Bitwise not (~) can only be applied to numbers, got %s (%s)", o.Type(), o.Inspect())
 	}
 }
 
-func evalMinusPrefixOperatorExpression(right object.Object) object.Object {
+func evalMinusPrefixOperatorExpression(tok token.Token, right object.Object) object.Object {
 	if right.Type() != object.NUMBER_OBJ {
-		return newError("unknown operator: -%s", right.Type())
+		return newError(tok, "unknown operator: -%s", right.Type())
 	}
 
 	value := right.(*object.Number).Value
@@ -364,7 +384,7 @@ func evalMinusPrefixOperatorExpression(right object.Object) object.Object {
 }
 
 func evalNumberInfixExpression(
-	operator string,
+	tok token.Token, operator string,
 	left, right object.Object,
 ) object.Object {
 	leftVal := left.(*object.Number).Value
@@ -372,18 +392,18 @@ func evalNumberInfixExpression(
 
 	switch operator {
 	case "+":
-		return &object.Number{Value: leftVal + rightVal}
+		return &object.Number{Token: tok, Value: leftVal + rightVal}
 	case "-":
-		return &object.Number{Value: leftVal - rightVal}
+		return &object.Number{Token: tok, Value: leftVal - rightVal}
 	case "*":
-		return &object.Number{Value: leftVal * rightVal}
+		return &object.Number{Token: tok, Value: leftVal * rightVal}
 	case "/":
-		return &object.Number{Value: leftVal / rightVal}
+		return &object.Number{Token: tok, Value: leftVal / rightVal}
 	case "**":
 		// TODO this does not support floats
-		return &object.Number{Value: math.Pow(leftVal, rightVal)}
+		return &object.Number{Token: tok, Value: math.Pow(leftVal, rightVal)}
 	case "%":
-		return &object.Number{Value: math.Mod(leftVal, rightVal)}
+		return &object.Number{Token: tok, Value: math.Mod(leftVal, rightVal)}
 	case "<":
 		return nativeBoolToBooleanObject(leftVal < rightVal)
 	case ">":
@@ -393,7 +413,7 @@ func evalNumberInfixExpression(
 	case ">=":
 		return nativeBoolToBooleanObject(leftVal >= rightVal)
 	case "<=>":
-		i := &object.Number{}
+		i := &object.Number{Token: tok}
 
 		if leftVal == rightVal {
 			i.Value = 0
@@ -409,64 +429,66 @@ func evalNumberInfixExpression(
 	case "!=":
 		return nativeBoolToBooleanObject(leftVal != rightVal)
 	case "&":
-		return &object.Number{Value: float64(int64(leftVal) & int64(rightVal))}
+		return &object.Number{Token: tok, Value: float64(int64(leftVal) & int64(rightVal))}
 	case "|":
-		return &object.Number{Value: float64(int64(leftVal) | int64(rightVal))}
+		return &object.Number{Token: tok, Value: float64(int64(leftVal) | int64(rightVal))}
 	case ">>":
-		return &object.Number{Value: float64(uint64(leftVal) >> uint64(rightVal))}
+		return &object.Number{Token: tok, Value: float64(uint64(leftVal) >> uint64(rightVal))}
 	case "<<":
-		return &object.Number{Value: float64(uint64(leftVal) << uint64(rightVal))}
+		return &object.Number{Token: tok, Value: float64(uint64(leftVal) << uint64(rightVal))}
 	case "^":
-		return &object.Number{Value: float64(int64(leftVal) ^ int64(rightVal))}
+		return &object.Number{Token: tok, Value: float64(int64(leftVal) ^ int64(rightVal))}
 	// A range results in an array of integers from left to right
 	case "..":
 		a := make([]object.Object, 0)
 
 		for i := leftVal; i <= rightVal; i++ {
-			a = append(a, &object.Number{Value: float64(i)})
+			a = append(a, &object.Number{Token: tok, Value: float64(i)})
 		}
-		return &object.Array{Elements: a}
+		return &object.Array{Token: tok, Elements: a}
 	default:
-		return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
+		return newError(tok, "unknown operator: %s %s %s", left.Type(), operator, right.Type())
 	}
 }
 
 func evalStringInfixExpression(
+	tok token.Token,
 	operator string,
 	left, right object.Object,
 ) object.Object {
 	if operator == "+" {
 		leftVal := left.(*object.String).Value
 		rightVal := right.(*object.String).Value
-		return &object.String{Value: leftVal + rightVal}
+		return &object.String{Token: tok, Value: leftVal + rightVal}
 	}
 
 	if operator == "==" {
-		return &object.Boolean{Value: left.(*object.String).Value == right.(*object.String).Value}
+		return &object.Boolean{Token: tok, Value: left.(*object.String).Value == right.(*object.String).Value}
 	}
 
 	if operator == "!=" {
-		return &object.Boolean{Value: left.(*object.String).Value != right.(*object.String).Value}
+		return &object.Boolean{Token: tok, Value: left.(*object.String).Value != right.(*object.String).Value}
 	}
 
 	if operator == "~" {
-		return &object.Boolean{Value: strings.ToLower(left.(*object.String).Value) == strings.ToLower(right.(*object.String).Value)}
+		return &object.Boolean{Token: tok, Value: strings.ToLower(left.(*object.String).Value) == strings.ToLower(right.(*object.String).Value)}
 	}
 
-	return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
+	return newError(tok, "unknown operator: %s %s %s", left.Type(), operator, right.Type())
 }
 
 func evalArrayInfixExpression(
+	tok token.Token,
 	operator string,
 	left, right object.Object,
 ) object.Object {
 	if operator == "+" {
 		leftVal := left.(*object.Array).Elements
 		rightVal := right.(*object.Array).Elements
-		return &object.Array{Elements: append(leftVal, rightVal...)}
+		return &object.Array{Token: tok, Elements: append(leftVal, rightVal...)}
 	}
 
-	return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
+	return newError(tok, "unknown operator: %s %s %s", left.Type(), operator, right.Type())
 }
 
 func evalIfExpression(
@@ -603,12 +625,12 @@ func evalForInExpression(
 		return loopIterable(i.Next, env, fie, 0)
 	case *object.Builtin:
 		if i.Next == nil {
-			return newError("builtin function cannot be used in loop")
+			return newError(fie.Token, "builtin function cannot be used in loop")
 		}
 
 		return loopIterable(i.Next, env, fie, 0)
 	default:
-		return newError("'%s' is a %s, not an iterable, cannot be used in for loop", i.Inspect(), i.Type())
+		return newError(fie.Token, "'%s' is a %s, not an iterable, cannot be used in for loop", i.Inspect(), i.Type())
 	}
 }
 
@@ -621,7 +643,7 @@ func loopIterable(next func(int) (int, object.Object), env *object.Environment,
 
 	// set the special k v variables in the
 	// environment
-	env.Set(fie.Key, &object.Number{Value: float64(k)})
+	env.Set(fie.Key, &object.Number{Token: fie.Token, Value: float64(k)})
 	env.Set(fie.Value, v)
 	err := Eval(fie.Block, env)
 
@@ -648,7 +670,7 @@ func evalIdentifier(
 		return builtin
 	}
 
-	return newError("identifier not found: " + node.Value)
+	return newError(node.Token, "identifier not found: "+node.Value)
 }
 
 // This is the core of ABS's logical
@@ -733,13 +755,13 @@ func evalPropertyExpression(pe *ast.PropertyExpression, env *object.Environment)
 			return FALSE
 		}
 	case *object.Hash:
-		return evalHashIndexExpression(obj, &object.String{Value: pe.Property.String()})
+		return evalHashIndexExpression(obj.Token, obj, &object.String{Token: pe.Token, Value: pe.Property.String()})
 	}
 
-	return newError("invalid property '%s' on type %s", pe.Property.String(), o.Type())
+	return newError(pe.Token, "invalid property '%s' on type %s", pe.Property.String(), o.Type())
 }
 
-func applyFunction(fn object.Object, args []object.Object) object.Object {
+func applyFunction(tok token.Token, fn object.Object, args []object.Object) object.Object {
 	switch fn := fn.(type) {
 
 	case *object.Function:
@@ -755,19 +777,19 @@ func applyFunction(fn object.Object, args []object.Object) object.Object {
 		return fn.Fn(args...)
 
 	default:
-		return newError("not a function: %s", fn.Type())
+		return newError(tok, "not a function: %s", fn.Type())
 	}
 }
 
-func applyMethod(o object.Object, method string, args []object.Object) object.Object {
+func applyMethod(tok token.Token, o object.Object, method string, args []object.Object) object.Object {
 	f, ok := Fns[method]
 
 	if !ok {
-		return newError("%s does not have method '%s()'", o.Type(), method)
+		return newError(tok, "%s does not have method '%s()'", o.Type(), method)
 	}
 
 	if !util.Contains(f.Types, string(o.Type())) && len(f.Types) != 0 {
-		return newError("cannot call method '%s()' on '%s'", method, o.Type())
+		return newError(tok, "cannot call method '%s()' on '%s'", method, o.Type())
 	}
 
 	args = append([]object.Object{o}, args...)
@@ -781,7 +803,7 @@ func extendFunctionEnv(
 	env := object.NewEnclosedEnvironment(fn.Env)
 
 	if len(args) != len(fn.Parameters) {
-		return nil, newError("Wrong number of arguments passed to %s. Want %s, got %s", fn.Inspect(), fn.Parameters, args)
+		return nil, newError(fn.Token, "Wrong number of arguments passed to %s. Want %s, got %s", fn.Inspect(), fn.Parameters, args)
 	}
 
 	for paramIdx, param := range fn.Parameters {
@@ -799,20 +821,20 @@ func unwrapReturnValue(obj object.Object) object.Object {
 	return obj
 }
 
-func evalIndexExpression(left, index object.Object) object.Object {
+func evalIndexExpression(tok token.Token, left, index object.Object) object.Object {
 	switch {
 	case left.Type() == object.ARRAY_OBJ && index.Type() == object.NUMBER_OBJ:
 		return evalArrayIndexExpression(left, index)
 	case left.Type() == object.HASH_OBJ && index.Type() == object.STRING_OBJ:
-		return evalHashIndexExpression(left, index)
+		return evalHashIndexExpression(tok, left, index)
 	case left.Type() == object.STRING_OBJ && index.Type() == object.NUMBER_OBJ:
-		return evalStringIndexExpression(left, index)
+		return evalStringIndexExpression(tok, left, index)
 	default:
-		return newError("index operator not supported: %s on %s", index.Inspect(), left.Type())
+		return newError(tok, "index operator not supported: %s on %s", index.Inspect(), left.Type())
 	}
 }
 
-func evalStringIndexExpression(array, index object.Object) object.Object {
+func evalStringIndexExpression(tok token.Token, array, index object.Object) object.Object {
 	stringObject := array.(*object.String)
 	idx := index.(*object.Number).Int()
 	max := len(stringObject.Value) - 1
@@ -821,7 +843,7 @@ func evalStringIndexExpression(array, index object.Object) object.Object {
 		return NULL
 	}
 
-	return &object.String{Value: string(stringObject.Value[idx])}
+	return &object.String{Token: tok, Value: string(stringObject.Value[idx])}
 }
 
 func evalArrayIndexExpression(array, index object.Object) object.Object {
@@ -850,7 +872,7 @@ func evalHashLiteral(
 
 		hashKey, ok := key.(object.Hashable)
 		if !ok {
-			return newError("unusable as hash key: %s", key.Type())
+			return newError(node.Token, "unusable as hash key: %s", key.Type())
 		}
 
 		value := Eval(valueNode, env)
@@ -865,12 +887,12 @@ func evalHashLiteral(
 	return &object.Hash{Pairs: pairs}
 }
 
-func evalHashIndexExpression(hash, index object.Object) object.Object {
+func evalHashIndexExpression(tok token.Token, hash, index object.Object) object.Object {
 	hashObject := hash.(*object.Hash)
 
 	key, ok := index.(object.Hashable)
 	if !ok {
-		return newError("unusable as hash key: %s", index.Type())
+		return newError(tok, "unusable as hash key: %s", index.Type())
 	}
 
 	pair, ok := hashObject.Pairs[key.HashKey()]
@@ -881,7 +903,7 @@ func evalHashIndexExpression(hash, index object.Object) object.Object {
 	return pair.Value
 }
 
-func evalCommandExpression(cmd string, env *object.Environment) object.Object {
+func evalCommandExpression(tok token.Token, cmd string, env *object.Environment) object.Object {
 	// Match all strings preceded by
 	// a $ or a \$
 	re := regexp.MustCompile("(\\\\)?\\$([a-zA-Z_]{1,})")
@@ -920,8 +942,8 @@ func evalCommandExpression(cmd string, env *object.Environment) object.Object {
 	err := c.Run()
 
 	if err != nil {
-		return &object.String{Value: stderr.String(), Ok: FALSE}
+		return &object.String{Token: tok, Value: stderr.String(), Ok: FALSE}
 	}
 
-	return &object.String{Value: strings.Trim(out.String(), "\n"), Ok: TRUE}
+	return &object.String{Token: tok, Value: strings.Trim(out.String(), "\n"), Ok: TRUE}
 }
diff --git a/evaluator/functions.go b/evaluator/functions.go
index ed8896c..cce1178 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -14,26 +14,28 @@ import (
 	"github.com/abs-lang/abs/lexer"
 	"github.com/abs-lang/abs/object"
 	"github.com/abs-lang/abs/parser"
+	"github.com/abs-lang/abs/token"
 	"github.com/abs-lang/abs/util"
 )
 
 var scanner *bufio.Scanner
+var tok token.Token
 
 func init() {
 	scanner = bufio.NewScanner(os.Stdin)
-
+	tok = token.Token{Type: token.FUNCTION, Position: 0, Literal: "BuiltinFunction"}
 }
 
 // Utility function that validates arguments passed to builtin
 // functions.
 func validateArgs(name string, args []object.Object, size int, types [][]string) object.Object {
 	if len(args) != size {
-		return newError("wrong number of arguments to %s(...): got=%d, want=%d", name, len(args), size)
+		return newError(tok, "wrong number of arguments to %s(...): got=%d, want=%d", name, len(args), size)
 	}
 
 	for i, t := range types {
 		if !util.Contains(t, string(args[i].Type())) {
-			return newError("argument %d to %s(...) is not supported (got: %s, allowed: %s)", i, name, args[i].Inspect(), strings.Join(t, ", "))
+			return newError(tok, "argument %d to %s(...) is not supported (got: %s, allowed: %s)", i, name, args[i].Inspect(), strings.Join(t, ", "))
 		}
 	}
 
@@ -53,12 +55,11 @@ func getFns() map[string]*object.Builtin {
 
 				switch arg := args[0].(type) {
 				case *object.Array:
-					return &object.Number{Value: float64(len(arg.Elements))}
+					return &object.Number{Token: tok, Value: float64(len(arg.Elements))}
 				case *object.String:
-					return &object.Number{Value: float64(len(arg.Value))}
+					return &object.Number{Token: tok, Value: float64(len(arg.Value))}
 				default:
-					return newError("argument to `len` not supported, got %s",
-						args[0].Type())
+					return newError(tok, "argument to `len` not supported, got %s", args[0].Type())
 				}
 			},
 		},
@@ -75,10 +76,10 @@ func getFns() map[string]*object.Builtin {
 				r, e := rand.Int(rand.Reader, big.NewInt(int64(arg.Value)))
 
 				if e != nil {
-					return newError("error occurred while calling 'rand(%v)': %s", arg.Value, e.Error())
+					return newError(tok, "error occurred while calling 'rand(%v)': %s", arg.Value, e.Error())
 				}
 
-				return &object.Number{Value: float64(r.Int64())}
+				return &object.Number{Token: tok, Value: float64(r.Int64())}
 			},
 		},
 		// exit(code:0)
@@ -103,7 +104,7 @@ func getFns() map[string]*object.Builtin {
 		"flag": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
 			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("exit", args, 1, [][]string{{object.STRING_OBJ}})
+				err := validateArgs("flag", args, 1, [][]string{{object.STRING_OBJ}})
 				if err != nil {
 					return err
 				}
@@ -129,7 +130,7 @@ func getFns() map[string]*object.Builtin {
 
 						// else return the next argument
 						// eg --flag1 something --flag2
-						return &object.String{Value: v}
+						return &object.String{Token: tok, Value: v}
 					}
 
 					// try to parse the flag as key=value
@@ -142,7 +143,7 @@ func getFns() map[string]*object.Builtin {
 					// ..BINGO!
 					if (len(left) > 1 && left[1:] == name.Value) || (len(left) > 2 && left[2:] == name.Value) {
 						if len(parts) > 1 {
-							return &object.String{Value: parts[1]}
+							return &object.String{Token: tok, Value: parts[1]}
 						} else {
 							found = true
 						}
@@ -153,7 +154,7 @@ func getFns() map[string]*object.Builtin {
 				// it means no value was assigned to it,
 				// so let's default to true
 				if found {
-					return &object.Boolean{Value: true}
+					return &object.Boolean{Token: tok, Value: true}
 				}
 
 				// else a flag that's not found is NULL
@@ -166,9 +167,9 @@ func getFns() map[string]*object.Builtin {
 			Fn: func(args ...object.Object) object.Object {
 				dir, err := os.Getwd()
 				if err != nil {
-					return newError(err.Error())
+					return newError(tok, err.Error())
 				}
-				return &object.String{Value: dir}
+				return &object.String{Token: tok, Value: dir}
 			},
 		},
 		// echo(arg:"hello")
@@ -199,18 +200,18 @@ func getFns() map[string]*object.Builtin {
 
 				switch arg := args[0].(type) {
 				case *object.Number:
-					return &object.Number{Value: float64(int64(arg.Value))}
+					return &object.Number{Token: tok, Value: float64(int64(arg.Value))}
 				case *object.String:
 					i, err := strconv.ParseFloat(arg.Value, 64)
 
 					if err != nil {
-						return newError("int(...) can only be called on strings which represent numbers, '%s' given", arg.Value)
+						return newError(tok, "int(...) can only be called on strings which represent numbers, '%s' given", arg.Value)
 					}
 
-					return &object.Number{Value: float64(int64(i))}
+					return &object.Number{Token: tok, Value: float64(int64(i))}
 				default:
 					// we will never reach here
-					return newError("argument to `int` not supported, got %s", args[0].Type())
+					return newError(tok, "argument to `int` not supported, got %s", args[0].Type())
 				}
 			},
 		},
@@ -230,13 +231,13 @@ func getFns() map[string]*object.Builtin {
 					i, err := strconv.ParseFloat(arg.Value, 64)
 
 					if err != nil {
-						return newError("number(...) can only be called on strings which represent numbers, '%s' given", arg.Value)
+						return newError(tok, "number(...) can only be called on strings which represent numbers, '%s' given", arg.Value)
 					}
 
-					return &object.Number{Value: i}
+					return &object.Number{Token: tok, Value: i}
 				default:
 					// we will never reach here
-					return newError("argument to `number` not supported, got %s", args[0].Type())
+					return newError(tok, "argument to `number` not supported, got %s", args[0].Type())
 				}
 			},
 		},
@@ -251,14 +252,14 @@ func getFns() map[string]*object.Builtin {
 
 				switch arg := args[0].(type) {
 				case *object.Number:
-					return &object.Boolean{Value: true}
+					return &object.Boolean{Token: tok, Value: true}
 				case *object.String:
 					_, err := strconv.ParseFloat(arg.Value, 64)
 
-					return &object.Boolean{Value: err == nil}
+					return &object.Boolean{Token: tok, Value: err == nil}
 				default:
 					// we will never reach here
-					return newError("argument to `is_number` not supported, got %s", args[0].Type())
+					return newError(tok, "argument to `is_number` not supported, got %s", args[0].Type())
 				}
 			},
 		},
@@ -271,7 +272,7 @@ func getFns() map[string]*object.Builtin {
 					return pos, EOF
 				}
 
-				return pos, &object.String{Value: scanner.Text()}
+				return pos, &object.String{Token: tok, Value: scanner.Text()}
 			},
 			Types: []string{},
 			Fn: func(args ...object.Object) object.Object {
@@ -281,7 +282,7 @@ func getFns() map[string]*object.Builtin {
 					return EOF
 				}
 
-				return &object.String{Value: scanner.Text()}
+				return &object.String{Token: tok, Value: scanner.Text()}
 			},
 		},
 		// env(variable:"PWD")
@@ -294,7 +295,7 @@ func getFns() map[string]*object.Builtin {
 				}
 
 				arg := args[0].(*object.String)
-				return &object.String{Value: os.Getenv(arg.Value)}
+				return &object.String{Token: tok, Value: os.Getenv(arg.Value)}
 			},
 		},
 		// arg(position:1)
@@ -310,10 +311,10 @@ func getFns() map[string]*object.Builtin {
 				i := arg.Int()
 
 				if int(i) > len(os.Args)-1 {
-					return &object.String{Value: ""}
+					return &object.String{Token: tok, Value: ""}
 				}
 
-				return &object.String{Value: os.Args[i]}
+				return &object.String{Token: tok, Value: os.Args[i]}
 			},
 		},
 		// type(variable:"hello")
@@ -325,7 +326,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.String{Value: string(args[0].Type())}
+				return &object.String{Token: tok, Value: string(args[0].Type())}
 			},
 		},
 		// split(string:"hello")
@@ -345,7 +346,7 @@ func getFns() map[string]*object.Builtin {
 				elements := make([]object.Object, length, length)
 
 				for k, v := range parts {
-					elements[k] = &object.String{Value: v}
+					elements[k] = &object.String{Token: tok, Value: v}
 				}
 
 				return &object.Array{Elements: elements}
@@ -368,7 +369,7 @@ func getFns() map[string]*object.Builtin {
 				elements := make([]object.Object, length, length)
 
 				for k, v := range parts {
-					elements[k] = &object.String{Value: v}
+					elements[k] = &object.String{Token: tok, Value: v}
 				}
 
 				return &object.Array{Elements: elements}
@@ -410,7 +411,7 @@ func getFns() map[string]*object.Builtin {
 					return evalHashLiteral(hl, env)
 				}
 
-				return newError("argument to `json` must be a valid JSON object, got '%s'", s.Value)
+				return newError(tok, "argument to `json` must be a valid JSON object, got '%s'", s.Value)
 			},
 		},
 		// "a %s".fmt(b)
@@ -428,7 +429,7 @@ func getFns() map[string]*object.Builtin {
 					list = append(list, s.Inspect())
 				}
 
-				return &object.String{Value: fmt.Sprintf(args[0].(*object.String).Value, list...)}
+				return &object.String{Token: tok, Value: fmt.Sprintf(args[0].(*object.String).Value, list...)}
 			},
 		},
 		// sum(array:[1, 2, 3])
@@ -442,15 +443,15 @@ func getFns() map[string]*object.Builtin {
 
 				arr := args[0].(*object.Array)
 				if arr.Empty() {
-					return &object.Number{Value: float64(0)}
+					return &object.Number{Token: tok, Value: float64(0)}
 				}
 
 				if !arr.Homogeneous() {
-					return newError("sum(...) can only be called on an homogeneous array, got %s", arr.Inspect())
+					return newError(tok, "sum(...) can only be called on an homogeneous array, got %s", arr.Inspect())
 				}
 
 				if arr.Elements[0].Type() != object.NUMBER_OBJ {
-					return newError("sum(...) can only be called on arrays of numbers, got %s", arr.Inspect())
+					return newError(tok, "sum(...) can only be called on arrays of numbers, got %s", arr.Inspect())
 				}
 
 				var sum float64 = 0
@@ -460,7 +461,7 @@ func getFns() map[string]*object.Builtin {
 					sum += elem.Value
 				}
 
-				return &object.Number{Value: sum}
+				return &object.Number{Token: tok, Value: sum}
 			},
 		},
 		// sort(array:[1, 2, 3])
@@ -480,7 +481,7 @@ func getFns() map[string]*object.Builtin {
 				}
 
 				if !arr.Homogeneous() {
-					return newError("argument to `sort` must be an homogeneous array (elements of the same type), got %s", arr.Inspect())
+					return newError(tok, "argument to `sort` must be an homogeneous array (elements of the same type), got %s", arr.Inspect())
 				}
 
 				switch elements[0].(type) {
@@ -494,7 +495,7 @@ func getFns() map[string]*object.Builtin {
 					o := []object.Object{}
 
 					for _, v := range a {
-						o = append(o, &object.Number{Value: v})
+						o = append(o, &object.Number{Token: tok, Value: v})
 					}
 					return &object.Array{Elements: o}
 				case *object.String:
@@ -507,11 +508,11 @@ func getFns() map[string]*object.Builtin {
 					o := []object.Object{}
 
 					for _, v := range a {
-						o = append(o, &object.String{Value: v})
+						o = append(o, &object.String{Token: tok, Value: v})
 					}
 					return &object.Array{Elements: o}
 				default:
-					return newError("cannot sort an array with given elements elements (%s)", arr.Inspect())
+					return newError(tok, "cannot sort an array with given elements elements (%s)", arr.Inspect())
 				}
 			},
 		},
@@ -530,7 +531,7 @@ func getFns() map[string]*object.Builtin {
 				copy(newElements, arr.Elements)
 
 				for k, v := range arr.Elements {
-					evaluated := applyFunction(args[1], []object.Object{v})
+					evaluated := applyFunction(tok, args[1], []object.Object{v})
 
 					if isError(evaluated) {
 						return evaluated
@@ -555,7 +556,7 @@ func getFns() map[string]*object.Builtin {
 				arr := args[0].(*object.Array)
 
 				for _, v := range arr.Elements {
-					r := applyFunction(args[1], []object.Object{v})
+					r := applyFunction(tok, args[1], []object.Object{v})
 
 					if isTruthy(r) {
 						result = true
@@ -563,7 +564,7 @@ func getFns() map[string]*object.Builtin {
 					}
 				}
 
-				return &object.Boolean{Value: result}
+				return &object.Boolean{Token: tok, Value: result}
 			},
 		},
 		// every(array:[1, 2, 3], function:f(x) { x == 2 })
@@ -580,14 +581,14 @@ func getFns() map[string]*object.Builtin {
 				arr := args[0].(*object.Array)
 
 				for _, v := range arr.Elements {
-					r := applyFunction(args[1], []object.Object{v})
+					r := applyFunction(tok, args[1], []object.Object{v})
 
 					if !isTruthy(r) {
 						result = false
 					}
 				}
 
-				return &object.Boolean{Value: result}
+				return &object.Boolean{Token: tok, Value: result}
 			},
 		},
 		// find(array:[1, 2, 3], function:f(x) { x == 2 })
@@ -602,7 +603,7 @@ func getFns() map[string]*object.Builtin {
 				arr := args[0].(*object.Array)
 
 				for _, v := range arr.Elements {
-					r := applyFunction(args[1], []object.Object{v})
+					r := applyFunction(tok, args[1], []object.Object{v})
 
 					if isTruthy(r) {
 						return v
@@ -625,7 +626,7 @@ func getFns() map[string]*object.Builtin {
 				arr := args[0].(*object.Array)
 
 				for _, v := range arr.Elements {
-					evaluated := applyFunction(args[1], []object.Object{v})
+					evaluated := applyFunction(tok, args[1], []object.Object{v})
 
 					if isError(evaluated) {
 						return evaluated
@@ -653,7 +654,7 @@ func getFns() map[string]*object.Builtin {
 					needle, ok := args[1].(*object.String)
 
 					if ok {
-						return &object.Boolean{Value: strings.Contains(arg.Value, needle.Value)}
+						return &object.Boolean{Token: tok, Value: strings.Contains(arg.Value, needle.Value)}
 					}
 				case *object.Array:
 					var found bool
@@ -667,7 +668,7 @@ func getFns() map[string]*object.Builtin {
 							}
 						}
 
-						return &object.Boolean{Value: found}
+						return &object.Boolean{Token: tok, Value: found}
 					case *object.Number:
 						for _, v := range arg.Elements {
 							// Quite ghetto but also the easiest way out
@@ -681,11 +682,11 @@ func getFns() map[string]*object.Builtin {
 							}
 						}
 
-						return &object.Boolean{Value: found}
+						return &object.Boolean{Token: tok, Value: found}
 					}
 				}
 
-				return &object.Boolean{Value: false}
+				return &object.Boolean{Token: tok, Value: false}
 			},
 		},
 		// str(1)
@@ -697,7 +698,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.String{Value: args[0].Inspect()}
+				return &object.String{Token: tok, Value: args[0].Inspect()}
 			},
 		},
 		// any("abc", "b")
@@ -709,7 +710,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.Boolean{Value: strings.ContainsAny(args[0].(*object.String).Value, args[1].(*object.String).Value)}
+				return &object.Boolean{Token: tok, Value: strings.ContainsAny(args[0].(*object.String).Value, args[1].(*object.String).Value)}
 			},
 		},
 		// prefix("abc", "a")
@@ -721,7 +722,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.Boolean{Value: strings.HasPrefix(args[0].(*object.String).Value, args[1].(*object.String).Value)}
+				return &object.Boolean{Token: tok, Value: strings.HasPrefix(args[0].(*object.String).Value, args[1].(*object.String).Value)}
 			},
 		},
 		// suffix("abc", "a")
@@ -733,7 +734,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.Boolean{Value: strings.HasSuffix(args[0].(*object.String).Value, args[1].(*object.String).Value)}
+				return &object.Boolean{Token: tok, Value: strings.HasSuffix(args[0].(*object.String).Value, args[1].(*object.String).Value)}
 			},
 		},
 		// repeat("abc", 3)
@@ -745,7 +746,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.String{Value: strings.Repeat(args[0].(*object.String).Value, int(args[1].(*object.Number).Value))}
+				return &object.String{Token: tok, Value: strings.Repeat(args[0].(*object.String).Value, int(args[1].(*object.Number).Value))}
 			},
 		},
 		// replace("abc", "b", "f", -1)
@@ -757,7 +758,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.String{Value: strings.Replace(args[0].(*object.String).Value, args[1].(*object.String).Value, args[2].(*object.String).Value, int(args[3].(*object.Number).Value))}
+				return &object.String{Token: tok, Value: strings.Replace(args[0].(*object.String).Value, args[1].(*object.String).Value, args[2].(*object.String).Value, int(args[3].(*object.Number).Value))}
 			},
 		},
 		// title("some thing")
@@ -769,7 +770,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.String{Value: strings.Title(args[0].(*object.String).Value)}
+				return &object.String{Token: tok, Value: strings.Title(args[0].(*object.String).Value)}
 			},
 		},
 		// lower("ABC")
@@ -781,7 +782,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.String{Value: strings.ToLower(args[0].(*object.String).Value)}
+				return &object.String{Token: tok, Value: strings.ToLower(args[0].(*object.String).Value)}
 			},
 		},
 		// upper("abc")
@@ -793,7 +794,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.String{Value: strings.ToUpper(args[0].(*object.String).Value)}
+				return &object.String{Token: tok, Value: strings.ToUpper(args[0].(*object.String).Value)}
 			},
 		},
 		// trim("abc")
@@ -805,7 +806,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.String{Value: strings.Trim(args[0].(*object.String).Value, " ")}
+				return &object.String{Token: tok, Value: strings.Trim(args[0].(*object.String).Value, " ")}
 			},
 		},
 		// trim_by("abc", "c")
@@ -817,7 +818,7 @@ func getFns() map[string]*object.Builtin {
 					return err
 				}
 
-				return &object.String{Value: strings.Trim(args[0].(*object.String).Value, args[1].(*object.String).Value)}
+				return &object.String{Token: tok, Value: strings.Trim(args[0].(*object.String).Value, args[1].(*object.String).Value)}
 			},
 		},
 		// index("abc", "c")
@@ -835,7 +836,7 @@ func getFns() map[string]*object.Builtin {
 					return NULL
 				}
 
-				return &object.Number{Value: float64(i)}
+				return &object.Number{Token: tok, Value: float64(i)}
 			},
 		},
 		// last_index("abcc", "c")
@@ -853,7 +854,7 @@ func getFns() map[string]*object.Builtin {
 					return NULL
 				}
 
-				return &object.Number{Value: float64(i)}
+				return &object.Number{Token: tok, Value: float64(i)}
 			},
 		},
 		// slice("abcc", 0, -1)
@@ -873,7 +874,7 @@ func getFns() map[string]*object.Builtin {
 					s := arg.Value
 					start, end := sliceStartAndEnd(len(s), start, end)
 
-					return &object.String{Value: s[start:end]}
+					return &object.String{Token: tok, Value: s[start:end]}
 				case *object.Array:
 					start, end := sliceStartAndEnd(len(arg.Elements), start, end)
 
@@ -965,7 +966,7 @@ func getFns() map[string]*object.Builtin {
 				newElements := make([]object.Object, length, length)
 
 				for k, _ := range arr.Elements {
-					newElements[k] = &object.Number{Value: float64(k)}
+					newElements[k] = &object.Number{Token: tok, Value: float64(k)}
 				}
 
 				return &object.Array{Elements: newElements}
@@ -988,7 +989,7 @@ func getFns() map[string]*object.Builtin {
 					newElements[k] = v.Inspect()
 				}
 
-				return &object.String{Value: strings.Join(newElements, args[1].(*object.String).Value)}
+				return &object.String{Token: tok, Value: strings.Join(newElements, args[1].(*object.String).Value)}
 			},
 		},
 	}
diff --git a/examples/error-eval.abs b/examples/error-eval.abs
new file mode 100644
index 0000000..5416ed1
--- /dev/null
+++ b/examples/error-eval.abs
@@ -0,0 +1,3 @@
+# there is an evaluation error on line 2
+1 + "hello"
+echo("should not reach here")
diff --git a/examples/error-parse.abs b/examples/error-parse.abs
new file mode 100644
index 0000000..cb86788
--- /dev/null
+++ b/examples/error-parse.abs
@@ -0,0 +1,13 @@
+# there are multiple parser errors in this file
+
+# this is a malformed identifier
+m.a = 'abc'
+
+# this is a command terminated with a semi
+d/d = $(command);
+
+# this is a command terminated with a LF
+c/c = $(command)
+
+# this is a bad infix operator
+b %% c
diff --git a/lexer/lexer.go b/lexer/lexer.go
index cedc9ff..eed5064 100644
--- a/lexer/lexer.go
+++ b/lexer/lexer.go
@@ -11,18 +11,70 @@ type Lexer struct {
 	position     int  // current position in input (points to current char)
 	readPosition int  // current reading position in input (after current char)
 	ch           byte // current char under examination
+	// map of input line boundaries used by linePosition() for error location
+	lineMap [][2]int // array of [begin, end] pairs: [[0,12], [13,22], [23,33] ... ]
 }
 
 func New(input string) *Lexer {
 	l := &Lexer{input: input}
+	// map the input line boundaries for CurrentLine()
+	l.buildLineMap()
+	// read the first char
 	l.readChar()
 	return l
 }
 
+// buildLineMap creates map of input line boundaries used by LinePosition() for error location
+func (l *Lexer) buildLineMap() {
+	begin := 0
+	idx := 0
+	for i, ch := range l.input {
+		idx = i
+		if ch == '\n' {
+			l.lineMap = append(l.lineMap, [2]int{begin, idx})
+			begin = idx + 1
+		}
+	}
+	// last line
+	l.lineMap = append(l.lineMap, [2]int{begin, idx + 1})
+}
+
+// CurrentPosition returns l.position
 func (l *Lexer) CurrentPosition() int {
 	return l.position
 }
 
+// linePosition (pos) returns lineNum, begin, end
+func (l *Lexer) linePosition(pos int) (int, int, int) {
+	idx := 0
+	begin := 0
+	end := 0
+	for i, tuple := range l.lineMap {
+		idx = i
+		begin, end = tuple[0], tuple[1]
+		if pos >= begin && pos <= end {
+			break
+		}
+	}
+	lineNum := idx + 1
+	return lineNum, begin, end
+}
+
+// ErrorLine (pos) returns lineNum, column, errorLine
+func (l *Lexer) ErrorLine(pos int) (int, int, string) {
+	lineNum, begin, end := l.linePosition(pos)
+	errorLine := l.input[begin:end]
+	column := pos - begin + 1
+	return lineNum, column, errorLine
+}
+
+func (lex *Lexer) newToken(tokenType token.TokenType) token.Token {
+	return token.Token{
+		Type:     tokenType,
+		Position: lex.position,
+		Literal:  string(lex.ch)}
+}
+
 func (l *Lexer) NextToken() token.Token {
 	var tok token.Token
 
@@ -31,86 +83,98 @@ func (l *Lexer) NextToken() token.Token {
 	switch l.ch {
 	case '=':
 		if l.peekChar() == '=' {
+			tok = l.newToken(token.EQ)
 			ch := l.ch
 			l.readChar()
 			literal := string(ch) + string(l.ch)
-			tok = token.Token{Type: token.EQ, Literal: literal}
+			tok.Literal = literal
 		} else {
-			tok = newToken(token.ASSIGN, l.ch)
+			tok = l.newToken(token.ASSIGN)
 		}
 	case '+':
 		if l.peekChar() == '=' {
 			tok.Type = token.COMP_PLUS
+			tok.Position = l.position
 			tok.Literal = "+="
 			l.readChar()
 		} else {
-			tok = newToken(token.PLUS, l.ch)
+			tok = l.newToken(token.PLUS)
 		}
 	case '-':
 		if l.peekChar() == '=' {
 			tok.Type = token.COMP_MINUS
+			tok.Position = l.position
 			tok.Literal = "-="
 			l.readChar()
 		} else {
-			tok = newToken(token.MINUS, l.ch)
+			tok = l.newToken(token.MINUS)
 		}
 	case '%':
 		if l.peekChar() == '=' {
 			tok.Type = token.COMP_MODULO
+			tok.Position = l.position
 			tok.Literal = "%="
 			l.readChar()
 		} else {
-			tok = newToken(token.MODULO, l.ch)
+			tok = l.newToken(token.MODULO)
 		}
 	case '!':
 		if l.peekChar() == '=' {
+			tok = l.newToken(token.NOT_EQ)
 			ch := l.ch
 			l.readChar()
 			literal := string(ch) + string(l.ch)
-			tok = token.Token{Type: token.NOT_EQ, Literal: literal}
+			tok.Literal = literal
 		} else {
-			tok = newToken(token.BANG, l.ch)
+			tok = l.newToken(token.BANG)
 		}
 	case '/':
 		if l.peekChar() == '/' {
 			tok.Type = token.COMMENT
+			tok.Position = l.position
 			tok.Literal = l.readLine()
 		} else if l.peekChar() == '=' {
 			tok.Type = token.COMP_SLASH
+			tok.Position = l.position
 			tok.Literal = "/="
 			l.readChar()
 		} else {
-			tok = newToken(token.SLASH, l.ch)
+			tok = l.newToken(token.SLASH)
 		}
 	case '#':
 		tok.Type = token.COMMENT
+		tok.Position = l.position
 		tok.Literal = l.readLine()
 	case '&':
 		if l.peekChar() == '&' {
 			tok.Type = token.AND
+			tok.Position = l.position
 			tok.Literal = l.readLogicalOperator()
 		} else {
-			tok = newToken(token.BIT_AND, l.ch)
+			tok = l.newToken(token.BIT_AND)
 		}
 	case '^':
-		tok = newToken(token.BIT_XOR, l.ch)
+		tok = l.newToken(token.BIT_XOR)
 	case '*':
 		if l.peekChar() == '*' {
 			l.readChar()
 			if l.peekChar() == '=' {
 				tok.Type = token.COMP_EXPONENT
+				tok.Position = l.position
 				tok.Literal = "**="
 				l.readChar()
 			} else {
 				tok.Type = token.EXPONENT
+				tok.Position = l.position
 				tok.Literal = "**"
 			}
 		} else if l.peekChar() == '=' {
 			tok.Type = token.COMP_ASTERISK
+			tok.Position = l.position
 			tok.Literal = "*="
 			l.readChar()
 		} else {
-			tok = newToken(token.ASTERISK, l.ch)
+			tok = l.newToken(token.ASTERISK)
 		}
 	case '<':
 		if l.peekChar() == '=' {
@@ -118,95 +182,109 @@ func (l *Lexer) NextToken() token.Token {
 
 			if l.peekChar() == '>' {
 				tok.Type = token.COMBINED_COMP
+				tok.Position = l.position
 				tok.Literal = "<=>"
 				l.readChar()
 			} else {
 				tok.Type = token.LT_EQ
+				tok.Position = l.position
 				tok.Literal = "<="
 			}
 		} else if l.peekChar() == '<' {
 			tok.Type = token.BIT_LSHIFT
+			tok.Position = l.position
 			tok.Literal = "<<"
 			l.readChar()
 		} else {
-			tok = newToken(token.LT, l.ch)
+			tok = l.newToken(token.LT)
 		}
 	case '>':
 		if l.peekChar() == '=' {
 			tok.Type = token.GT_EQ
+			tok.Position = l.position
 			tok.Literal = ">="
 			l.readChar()
 		} else if l.peekChar() == '>' {
 			tok.Type = token.BIT_RSHIFT
+			tok.Position = l.position
 			tok.Literal = ">>"
 			l.readChar()
 		} else {
-			tok = newToken(token.GT, l.ch)
+			tok = l.newToken(token.GT)
 		}
 	case ';':
-		tok = newToken(token.SEMICOLON, l.ch)
+		tok = l.newToken(token.SEMICOLON)
 	case ':':
-		tok = newToken(token.COLON, l.ch)
+		tok = l.newToken(token.COLON)
 	case ',':
-		tok = newToken(token.COMMA, l.ch)
+		tok = l.newToken(token.COMMA)
 	case '.':
 		if l.peekChar() == '.' {
 			tok.Type = token.RANGE
+			tok.Position = l.position
 			tok.Literal = ".."
 			l.readChar()
 		} else {
-			tok = newToken(token.DOT, l.ch)
+			tok = l.newToken(token.DOT)
 		}
 	case '|':
 		if l.peekChar() == '|' {
 			tok.Type = token.OR
+			tok.Position = l.position
 			tok.Literal = l.readLogicalOperator()
 		} else {
-			tok = newToken(token.PIPE, l.ch)
+			tok = l.newToken(token.PIPE)
 		}
 	case '{':
-		tok = newToken(token.LBRACE, l.ch)
+		tok = l.newToken(token.LBRACE)
 	case '}':
-		tok = newToken(token.RBRACE, l.ch)
+		tok = l.newToken(token.RBRACE)
 	case '~':
-		tok = newToken(token.TILDE, l.ch)
+		tok = l.newToken(token.TILDE)
 	case '(':
-		tok = newToken(token.LPAREN, l.ch)
+		tok = l.newToken(token.LPAREN)
 	case ')':
-		tok = newToken(token.RPAREN, l.ch)
+		tok = l.newToken(token.RPAREN)
 	case '"':
 		tok.Type = token.STRING
+		tok.Position = l.position
 		tok.Literal = l.readString('"')
 	case '\'':
 		tok.Type = token.STRING
+		tok.Position = l.position
 		tok.Literal = l.readString('\'')
 	case '$':
 		if l.peekChar() == '(' {
 			tok.Type = token.COMMAND
+			tok.Position = l.position
 			tok.Literal = l.readCommand()
 		} else {
 			tok.Type = token.ILLEGAL
+			tok.Position = l.position
 			tok.Literal = l.readLine()
 		}
 	case '[':
-		tok = newToken(token.LBRACKET, l.ch)
+		tok = l.newToken(token.LBRACKET)
 	case ']':
-		tok = newToken(token.RBRACKET, l.ch)
+		tok = l.newToken(token.RBRACKET)
 	case 0:
-		tok.Literal = ""
 		tok.Type = token.EOF
+		tok.Position = l.position
+		tok.Literal = ""
 	default:
 		if isLetter(l.ch) {
+			tok.Position = l.position
 			tok.Literal = l.readIdentifier()
 			tok.Type = token.LookupIdent(tok.Literal)
 			return tok
 		} else if isDigit(l.ch) {
+			tok.Position = l.position
 			literal, kind := l.readNumber()
 			tok.Type = kind
 			tok.Literal = literal
 			return tok
 		} else {
-			tok = newToken(token.ILLEGAL, l.ch)
+			tok = l.newToken(token.ILLEGAL)
 		}
 	}
 
@@ -227,7 +305,7 @@ func (l *Lexer) readChar() {
 		l.ch = l.input[l.readPosition]
 	}
 	l.position = l.readPosition
-	l.readPosition += 1
+	l.readPosition++
 }
 
 func (l *Lexer) Rewind(pos int) {
@@ -373,7 +451,6 @@ func (l *Lexer) readCommand() string {
 			break
 		}
 	}
-
 	ret := l.input[position : l.position-subtract]
 
 	// Let's make sure the semicolo is the next token, without
@@ -393,7 +470,3 @@ func isLetter(ch byte) bool {
 func isDigit(ch byte) bool {
 	return '0' <= ch && ch <= '9'
 }
-
-func newToken(tokenType token.TokenType, ch byte) token.Token {
-	return token.Token{Type: tokenType, Literal: string(ch)}
-}
diff --git a/main.go b/main.go
index 6ec1fa4..c231ab2 100644
--- a/main.go
+++ b/main.go
@@ -21,6 +21,11 @@ func main() {
 
 	args := os.Args
 
+	if len(args) == 2 && args[1] == "--version" {
+		fmt.Println(VERSION)
+		return
+	}
+
 	// if we're called without arguments,
 	// launch the REPL
 	if len(args) == 1 || strings.HasPrefix(args[1], "-") {
@@ -30,11 +35,6 @@ func main() {
 		return
 	}
 
-	if args[1] == "--version" {
-		fmt.Println(VERSION)
-		return
-	}
-
 	// let's parse our argument as a file
 	code, err := ioutil.ReadFile(args[1])
 
diff --git a/object/object.go b/object/object.go
index d95999a..c721e52 100644
--- a/object/object.go
+++ b/object/object.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 
 	"github.com/abs-lang/abs/ast"
+	"github.com/abs-lang/abs/token"
 )
 
 type BuiltinFunction func(args ...Object) Object
@@ -31,6 +32,7 @@ const (
 )
 
 type HashKey struct {
+	Token token.Token
 	Type  ObjectType
 	Value string
 }
@@ -50,6 +52,7 @@ type Iterable interface {
 }
 
 type Number struct {
+	Token token.Token
 	Value float64
 }
 
@@ -69,18 +72,22 @@ func (n *Number) ZeroValue() float64 { return float64(0) }
 func (n *Number) Int() int           { return int(n.Value) }
 
 type Boolean struct {
+	Token token.Token
 	Value bool
 }
 
 func (b *Boolean) Type() ObjectType { return BOOLEAN_OBJ }
 func (b *Boolean) Inspect() string  { return fmt.Sprintf("%t", b.Value) }
 
-type Null struct{}
+type Null struct {
+	Token token.Token
+}
 
 func (n *Null) Type() ObjectType { return NULL_OBJ }
 func (n *Null) Inspect() string  { return "null" }
 
 type ReturnValue struct {
+	Token token.Token
 	Value Object
 }
 
@@ -95,6 +102,7 @@ func (e *Error) Type() ObjectType { return ERROR_OBJ }
 func (e *Error) Inspect() string  { return "ERROR: " + e.Message }
 
 type Function struct {
+	Token      token.Token
 	Parameters []*ast.Identifier
 	Body       *ast.BlockStatement
 	Env        *Environment
@@ -112,9 +120,9 @@ func (f *Function) Inspect() string {
 	out.WriteString("f")
 	out.WriteString("(")
 	out.WriteString(strings.Join(params, ", "))
-	out.WriteString(") {\n")
+	out.WriteString(") {")
 	out.WriteString(f.Body.String())
-	out.WriteString("\n}")
+	out.WriteString("}")
 
 	return out.String()
 }
@@ -140,6 +148,7 @@ func (f *Function) Inspect() string {
 // type(cmd) // STRING
 // cmd.ok // FALSE
 type String struct {
+	Token token.Token
 	Value string
 	Ok    *Boolean // A special property to check whether a command exited correctly
 }
@@ -152,6 +161,7 @@ func (s *String) HashKey() HashKey {
 }
 
 type Builtin struct {
+	Token    token.Token
 	Fn       BuiltinFunction
 	Next     func(int) (int, Object)
 	Types    []string
@@ -162,6 +172,7 @@ func (b *Builtin) Type() ObjectType { return BUILTIN_OBJ }
 func (b *Builtin) Inspect() string  { return "builtin function" }
 
 type Array struct {
+	Token    token.Token
 	Elements []Object
 	position int
 }
@@ -219,6 +230,7 @@ type HashPair struct {
 }
 
 type Hash struct {
+	Token token.Token
 	Pairs map[HashKey]HashPair
 }
 
diff --git a/parser/parser.go b/parser/parser.go
index c1d556c..18804d8 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -149,40 +149,47 @@ func (p *Parser) nextToken() {
 
 	if p.curTokenIs(token.ILLEGAL) {
 		msg := fmt.Sprintf(`Illegal token '%s'`, p.curToken.Literal)
-		p.errors = append(p.errors, msg)
+		p.reportError(msg, p.curToken)
 	}
 }
 
-func (p *Parser) curTokenIs(t token.TokenType) bool {
-	return p.curToken.Type == t
+func (p *Parser) curTokenIs(typ token.TokenType) bool {
+	return p.curToken.Type == typ
 }
 
-func (p *Parser) peekTokenIs(t token.TokenType) bool {
-	return p.peekToken.Type == t
+func (p *Parser) peekTokenIs(typ token.TokenType) bool {
+	return p.peekToken.Type == typ
 }
 
-func (p *Parser) expectPeek(t token.TokenType) bool {
-	if p.peekTokenIs(t) {
+func (p *Parser) expectPeek(typ token.TokenType) bool {
+	if p.peekTokenIs(typ) {
 		p.nextToken()
 		return true
-	} else {
-		p.peekError(t)
-		return false
 	}
+	p.peekError(p.curToken)
+	return false
 }
 
 func (p *Parser) Errors() []string {
 	return p.errors
 }
 
-func (p *Parser) peekError(t token.TokenType) {
-	msg := fmt.Sprintf("expected next token to be %s, got %s instead", t, p.peekToken.Type)
+func (p *Parser) reportError(err string, tok token.Token) {
+	// report error at token location
+	lineNum, column, errorLine := p.l.ErrorLine(tok.Position)
+	msg := fmt.Sprintf("%s\n\t[%d:%d]\t%s", err, lineNum, column, errorLine)
 	p.errors = append(p.errors, msg)
 }
 
-func (p *Parser) noPrefixParseFnError(t token.TokenType) {
-	msg := fmt.Sprintf("no prefix parse function for %s found", t)
-	p.errors = append(p.errors, msg)
+func (p *Parser) peekError(tok token.Token) {
+	msg := fmt.Sprintf("expected next token to be %s, got %s instead", tok.Type, p.peekToken.Type)
+	p.reportError(msg, tok)
+}
+
+func (p *Parser) noPrefixParseFnError(tok token.Token) {
+	msg := fmt.Sprintf("no prefix parse function for '%s' found", tok.Literal)
+	// match := fmt.Sprintf("%s", t)
+	p.reportError(msg, tok)
 }
 
 func (p *Parser) ParseProgram() *ast.Program {
@@ -196,7 +203,6 @@ func (p *Parser) ParseProgram() *ast.Program {
 		}
 		p.nextToken()
 	}
-
 	return program
 }
 
@@ -321,7 +327,7 @@ func (p *Parser) parseExpressionStatement() *ast.ExpressionStatement {
 func (p *Parser) parseExpression(precedence int) ast.Expression {
 	prefix := p.prefixParseFns[p.curToken.Type]
 	if prefix == nil {
-		p.noPrefixParseFnError(p.curToken.Type)
+		p.noPrefixParseFnError(p.curToken)
 		return nil
 	}
 	leftExp := prefix()
@@ -368,7 +374,7 @@ func (p *Parser) parseNumberLiteral() ast.Expression {
 	value, err := strconv.ParseFloat(p.curToken.Literal, 64)
 	if err != nil {
 		msg := fmt.Sprintf("could not parse %q as number", p.curToken.Literal)
-		p.errors = append(p.errors, msg)
+		p.reportError(msg, p.curToken)
 		return nil
 	}
 
@@ -450,13 +456,13 @@ func (p *Parser) parseDottedExpression(object ast.Expression) ast.Expression {
 		exp.Method = p.parseExpression(precedence)
 		p.nextToken()
 		exp.Arguments = p.parseExpressionList(token.RPAREN)
-
 		return exp
 	} else {
 		exp := &ast.PropertyExpression{Token: t, Object: object}
 
 		if !p.curTokenIs(token.IDENT) {
-			p.errors = append(p.errors, fmt.Sprintf("property needs to be an identifier, got '%s'", p.curToken.Literal))
+			msg := fmt.Sprintf("property needs to be an identifier, got '%s'", p.curToken.Literal)
+			p.reportError(msg, p.curToken)
 		}
 
 		exp.Property = p.parseIdentifier()
@@ -477,7 +483,8 @@ func (p *Parser) parseMethodExpression(object ast.Expression) ast.Expression {
 
 // true
 func (p *Parser) parseBoolean() ast.Expression {
-	return &ast.Boolean{Token: p.curToken, Value: p.curTokenIs(token.TRUE)}
+	b := &ast.Boolean{Token: p.curToken, Value: p.curTokenIs(token.TRUE)}
+	return b
 }
 
 func (p *Parser) parseGroupedExpression() ast.Expression {
@@ -517,7 +524,6 @@ func (p *Parser) parseIfExpression() ast.Expression {
 
 		expression.Alternative = p.parseBlockStatement()
 	}
-
 	return expression
 }
 
@@ -535,7 +541,6 @@ func (p *Parser) parseWhileExpression() ast.Expression {
 	}
 
 	expression.Consequence = p.parseBlockStatement()
-
 	return expression
 }
 
@@ -777,7 +782,8 @@ func (p *Parser) ParseHashLiteral() ast.Expression {
 }
 
 func (p *Parser) parseCommand() ast.Expression {
-	return &ast.CommandExpression{Token: p.curToken, Value: p.curToken.Literal}
+	cmd := &ast.CommandExpression{Token: p.curToken, Value: p.curToken.Literal}
+	return cmd
 }
 
 // We don't really have to do anything when comments
diff --git a/repl/repl.go b/repl/repl.go
index 3b043f6..8d02f35 100644
--- a/repl/repl.go
+++ b/repl/repl.go
@@ -96,16 +96,21 @@ func executor(line string) {
 // This function takes code and evaluates
 // it, spitting out the result.
 func Run(code string, interactive bool) {
-	l := lexer.New(code)
-	p := parser.New(l)
+	lex := lexer.New(code)
+	p := parser.New(lex)
 
 	program := p.ParseProgram()
 	if len(p.Errors()) != 0 {
 		printParserErrors(p.Errors())
+		if !interactive {
+			os.Exit(99)
+		}
 		return
 	}
 
-	evaluated := evaluator.Eval(program, env)
+	// invoke BeginEval() passing in the program, env, and lexer for error position
+	// NB. Eval(node, env) is recursive so we can't call it directly
+	evaluated := evaluator.BeginEval(program, env, lex)
 
 	if evaluated != nil {
 		isError := evaluated.Type() == object.ERROR_OBJ
diff --git a/token/token.go b/token/token.go
index 743742a..91e959a 100644
--- a/token/token.go
+++ b/token/token.go
@@ -1,5 +1,7 @@
 package token
 
+// import "github.com/abs-lang/abs/lexer"
+
 type TokenType string
 
 const (
@@ -33,7 +35,7 @@ const (
 
 	// Logical operators
 	AND = "&&"
-	OR  = "OR"
+	OR  = "||"
 
 	// Bitwise operators
 	// It might be worth
@@ -81,8 +83,9 @@ const (
 )
 
 type Token struct {
-	Type    TokenType
-	Literal string
+	Type     TokenType
+	Position int // lexer position in file before token
+	Literal  string
 }
 
 var keywords = map[string]TokenType{


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index 702db5e..6feecf9 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -1,6 +1,8 @@
 package evaluator
 
 import (
+	"fmt"
+	"strings"
 	"testing"
 
 	"github.com/abs-lang/abs/lexer"
@@ -8,6 +10,17 @@ import (
 	"github.com/abs-lang/abs/parser"
 )
 
+func logErrorWithPosition(t *testing.T, msg string, expected interface{}) {
+	errorStr := msg
+	expected, _ = expected.(string)
+	expectedStr := fmt.Sprintf("%s", expected)
+	if strings.Contains(errorStr, expectedStr) {
+		t.Log("expected error:", errorStr)
+	} else {
+		expectedStr = fmt.Sprintf("ERROR: wrong error message. expected='%s',", expectedStr)
+		t.Error(expectedStr, "\ngot=", errorStr)
+	}
+}
 func TestEvalFloatExpression(t *testing.T) {
 	tests := []struct {
 		input    string
@@ -209,10 +222,7 @@ func TestForExpressions(t *testing.T) {
 				t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)
 				continue
 			}
-
-			if errObj.Message != tt.expected {
-				t.Errorf("wrong error message. expected=%q, got=%q", tt.expected, errObj.Message)
-			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
 		}
 	}
 }
@@ -259,10 +269,7 @@ func TestBitwiseExpressions(t *testing.T) {
 				t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)
 				continue
 			}
-
-			if errObj.Message != tt.expected {
-				t.Errorf("wrong error message. expected=%q, got=%q", tt.expected, errObj.Message)
-			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
 		}
 	}
 }
@@ -283,7 +290,7 @@ func TestForInExpressions(t *testing.T) {
 		{`k = 100; for k, v in ["x", "y", "z"] {}; k`, 100},
 		{`v = 100; for k, v in ["x", "y", "z"] {}; v`, 100},
 		{`for k, v in ["x", "y", "z"] {k=y}; v`, "identifier not found: y"},
-		{`for k, v in ["x", "y", z] {k=y}; v`, "'ERROR: identifier not found: z' is a ERROR, not an iterable, cannot be used in for loop"},
+		{`for k, v in ["x", "y", z] {k=y}; v`, "'ERROR: identifier not found: z"},
 	}
 
 	for _, tt := range tests {
@@ -297,10 +304,7 @@ func TestForInExpressions(t *testing.T) {
 				t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)
 				continue
 			}
-
-			if errObj.Message != tt.expected {
-				t.Errorf("wrong error message. expected=%q, got=%q", tt.expected, errObj.Message)
-			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
 		}
 	}
 }
@@ -326,10 +330,7 @@ func TestWhileExpressions(t *testing.T) {
 				testStringObject(t, evaluated, tt.expected.(string))
 				continue
 			}
-
-			if errObj.Message != tt.expected {
-				t.Errorf("wrong error message. expected=%q, got=%q", tt.expected, errObj.Message)
-			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
 		default:
 			panic("should not reach here")
 		}
@@ -387,8 +388,8 @@ fn(10);`,
 
 func TestErrorHandling(t *testing.T) {
 	tests := []struct {
-		input           string
-		expectedMessage string
+		input    string
+		expected interface{}
 	}{
 		{
 			"5 + true;",
@@ -439,10 +440,9 @@ if (10 > 1) {
 			"identifier not found: foobar",
 		},
 		{
-			`{"name": "Abs"}[f(x) { x }];`,
-			`index operator not supported: f(x) {
-x
-} on HASH`,
+			// `{"name": "Abs"}[f(x) {x}];`,
+			`{"name": "Abs"}[f(x) {x}];`,
+			`index operator not supported: f(x) {x} on HASH`,
 		},
 		{
 			`999[1]`,
@@ -459,11 +459,7 @@ x
 				evaluated, evaluated)
 			continue
 		}
-
-		if errObj.Message != tt.expectedMessage {
-			t.Errorf("wrong error message. expected=%q, got=%q",
-				tt.expectedMessage, errObj.Message)
-		}
+		logErrorWithPosition(t, errObj.Message, tt.expected)
 	}
 }
 
@@ -500,9 +496,7 @@ b, c = [1, 2]; b`, 1},
 				t.Errorf("object is not Error. got=%T (%+v)", evaluated, evaluated)
 				continue
 			}
-			if errObj.Message != expected {
-				t.Errorf("wrong error message. expected=%q, got=%q", expected, errObj.Message)
-			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
 		}
 	}
 }
@@ -543,7 +537,7 @@ func TestFunctionApplication(t *testing.T) {
 		{"add = f(x, y) { x + y; }; add(5, 5);", 10},
 		{"add = f(x, y) { x + y; }; add(5 + 5, add(5, 5));", 20},
 		{"f(x) { x; }(5)", 5},
-		{"f(x) { x; }()", "Wrong number of arguments passed to f(x) {\nx\n}. Want [x], got []"},
+		{"f(x) { x; }()", "Wrong number of arguments passed to f(x) {x}. Want [x], got []"},
 	}
 
 	for _, tt := range tests {
@@ -555,10 +549,7 @@ func TestFunctionApplication(t *testing.T) {
 				t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)
 				continue
 			}
-
-			if errObj.Message != tt.expected {
-				t.Errorf("wrong error message. expected=%q, got=%q", tt.expected, errObj.Message)
-			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
 		case float64:
 			testNumberObject(t, evaluated, float64(expected))
 		}
@@ -660,7 +651,7 @@ func TestBuiltinFunctions(t *testing.T) {
 		{`find([1,2], f(x) {x == "some"})`, nil},
 		{`arg("o")`, "argument 0 to arg(...) is not supported (got: o, allowed: NUMBER)"},
 		{`arg(3)`, ""},
-		{`pwd().split("").reverse().slice(0, 33).reverse().join("").replace("\\", "/", -1)`, "github.com/abs-lang/abs/evaluator"}, // Little trick to get travis to run this test, as the base path is not /go/src/
+		{`pwd().split("").reverse().slice(0, 33).reverse().join("").replace("\\", "/", -1)`, "/evaluator"}, // Little trick to get travis to run this test, as the base path is not /go/src/
 		{`rand(1)`, 0},
 		{`int(10)`, 10},
 		{`int(10.5)`, 10},
@@ -783,10 +774,9 @@ c")`, []string{"a", "b", "c"}},
 		case string:
 			s, ok := evaluated.(*object.String)
 			if ok {
-				if s.Value != tt.expected {
-					t.Errorf("object is not the right string. got=%s want:%s", s.Value, tt.expected)
+				if !strings.Contains(s.Value, tt.expected.(string)) {
+					t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected)
 				}
-
 				continue
 			}
 
@@ -795,9 +785,7 @@ c")`, []string{"a", "b", "c"}},
 				t.Errorf("object is not Error. got=%T (%+v)", evaluated, evaluated)
 				continue
 			}
-			if errObj.Message != expected {
-				t.Errorf("wrong error message. expected=%q, got=%q", expected, errObj.Message)
-			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
 		case []int:
 			array, ok := evaluated.(*object.Array)
 			if !ok {
@@ -875,8 +863,8 @@ func TestLogicalOperators(t *testing.T) {
 		case string:
 			s, ok := evaluated.(*object.String)
 			if ok {
-				if s.Value != tt.expected {
-					t.Errorf("object is not the right string. got=%s want:%s", s.Value, tt.expected)
+				if !strings.Contains(s.Value, tt.expected.(string)) {
+					t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected)
 				}
 
 				continue
@@ -887,9 +875,7 @@ func TestLogicalOperators(t *testing.T) {
 				t.Errorf("object is not Error. got=%T (%+v)", evaluated, evaluated)
 				continue
 			}
-			if errObj.Message != expected {
-				t.Errorf("wrong error message. expected=%q, got=%q", expected, errObj.Message)
-			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
 		case []int:
 			array, ok := evaluated.(*object.Array)
 			if !ok {
@@ -930,8 +916,8 @@ func TestRangesOperators(t *testing.T) {
 		case string:
 			s, ok := evaluated.(*object.String)
 			if ok {
-				if s.Value != tt.expected {
-					t.Errorf("object is not the right string. got=%s want:%s", s.Value, tt.expected)
+				if !strings.Contains(s.Value, tt.expected.(string)) {
+					t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected)
 				}
 
 				continue
@@ -942,9 +928,7 @@ func TestRangesOperators(t *testing.T) {
 				t.Errorf("object is not Error. got=%T (%+v)", evaluated, evaluated)
 				continue
 			}
-			if errObj.Message != expected {
-				t.Errorf("wrong error message. expected=%q, got=%q", expected, errObj.Message)
-			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
 		case []int:
 			array, ok := evaluated.(*object.Array)
 			if !ok {
@@ -991,8 +975,8 @@ func TestBuiltinProperties(t *testing.T) {
 		case string:
 			s, ok := evaluated.(*object.String)
 			if ok {
-				if s.Value != tt.expected {
-					t.Errorf("object is not the right string. got=%s want:%s", s.Value, tt.expected)
+				if !strings.Contains(s.Value, tt.expected.(string)) {
+					t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected)
 				}
 
 				continue
@@ -1004,10 +988,7 @@ func TestBuiltinProperties(t *testing.T) {
 					evaluated, evaluated)
 				continue
 			}
-			if errObj.Message != expected {
-				t.Errorf("wrong error message. expected=%q, got=%q",
-					expected, errObj.Message)
-			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
 		case []int:
 			array, ok := evaluated.(*object.Array)
 			if !ok {
@@ -1067,8 +1048,8 @@ func TestCommand(t *testing.T) {
 				t.Errorf("object is not String. got=%T (%+v)", evaluated, evaluated)
 				continue
 			}
-			if stringObj.Value != expected {
-				t.Errorf("wrong string. expected=%q, got=%q", expected, stringObj.Value)
+			if !strings.Contains(stringObj.Value, expected) {
+				t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, stringObj.Value, expected)
 			}
 		}
 	}
@@ -1254,12 +1235,12 @@ func TestStringIndexExpressions(t *testing.T) {
 }
 
 func testEval(input string) object.Object {
-	l := lexer.New(input)
-	p := parser.New(l)
+	lex := lexer.New(input)
+	p := parser.New(lex)
 	program := p.ParseProgram()
 	env := object.NewEnvironment()
 
-	return Eval(program, env)
+	return BeginEval(program, env, lex)
 }
 
 func testNumberObject(t *testing.T, obj object.Object, expected interface{}) bool {
@@ -1306,8 +1287,9 @@ func testBooleanObject(t *testing.T, obj object.Object, expected bool) bool {
 }
 
 func testNullObject(t *testing.T, obj object.Object) bool {
-	if obj != NULL {
-		t.Errorf("object is not NULL. got=%T (%+v)", obj, obj)
+	_, ok := obj.(*object.Null)
+	if !ok {
+		t.Errorf("object is not Null. got=%T (%+v)", obj, obj)
 		return false
 	}
 	return true
diff --git a/tests/test-abs.sh b/tests/test-abs.sh
new file mode 100755
index 0000000..a8513da
--- /dev/null
+++ b/tests/test-abs.sh
@@ -0,0 +1,36 @@
+#! /bin/bash
+# test the abs parser and eval error location
+
+DIR=~/go/src/github.com/abs-lang/abs
+ABS=$(command -v abs)
+
+DEBUG=$1
+
+if [[ "$DEBUG" == "-d" ]]; then
+    ABS=$DIR/builds/abs
+fi
+if [ -z $ABS ]; then
+    echo "Cannot locate abs binary; exiting"
+    exit 1
+fi
+
+cd $DIR
+LINE="======================================="
+echo $LINE
+echo "Test Parser"
+FILE=tests/test-parser.abs
+echo $FILE
+$ABS $FILE
+echo "Exit code: $?"
+echo
+
+echo $LINE
+echo "Test Eval()"
+FILE=tests/test-eval.abs
+echo $FILE
+
+for i in 1 2 3; do
+    $ABS $FILE $i
+    echo "Exit code: $?"
+    echo
+done
diff --git a/tests/test-eval.abs b/tests/test-eval.abs
new file mode 100644
index 0000000..c5d7684
--- /dev/null
+++ b/tests/test-eval.abs
@@ -0,0 +1,20 @@
+# eval fails
+
+test_num = int(arg(2))
+
+if test_num == 1 {
+    # mismatched type
+    s = "string"
+    s = s + 1   # this is a comment
+}
+
+if test_num == 2 {
+    # invalid property
+    a = [1,2,3]
+    a.junk
+}
+
+if test_num == 3 {
+    # invalid function as index
+    {"name": "Abs"}[f(x) {x}];  
+}
diff --git a/tests/test-parser.abs b/tests/test-parser.abs
new file mode 100644
index 0000000..c095039
--- /dev/null
+++ b/tests/test-parser.abs
@@ -0,0 +1,81 @@
+# parser fails
+
+# this is a malformed identifier
+m.a = 'abc'
+
+# this is a command terminated with a semi
+d/d = $(command);
+
+# this is a command terminated with a LF
+c/c = $(command)
+
+# this is a bad infix operator
+b %% c
+
+# test from lexer/lexer_test.go
+five = 5;
+ten = 10;
+add = f(x, y) {
+  x + y;
+};
+result = add(five, ten);
+&&||!-/*5;
+5 < 10 > 5;
+1 <= 1 >= 1;
+<=>
+if (5 < 10) {
+	return true;
+} else {
+	return false;
+}
+while (1 > 0) {
+	echo("hello")
+}
+for x in xs {
+	x
+}
+for x = 0; x < 10; x = x + 1 {
+	x
+}
+10 == 10;
+10 != 9;
+"foobar"
+"foo bar"
+[1, 2];
+$(echo "()");
+{"foo": "bar"}
+$(curl icanhazip.com -X POST)
+$(ls *.go);
+a = [1]
+a.first()
+a.prop
+# Comment
+// Comment
+hello
+$(command; command)
+$(command2; command2);
+one | two | tree
+"hel\"lo"
+"hel\lo"
+"hel\\\\lo"
+"\"hello\""
+"\"he\"\"llo\""
+"hello\\"
+"hello\\\\"
+"\\\\hello"
+**
+1..10
+~%
++=
+-=
+*=
+/=
+**=
+%=
+1.23
+1.str()
+null
+nullo
+&^>><<
+$111
+'123'