Repository: abs-lang/abs
Pull request #167 (2019-02-06 09:42:28)

1.2.x release
Tracking PR for all changes landing in 1.2


Patch:
diff --git a/Makefile b/Makefile
index ab7c431..ebdc036 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 .PHONY: repl
 run:
-	docker run -tiv $$(pwd):/go/src/github.com/abs-lang/abs --name abs --rm abs
+	docker run -ti -v $$(pwd):/go/src/github.com/abs-lang/abs -v /home/`whoami`/.abs_history:/root/.abs_history --name abs --rm abs
 fmt:
 	go fmt ./...
 build:
diff --git a/docs/installer.sh b/docs/installer.sh
index 2b02c80..13dfb4a 100644
--- a/docs/installer.sh
+++ b/docs/installer.sh
@@ -24,7 +24,7 @@ MACHINE_TYPE=$(uname -m)
 if [ "${MACHINE_TYPE}" = 'x86_64' ]; then
   ARCH="amd64"
 fi
-VERSION=1.1.0
+VERSION=1.2.0
 
 echo "Trying to detect the details of your architecture."
 echo ""
diff --git a/docs/introduction/how-to-run-abs-code.md b/docs/introduction/how-to-run-abs-code.md
index aed1b73..9776ad6 100644
--- a/docs/introduction/how-to-run-abs-code.md
+++ b/docs/introduction/how-to-run-abs-code.md
@@ -19,6 +19,28 @@ Afterwards, you can run ABS scripts with:
 ``` bash
 $ abs path/to/scripts.abs
 ```
+You can also run an executable abs script directly from bash
+using a bash shebang line at the top of the script file. 
+
+In this example the abs executable is linked to `/usr/local/bin/abs`
+and the abs script `~/bin/remote.abs` has its execute permissions set.
+```bash
+$ cat ~/bin/remote.abs
+#! /usr/local/bin/abs
+# remote paths are <target>:<path> 
+from_path = arg(2) 
+to_path = arg(3)
+if ! (from_path && to_path) {
+    if ! from_path {from_path = "<missing>"}
+    if ! to_path {to_path = "<missing>"}
+    echo("FROM: %s, TO: %s", from_path, to_path)
+    exit(1)
+}
+...
+# the executable abs script above is in the PATH at ~/bin/remote.abs
+$ remote.abs
+FROM: <missing>, TO: <missing>
+```
 
 Scripts do not have to have a specific extension,
 although it's recommended to use `.abs` as a
@@ -47,10 +69,121 @@ ERROR: not a function: STRING
 ⧐  ip
 94.204.178.37
 ```
+### REPL Command History
+
+Interactive REPL sessions can restore and save the command 
+history to a history file containing a maximum number of command lines. 
+
+The prompt live history is restored from the history file when
+the REPL starts and then saved again when the REPL exits. This way you
+can navigate through the command lines from your previous sessions
+by using the up and down arrow keys at the prompt.
+
++ Note well that the live prompt history will show duplicate command
+lines, but the saved history will only contain a single command
+when the previous command and the current command are the same.
+
+The history file name and the maximum number of history lines are
+configurable through 
+1) the ABS environment (set by the ABS init file; see below)
+2) the OS environment
+3) The default values are `ABS_HISTORY_FILE="~/.abs_history"` 
+and `ABS_MAX_HISTORY_LINES=1000`.
+
++ If you wish to suppress the command line history completely, just 
+set `ABS_MAX_HISTORY_LINES=0`. In this case the history file
+will not be created.
+
+For example:
+```bash
+$ export ABS_HISTORY_FILE="~/my_abs_hist"
+$ export ABS_MAX_HISTORY_LINES=500
+$ abs
+Hello user, welcome to the ABS (1.1.0) programming language!
+Type 'quit' when you are done, 'help' if you get lost!
+⧐  pwd()
+/home/user/git/abs
+⧐  cd()
+/home/user
+⧐  echo("hello")
+hello
+⧐  quit
+Adios!
+
+$ cat ~/my_abs_hist`; echo
+pwd()
+cd()
+echo("hello")
+$
+```
+
+## ABS Init File
+
+When the ABS interpreter starts running, it will load an optional
+ABS script as its init file. The ABS init file path can be 
+configured via the OS environment variable `ABS_INIT_FILE`. The
+default value is `ABS_INIT_FILE=~/.absrc`.
+
+If the `ABS_INIT_FILE` exists, it will be evaluated before the
+interpreter begins in both interactive REPL or script modes.
+The result of all expressions evaluated in the init file become
+part of the ABS global environment which are available to command
+line expressions or script programs.
+
+Also, note that the `ABS_INTERACTIVE` global environment variable
+is pre-set to `true` or `false` so that the init file can determine
+which mode is running. This is useful if you wish to set the ABS REPL
+command line prompt or history configuration variables in the init file.
+This will preset the prompt and history parameters for the interactive
+REPL (see [REPL Command History](#REPL_Command_History) above).
+
+### Configuring the ABS REPL Command Line Prompt
+The ABS REPL command line prompt may be configured at start up using
+`ABS_PROMPT_LIVE_PREFIX` and `ABS_PROMPT_PREFIX` variables from either
+the ABS or OS environments. The default values are
+`ABS_PROMPT_LIVE_PREFIX=false` and `ABS_PROMPT_PREFIX="⧐  "`.
+
+REPL "static prompt" mode will be configured if `ABS_PROMPT_PREFIX`
+contains no live prompt `template string` or if
+`ABS_PROMPT_LIVE_PREFIX=false`. The `static prompt` will be the
+value of the `ABS_PROMPT_PREFIX` string (if present) or the default
+prompt `"⧐  "`. Static prompt mode is the default for the REPL.
+
+REPL "live prompt" mode follows the current working directory 
+set by `cd()` when both `ABS_PROMPT_LIVE_PREFIX=true` and the
+`ABS_PROMPT_PREFIX` variable contains a live prompt `template string`.
+
+A live prompt `template string` may contain the following
+named placeholders:
+* `{user}`: the current userId
+* `{host}`: the local hostname
+* `{dir}`:  the current working directory following `cd()`
+  
+For example, you can create a `bash`-style live prompt: 
+```bash
+$ cat ~/.absrc
+# ABS init script ~/.absrc 
+# For interactive REPL, override default prompt, history filename and size
+if ABS_INTERACTIVE {
+    ABS_PROMPT_LIVE_PREFIX = true
+    ABS_PROMPT_PREFIX = "{user}@{host}:{dir}$ "
+    ABS_HISTORY_FILE = "~/.abs_hist"
+    ABS_MAX_HISTORY_LINES = 500
+}
+$ abs
+Hello user, welcome to the ABS (1.1.0) programming language!
+Type 'quit' when you are done, 'help' if you get lost!
+user@hostname:~/git/abs$ cwd = cd()
+user@hostname:~$ `ls .absrc`
+.absrc
+user@hostname:~$  
+```
+
+Also see a `template ABS Init File` at [examples](https://github.com/abs-lang/abs/tree/master/examples/absrc.abs).
 
 ## Why is abs interpreted?
 
-ABS' goal is to be a portable, pragmatic, coincise, simple language:
+ABS' goal is to be a portable, pragmatic, concise, simple language:
 great performance comes second.
 
 With this in mind, we made a deliberate choice to avoid
@@ -64,4 +197,4 @@ it took to run a Bash script?
 That's about it for this section!
 
 You can now head over to read about ABS's syntax,
-starting with [assignments](/syntax/assignments)!
\ No newline at end of file
+starting with [assignments](/syntax/assignments)!
diff --git a/docs/misc/technical-details.md b/docs/misc/technical-details.md
index 218259e..34bf06c 100644
--- a/docs/misc/technical-details.md
+++ b/docs/misc/technical-details.md
@@ -205,7 +205,7 @@ ERROR: type mismatch: NULL + NUMBER
 
 ## Roadmap
 
-We're currently working on [1.2](https://github.com/abs-lang/abs/milestone/9).
+We're currently working on [1.3](https://github.com/abs-lang/abs/milestone/10).
 
 ## Next
 
diff --git a/docs/syntax/system-commands.md b/docs/syntax/system-commands.md
index ce0b1c0..d1572d8 100644
--- a/docs/syntax/system-commands.md
+++ b/docs/syntax/system-commands.md
@@ -4,7 +4,8 @@ Executing system commands is one of the most important features
 of ABS, as it allows the mixing of conveniency of the shell with
 the syntax of a modern programming language.
 
-Commands are executed either with `$(command)` or `` `command` ``, which resemble Bash's syntax to execute commands in a subshell:
+Commands are executed either with `$(command)` or `` `command` ``,
+which resemble Bash's syntax to execute commands in a subshell:
 
 ``` bash
 date = $(date) # "Sun Apr 1 04:30:59 +01 1995"
@@ -39,6 +40,41 @@ if `ls -la`.ok {
 }
 ```
 
+## Executing commands in background
+
+Sometimes you might want to execute a command in
+background, so that the script keeps executing
+while the command is running. In order to do so,
+you can simply add an `&` at the end of your script:
+
+``` bash
+`sleep 10 &`
+echo("This will be printed right away!")
+```
+
+You might also want to check whether a command
+is "done", by checking the boolean `.done` property:
+
+``` bash
+cmd = `sleep 10 &`
+cmd.done # false
+`sleep 11`
+cmd.done # true
+```
+
+If, at some point, you want to wait for the command
+to finish before running additional code, you can
+use the `wait` method:
+
+``` bash
+cmd = `sleep 10 &`
+echo("This will be printed right away!")
+cmd.wait()
+echo("This will be printed after 10s")
+```
+
+## Interpolation
+
 You can also replace parts of the command with variables
 declared within your program using the `$` symbol:
 
@@ -56,6 +92,8 @@ $(echo $PWD) # "" since the ABS variable PWD doesn't exist
 $(echo \$PWD) # "/go/src/github.com/abs-lang/abs"
 ```
 
+## Limitations
+
 Currently, commands that use the `$()` syntax need to be
 on their own line, meaning that you will not
 be able to have additional code on the same line.
@@ -68,12 +106,9 @@ $(sleep 10); echo("hello world")
 Note that this is currently a limitation that will likely
 be removed in the future (see [#41](https://github.com/abs-lang/abs/issues/41)).
 
-Commands are blocking and cannot be run in parallel, although
-we're planning to support background execution in the future
-(see [#70](https://github.com/abs-lang/abs/issues/70)).
-
 Also note that, currently, the implementation of system commands
 requires the `bash` executable to [be available on the system](https://github.com/abs-lang/abs/blob/5b5b0abf3115a5dd4dfe8485501f8765985ad0db/evaluator/evaluator.go#L696-L722).
+On Windows, commands are executed through [cmd.exe](https://github.com/abs-lang/abs/blob/ee793641be09ad8572c3e913fef8468f69b0c0a2/evaluator/evaluator.go#L1101-L1103).
 Future work will make it possible to select which shell to use,
 as well as bypassing the shell altogether (see [#73](https://github.com/abs-lang/abs/issues/73)).
 
diff --git a/docs/types/builtin-function.md b/docs/types/builtin-function.md
index ea76e03..d6f6390 100644
--- a/docs/types/builtin-function.md
+++ b/docs/types/builtin-function.md
@@ -116,10 +116,48 @@ type("") # "STRING"
 type({}) # "HASH"
 ```
 
+### cd() or cd(path)
+
+Sets the current working directory to `homeDir` or the given `path`
+in both Linux and Windows.
+
+Note that the path may have a `'~/'` prefix which will be replaced
+with `'homeDir/'`. Also, in Windows, any `'/'` path separator will be
+replaced with `'\'` and path names are not case-sensitive.
+
+Returns the `'/fully/expanded/path'` to the new current working directory and `path.ok`.
+If `path.ok` is `false`, that means there was an error changing directory:
+
+``` bash
+path = cd()
+path.ok     # true
+path        # /home/user or C:\Users\user
+
+here = pwd()
+path = cd("/path/to/nowhere")
+path.ok         # false
+path            # 'chdir /path/to/nowhere: no such file or directory'
+here == pwd()   # true
+
+cd("~/git/abs") # /home/user/git/abs or C:\Users\user\git\abs
+
+cd("..")        # /home/user/git or C:\Users\user\git
+
+cd("/usr/local/bin") # /usr/local/bin
+
+dirs = cd() && `ls`.lines()
+len(dirs)   # number of directories in homeDir
+```
+
 ### pwd()
 
-Returns the working directory where the script was started for -- equivalent
-to `env("PWD")`:
+Returns the path to the current working directory -- equivalent
+to `env("PWD")`. 
+
+If executed from a script this will initially be the directory
+containing the script.
+
+To change the working directory, see `cd()`.
 
 ``` bash
 pwd() # /go/src/github.com/abs-lang/abs
@@ -174,6 +212,72 @@ Halts the process for as many `ms` you specified:
 sleep(1000) # sleeps for 1 second
 ```
 
+### source(fileName) aka require(filename) 
+
+Evaluates the ABS script `fileName` in the context of the ABS global
+environment. The results of any expressions in the file become
+available to other commands in the REPL command line or to other
+scripts in the current script execution chain. 
+
+This is most useful for creating `library functions` in a script
+that can be used by many other scripts. Often the library functions
+are loaded via the ABS Init File `~/.absrc`. See [ABS Init File](/introduction/how-to-run-abs-code).
+
+For example:
+```bash
+$ cat ~/abs/lib/library.abs
+# Useful function library ~/abs/lib/library.abs
+adder = f(n, i) { n + i }
+
+$ cat ~/.absrc
+# ABS init file ~/.absrc
+source("~/abs/lib/library.abs")
+
+$ abs
+Hello user, welcome to the ABS (1.2.0) programming language!
+Type 'quit' when you are done, 'help' if you get lost!
+⧐ adder(1, 2)
+3
+⧐
+```
+
+In addition to source file inclusion in scripts, you can also use
+`source()` in the interactive REPL to load a script being
+debugged. When the loaded script completes, the REPL command line
+will have access to all variables and functions evaluated in the
+script.
+
+For example:
+```bash
+⧐  source("~/git/abs/tests/test-strings.abs")
+...
+=====================
+>>> Testing split and join strings with expanded LFs:
+s = split("a\nb\nc", "\n")
+echo(s)
+[a, b, c]
+...
+⧐  s
+[a, b, c]
+⧐ 
+```
+
+Note well that nested source files must not create a circular
+inclusion condition. You can configure the intended source file
+inclusion depth using the `ABS_SOURCE_DEPTH` OS or ABS environment
+variables. The default is `ABS_SOURCE_DEPTH=10`. This will prevent
+a panic in the ABS interpreter if there is an unintended circular
+source inclusion.
+
+For example an ABS Init File may contain:
+```bash
+ABS_SOURCE_DEPTH = 15
+source("~/path/to/abs/lib")
+```
+This will limit the source inclusion depth to 15 levels for this
+`source()` statement and will also apply to future `source()`
+statements until changed.
+
 ## Next
 
 That's about it for this section!
diff --git a/docs/types/number.md b/docs/types/number.md
index b5bb2c1..ba788cd 100644
--- a/docs/types/number.md
+++ b/docs/types/number.md
@@ -42,12 +42,40 @@ Identity:
 
 ### int()
 
-Rounds down the number to the closest integer:
+Rounds **down** the number to the closest integer:
 
 ``` bash
 10.3.int() # 10
 ```
 
+### round(precision?)
+
+Rounds the number with the given precision.
+The precision argument is optional, and set to `0`
+by default:
+
+``` bash
+10.3.round() # 10
+10.6.round() # 11
+10.333.round(1) # 10.3
+```
+
+### ceil()
+
+Rounds the number up to the closest integer:
+
+``` bash
+10.3.ceil() # 11
+```
+
+### floor()
+
+Rounds the number down to the closest integer:
+
+``` bash
+10.9.floor() # 10
+```
+
 ### str()
 
 Returns a string containing the number:
diff --git a/docs/types/string.md b/docs/types/string.md
index a44fba5..f42f5d8 100644
--- a/docs/types/string.md
+++ b/docs/types/string.md
@@ -83,7 +83,9 @@ a\\nb
 c
 ⧐  
 ```
+
 ### Working with special characters in string functions
+
 Special characters also work with `split()` and `join()` and other string functions as well.
 
 1) Double quoted expanded special characters:
@@ -160,13 +162,56 @@ Use this function when `"...".number()` might return an error.
 
 ### int()
 
-Converts a string to integer, if possible:
+Converts a string to a number, and then rounds it
+**down** to the closest integer.
+The string must represent a number.
 
 ``` bash
 "99.5".int() # 99
 "a".int() # ERROR: int(...) can only be called on strings which represent numbers, 'a' given
 ```
 
+### round(precision?)
+
+Converts a string to a number, and then rounds
+the number with the given precision.
+
+The precision argument is optional, and set to `0`
+by default.
+
+The string must represent a number.
+
+``` bash
+"10.3".round() # 10
+"10.6".round() # 11
+"10.333".round(1) # 10.3
+"a".round() # ERROR: round(...) can only be called on strings which represent numbers, 'a' given
+```
+
+### ceil()
+
+Converts a string to a number, and then rounds the
+number up to the closest integer.
+
+The string must represent a number.
+
+``` bash
+"10.3".ceil() # 11
+"a".ceil() # ERROR: ceil(...) can only be called on strings which represent numbers, 'a' given
+```
+
+### floor()
+
+Converts a string to a number, and then rounds the
+number down to the closest integer.
+
+The string must represent a number.
+
+``` bash
+"10.9".floor() # 10
+"a".floor() # ERROR: floor(...) can only be called on strings which represent numbers, 'a' given
+```
+
 ### split(separator)
 
 Splits a string by separator:
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index 364094b..95e46b4 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"os/exec"
 	"regexp"
+	"runtime"
 	"strconv"
 	"strings"
 
@@ -18,13 +19,16 @@ import (
 )
 
 var (
-	NULL  = &object.Null{}
-	EOF   = &object.Error{Message: "EOF"}
-	TRUE  = &object.Boolean{Value: true}
-	FALSE = &object.Boolean{Value: false}
+	NULL  = object.NULL
+	EOF   = object.EOF
+	TRUE  = object.TRUE
+	FALSE = object.FALSE
 	Fns   map[string]*object.Builtin
 )
 
+// This program's global environment can be used by builtin's to modify the env
+var globalEnv *object.Environment
+
 // This program's lexer used for error location in Eval(program)
 var lex *lexer.Lexer
 
@@ -43,6 +47,8 @@ func newError(tok token.Token, format string, a ...interface{}) *object.Error {
 // 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 environment
+	globalEnv = env
 	// global lexer
 	lex = lexer
 	// run the evaluator
@@ -50,7 +56,6 @@ func BeginEval(program ast.Node, env *object.Environment, lexer *lexer.Lexer) ob
 }
 
 func Eval(node ast.Node, env *object.Environment) object.Object {
-
 	switch node := node.(type) {
 	// Statements
 	case *ast.Program:
@@ -915,6 +920,14 @@ func evalPropertyExpression(pe *ast.PropertyExpression, env *object.Environment)
 				return obj.Ok
 			}
 
+			return FALSE
+		}
+		// Special .done property of commands
+		if pe.Property.String() == "done" {
+			if obj.Done != nil {
+				return obj.Done
+			}
+
 			return FALSE
 		}
 	case *object.Hash:
@@ -937,7 +950,7 @@ func applyFunction(tok token.Token, fn object.Object, args []object.Object) obje
 		return unwrapReturnValue(evaluated)
 
 	case *object.Builtin:
-		return fn.Fn(args...)
+		return fn.Fn(tok, args...)
 
 	default:
 		return newError(tok, "not a function: %s", fn.Type())
@@ -956,7 +969,7 @@ func applyMethod(tok token.Token, o object.Object, method string, args []object.
 	}
 
 	args = append([]object.Object{o}, args...)
-	return f.Fn(args...)
+	return f.Fn(tok, args...)
 }
 
 func extendFunctionEnv(
@@ -1067,6 +1080,7 @@ func evalHashIndexExpression(tok token.Token, hash, index object.Object) object.
 }
 
 func evalCommandExpression(tok token.Token, cmd string, env *object.Environment) object.Object {
+	cmd = strings.Trim(cmd, " ")
 	// Match all strings preceded by
 	// a $ or a \$
 	re := regexp.MustCompile("(\\\\)?\\$([a-zA-Z_]{1,})")
@@ -1094,19 +1108,82 @@ func evalCommandExpression(tok token.Token, cmd string, env *object.Environment)
 		return v.Inspect()
 	})
 
-	commands := []string{"-c", cmd}
-	c := exec.Command("bash", commands...)
+	// A background command ends with a '&'
+	background := len(cmd) > 1 && cmd[len(cmd)-1] == '&'
+	// If this is a background command
+	// we'll remove the trailing '&' and
+	// execute it in background ourselves
+	if background {
+		cmd = cmd[:len(cmd)-2]
+	}
+
+	// The string holding the command
+	s := &object.String{}
+
+	// thanks to @haifenghuang
+	var commands []string
+	var executor string
+	if runtime.GOOS == "windows" {
+		commands = []string{"/C", cmd}
+		executor = "cmd.exe"
+	} else { //assume it's linux, darwin, freebsd, openbsd, solaris, etc
+		commands = []string{"-c", cmd}
+		executor = "bash"
+	}
+	c := exec.Command(executor, commands...)
 	c.Env = os.Environ()
-	var out bytes.Buffer
-	var stderr bytes.Buffer
 	c.Stdin = os.Stdin
-	c.Stdout = &out
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+	c.Stdout = &stdout
 	c.Stderr = &stderr
-	err := c.Run()
+
+	s.Stdout = &stdout
+	s.Stderr = &stderr
+	s.Cmd = c
+	s.Token = tok
+
+	var err error
+	if background {
+		// If we want to run the command in background,
+		// let's set it as running. With this, others can
+		// wait for it by calling s.Wait().
+		s.SetRunning()
+		go evalCommandInBackground(c, s)
+	} else {
+		err = c.Run()
+	}
+
+	if !background {
+		if err != nil {
+			s.SetCmdResult(FALSE)
+		} else {
+			s.SetCmdResult(TRUE)
+		}
+	}
+
+	return s
+}
+
+// Runs a background command.
+// We will start it, set its result
+// and then mark it as done, so that
+// callers stuck on s.Wait() can resume.
+func evalCommandInBackground(c *exec.Cmd, s *object.String) {
+	defer s.SetDone()
+	err := c.Start()
+
+	if err != nil {
+		s.SetCmdResult(FALSE)
+		return
+	}
+
+	err = c.Wait()
 
 	if err != nil {
-		return &object.String{Token: tok, Value: stderr.String(), Ok: FALSE}
+		s.SetCmdResult(FALSE)
+		return
 	}
 
-	return &object.String{Token: tok, Value: strings.Trim(out.String(), "\n"), Ok: TRUE}
+	s.SetCmdResult(TRUE)
 }
diff --git a/evaluator/functions.go b/evaluator/functions.go
index 44730fa..e5fced9 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -4,8 +4,11 @@ import (
 	"bufio"
 	"crypto/rand"
 	"fmt"
+	"io/ioutil"
+	"math"
 	"math/big"
 	"os"
+	"os/user"
 	"sort"
 	"strconv"
 	"strings"
@@ -25,7 +28,6 @@ var scannerPosition int
 
 func init() {
 	scanner = bufio.NewScanner(os.Stdin)
-	tok = token.Token{Type: token.FUNCTION, Position: 0, Literal: "BuiltinFunction"}
 }
 
 /*
@@ -59,16 +61,40 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{},
 			Fn:    pwdFn,
 		},
+		// cd() or cd(path)
+		"cd": &object.Builtin{
+			Types: []string{},
+			Fn:    cdFn,
+		},
 		// echo(arg:"hello")
 		"echo": &object.Builtin{
 			Types: []string{},
 			Fn:    echoFn,
 		},
 		// int(string:"123")
+		// int(number:"123")
 		"int": &object.Builtin{
 			Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
 			Fn:    intFn,
 		},
+		// round(string:"123.1")
+		// round(number:"123.1", 2)
+		"round": &object.Builtin{
+			Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
+			Fn:    roundFn,
+		},
+		// floor(string:"123.1")
+		// floor(number:123.1)
+		"floor": &object.Builtin{
+			Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
+			Fn:    floorFn,
+		},
+		// ceil(string:"123.1")
+		// ceil(number:123.1)
+		"ceil": &object.Builtin{
+			Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
+			Fn:    ceilFn,
+		},
 		// number(string:"1.23456")
 		"number": &object.Builtin{
 			Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
@@ -206,6 +232,11 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{object.STRING_OBJ},
 			Fn:    upperFn,
 		},
+		// wait(`sleep 1 &`)
+		"wait": &object.Builtin{
+			Types: []string{object.STRING_OBJ},
+			Fn:    waitFn,
+		},
 		// trim("abc")
 		"trim": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
@@ -277,6 +308,17 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{object.NUMBER_OBJ},
 			Fn:    sleepFn,
 		},
+		// source("fileName")
+		// aka require()
+		"source": &object.Builtin{
+			Types: []string{object.STRING_OBJ},
+			Fn:    sourceFn,
+		},
+		// require("fileName") -- alias for source()
+		"require": &object.Builtin{
+			Types: []string{object.STRING_OBJ},
+			Fn:    sourceFn,
+		},
 	}
 }
 
@@ -285,8 +327,8 @@ Here be the actual Builtin Functions
 */
 
 // 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) == 0 || len(args) > size {
+func validateArgs(tok token.Token, name string, args []object.Object, size int, types [][]string) object.Object {
+	if len(args) == 0 || len(args) > size || len(args) < size {
 		return newError(tok, "wrong number of arguments to %s(...): got=%d, want=%d", name, len(args), size)
 	}
 
@@ -300,8 +342,8 @@ func validateArgs(name string, args []object.Object, size int, types [][]string)
 }
 
 // len(var:"hello")
-func lenFn(args ...object.Object) object.Object {
-	err := validateArgs("len", args, 1, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}})
+func lenFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "len", args, 1, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -317,8 +359,8 @@ func lenFn(args ...object.Object) object.Object {
 }
 
 // rand(max:20)
-func randFn(args ...object.Object) object.Object {
-	err := validateArgs("rand", args, 1, [][]string{{object.NUMBER_OBJ}})
+func randFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "rand", args, 1, [][]string{{object.NUMBER_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -334,8 +376,8 @@ func randFn(args ...object.Object) object.Object {
 }
 
 // exit(code:0)
-func exitFn(args ...object.Object) object.Object {
-	err := validateArgs("exit", args, 1, [][]string{{object.NUMBER_OBJ}})
+func exitFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "exit", args, 1, [][]string{{object.NUMBER_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -346,13 +388,13 @@ func exitFn(args ...object.Object) object.Object {
 }
 
 // flag("my-flag")
-func flagFn(args ...object.Object) object.Object {
+func flagFn(tok token.Token, args ...object.Object) object.Object {
 	// TODO:
 	// This seems a bit more complicated than it should,
 	// and I could probably use some unit testing for this.
 	// In any case it's a small function so YOLO
 
-	err := validateArgs("flag", args, 1, [][]string{{object.STRING_OBJ}})
+	err := validateArgs(tok, "flag", args, 1, [][]string{{object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -410,7 +452,7 @@ func flagFn(args ...object.Object) object.Object {
 }
 
 // pwd()
-func pwdFn(args ...object.Object) object.Object {
+func pwdFn(tok token.Token, args ...object.Object) object.Object {
 	dir, err := os.Getwd()
 	if err != nil {
 		return newError(tok, err.Error())
@@ -418,8 +460,34 @@ func pwdFn(args ...object.Object) object.Object {
 	return &object.String{Token: tok, Value: dir}
 }
 
+// cd() or cd(path) returns expanded path and path.ok
+func cdFn(tok token.Token, args ...object.Object) object.Object {
+	user, ok := user.Current()
+	if ok != nil {
+		return newError(tok, ok.Error())
+	}
+	// Default: cd to user's homeDir
+	path := user.HomeDir
+	if len(args) == 1 {
+		// arg: rawPath
+		pathStr := args[0].(*object.String)
+		rawPath := pathStr.Value
+		path, _ = util.ExpandPath(rawPath)
+	}
+	// NB. windows os.Chdir(path) will convert any '/' in path to '\', however linux will not
+	error := os.Chdir(path)
+	if error != nil {
+		// path does not exist, return error string and !path.ok
+		return &object.String{Token: tok, Value: error.Error(), Ok: &object.Boolean{Token: tok, Value: false}}
+	}
+	// return the full path we cd()'d into and path.ok
+	// this will also test true/false for cd("path/to/somewhere") && `ls`
+	dir, _ := os.Getwd()
+	return &object.String{Token: tok, Value: dir, Ok: &object.Boolean{Token: tok, Value: true}}
+}
+
 // echo(arg:"hello")
-func echoFn(args ...object.Object) object.Object {
+func echoFn(tok token.Token, args ...object.Object) object.Object {
 	if len(args) == 0 {
 		// allow echo() without crashing
 		fmt.Println("")
@@ -439,32 +507,94 @@ func echoFn(args ...object.Object) object.Object {
 }
 
 // int(string:"123")
-func intFn(args ...object.Object) object.Object {
-	err := validateArgs("int", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
+// int(number:123)
+func intFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "int", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
 
-	switch arg := args[0].(type) {
+	return applyMathFunction(tok, args[0], func(n float64) float64 {
+		return float64(int64(n))
+	}, "int")
+}
+
+// round(string:"123.1")
+// round(number:123.1)
+func roundFn(tok token.Token, args ...object.Object) object.Object {
+	// Validate first argument
+	err := validateArgs(tok, "round", args[:1], 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	decimal := float64(1)
+
+	// If we have a second argument, let's validate it
+	if len(args) > 1 {
+		err := validateArgs(tok, "round", args[1:], 1, [][]string{{object.NUMBER_OBJ}})
+		if err != nil {
+			return err
+		}
+
+		decimal = float64(math.Pow(10, args[1].(*object.Number).Value))
+	}
+
+	return applyMathFunction(tok, args[0], func(n float64) float64 {
+		return math.Round(n*decimal) / decimal
+	}, "round")
+}
+
+// floor(string:"123.1")
+// floor(number:123.1)
+func floorFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "floor", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return applyMathFunction(tok, args[0], math.Floor, "floor")
+}
+
+// ceil(string:"123.1")
+// ceil(number:123.1)
+func ceilFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "ceil", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return applyMathFunction(tok, args[0], math.Ceil, "ceil")
+}
+
+// Base function to do math operations. This is here
+// so that we abstract away some of the common logic
+// between all math functions, for example:
+// - allowing to be called on strings as well ("1.23".ceil())
+// - handling errors
+// NB. callers must pass the token that is used for error line reporting
+func applyMathFunction(tok token.Token, arg object.Object, fn func(float64) float64, fname string) object.Object {
+	switch arg := arg.(type) {
 	case *object.Number:
-		return &object.Number{Token: tok, Value: float64(int64(arg.Value))}
+		return &object.Number{Token: tok, Value: float64(fn(arg.Value))}
 	case *object.String:
 		i, err := strconv.ParseFloat(arg.Value, 64)
 
 		if err != nil {
-			return newError(tok, "int(...) can only be called on strings which represent numbers, '%s' given", arg.Value)
+			return newError(tok, "%s(...) can only be called on strings which represent numbers, '%s' given", fname, arg.Value)
 		}
 
-		return &object.Number{Token: tok, Value: float64(int64(i))}
+		return &object.Number{Token: tok, Value: float64(fn(i))}
 	default:
-		// we will never reach here
-		return newError(tok, "argument to `int` not supported, got %s", args[0].Type())
+		// we should never reach here since our callers should validate
+		// the type of the arguments
+		return newError(tok, "argument to `%s` not supported, got %s", fname, arg.Type())
 	}
 }
 
 // number(string:"1.23456")
-func numberFn(args ...object.Object) object.Object {
-	err := validateArgs("number", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
+func numberFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "number", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -487,8 +617,8 @@ func numberFn(args ...object.Object) object.Object {
 }
 
 // is_number(string:"1.23456")
-func isNumberFn(args ...object.Object) object.Object {
-	err := validateArgs("number", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
+func isNumberFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "number", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -505,7 +635,7 @@ func isNumberFn(args ...object.Object) object.Object {
 }
 
 // stdin() -- implemented with 2 functions
-func stdinFn(args ...object.Object) object.Object {
+func stdinFn(tok token.Token, args ...object.Object) object.Object {
 	v := scanner.Scan()
 
 	if !v {
@@ -528,8 +658,8 @@ func stdinNextFn() (object.Object, object.Object) {
 }
 
 // env(variable:"PWD")
-func envFn(args ...object.Object) object.Object {
-	err := validateArgs("env", args, 1, [][]string{{object.STRING_OBJ}})
+func envFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "env", args, 1, [][]string{{object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -539,8 +669,8 @@ func envFn(args ...object.Object) object.Object {
 }
 
 // arg(position:1)
-func argFn(args ...object.Object) object.Object {
-	err := validateArgs("arg", args, 1, [][]string{{object.NUMBER_OBJ}})
+func argFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "arg", args, 1, [][]string{{object.NUMBER_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -556,8 +686,8 @@ func argFn(args ...object.Object) object.Object {
 }
 
 // type(variable:"hello")
-func typeFn(args ...object.Object) object.Object {
-	err := validateArgs("type", args, 1, [][]string{})
+func typeFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "type", args, 1, [][]string{})
 	if err != nil {
 		return err
 	}
@@ -566,8 +696,8 @@ func typeFn(args ...object.Object) object.Object {
 }
 
 // split(string:"hello")
-func splitFn(args ...object.Object) object.Object {
-	err := validateArgs("split", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+func splitFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "split", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -587,8 +717,8 @@ func splitFn(args ...object.Object) object.Object {
 }
 
 // lines(string:"a\nb")
-func linesFn(args ...object.Object) object.Object {
-	err := validateArgs("lines", args, 1, [][]string{{object.STRING_OBJ}})
+func linesFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "lines", args, 1, [][]string{{object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -609,7 +739,7 @@ func linesFn(args ...object.Object) object.Object {
 
 // "{}".json()
 // Converts a valid JSON document to an ABS hash.
-func jsonFn(args ...object.Object) object.Object {
+func jsonFn(tok token.Token, args ...object.Object) object.Object {
 	// One interesting thing here is that we're creating
 	// a new environment from scratch, whereas it might
 	// be interesting to use the existing one. That would
@@ -621,7 +751,7 @@ func jsonFn(args ...object.Object) object.Object {
 	// Also, we're instantiating a new lexer & parser from
 	// scratch, so this is a tad slow.
 
-	err := validateArgs("json", args, 1, [][]string{{object.STRING_OBJ}})
+	err := validateArgs(tok, "json", args, 1, [][]string{{object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -673,7 +803,7 @@ func jsonFn(args ...object.Object) object.Object {
 }
 
 // "a %s".fmt(b)
-func fmtFn(args ...object.Object) object.Object {
+func fmtFn(tok token.Token, args ...object.Object) object.Object {
 	list := []interface{}{}
 
 	for _, s := range args[1:] {
@@ -684,8 +814,8 @@ func fmtFn(args ...object.Object) object.Object {
 }
 
 // sum(array:[1, 2, 3])
-func sumFn(args ...object.Object) object.Object {
-	err := validateArgs("sum", args, 1, [][]string{{object.ARRAY_OBJ}})
+func sumFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "sum", args, 1, [][]string{{object.ARRAY_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -714,8 +844,8 @@ func sumFn(args ...object.Object) object.Object {
 }
 
 // sort(array:[1, 2, 3])
-func sortFn(args ...object.Object) object.Object {
-	err := validateArgs("sort", args, 1, [][]string{{object.ARRAY_OBJ}})
+func sortFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "sort", args, 1, [][]string{{object.ARRAY_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -764,8 +894,8 @@ func sortFn(args ...object.Object) object.Object {
 }
 
 // map(array:[1, 2, 3], function:f(x) { x + 1 })
-func mapFn(args ...object.Object) object.Object {
-	err := validateArgs("map", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
+func mapFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "map", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -788,8 +918,8 @@ func mapFn(args ...object.Object) object.Object {
 }
 
 // some(array:[1, 2, 3], function:f(x) { x == 2 })
-func someFn(args ...object.Object) object.Object {
-	err := validateArgs("some", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
+func someFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "some", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -811,8 +941,8 @@ func someFn(args ...object.Object) object.Object {
 }
 
 // every(array:[1, 2, 3], function:f(x) { x == 2 })
-func everyFn(args ...object.Object) object.Object {
-	err := validateArgs("every", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
+func everyFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "every", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -833,8 +963,8 @@ func everyFn(args ...object.Object) object.Object {
 }
 
 // find(array:[1, 2, 3], function:f(x) { x == 2 })
-func findFn(args ...object.Object) object.Object {
-	err := validateArgs("find", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
+func findFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "find", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -853,8 +983,8 @@ func findFn(args ...object.Object) object.Object {
 }
 
 // filter(array:[1, 2, 3], function:f(x) { x == 2 })
-func filterFn(args ...object.Object) object.Object {
-	err := validateArgs("filter", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
+func filterFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "filter", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -878,8 +1008,8 @@ func filterFn(args ...object.Object) object.Object {
 }
 
 // contains("str", "tr")
-func containsFn(args ...object.Object) object.Object {
-	err := validateArgs("contains", args, 2, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}, {object.STRING_OBJ, object.NUMBER_OBJ}})
+func containsFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "contains", args, 2, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}, {object.STRING_OBJ, object.NUMBER_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -925,8 +1055,8 @@ func containsFn(args ...object.Object) object.Object {
 }
 
 // str(1)
-func strFn(args ...object.Object) object.Object {
-	err := validateArgs("str", args, 1, [][]string{})
+func strFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "str", args, 1, [][]string{})
 	if err != nil {
 		return err
 	}
@@ -935,8 +1065,8 @@ func strFn(args ...object.Object) object.Object {
 }
 
 // any("abc", "b")
-func anyFn(args ...object.Object) object.Object {
-	err := validateArgs("any", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+func anyFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "any", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -945,8 +1075,8 @@ func anyFn(args ...object.Object) object.Object {
 }
 
 // prefix("abc", "a")
-func prefixFn(args ...object.Object) object.Object {
-	err := validateArgs("prefix", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+func prefixFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "prefix", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -955,8 +1085,8 @@ func prefixFn(args ...object.Object) object.Object {
 }
 
 // suffix("abc", "a")
-func suffixFn(args ...object.Object) object.Object {
-	err := validateArgs("suffix", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+func suffixFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "suffix", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -965,8 +1095,8 @@ func suffixFn(args ...object.Object) object.Object {
 }
 
 // repeat("abc", 3)
-func repeatFn(args ...object.Object) object.Object {
-	err := validateArgs("repeat", args, 2, [][]string{{object.STRING_OBJ}, {object.NUMBER_OBJ}})
+func repeatFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "repeat", args, 2, [][]string{{object.STRING_OBJ}, {object.NUMBER_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -975,8 +1105,8 @@ func repeatFn(args ...object.Object) object.Object {
 }
 
 // replace("abc", "b", "f", -1)
-func replaceFn(args ...object.Object) object.Object {
-	err := validateArgs("replace", args, 4, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}, {object.STRING_OBJ}, {object.NUMBER_OBJ}})
+func replaceFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "replace", args, 4, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}, {object.STRING_OBJ}, {object.NUMBER_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -985,8 +1115,8 @@ func replaceFn(args ...object.Object) object.Object {
 }
 
 // title("some thing")
-func titleFn(args ...object.Object) object.Object {
-	err := validateArgs("title", args, 1, [][]string{{object.STRING_OBJ}})
+func titleFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "title", args, 1, [][]string{{object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -995,8 +1125,8 @@ func titleFn(args ...object.Object) object.Object {
 }
 
 // lower("ABC")
-func lowerFn(args ...object.Object) object.Object {
-	err := validateArgs("lower", args, 1, [][]string{{object.STRING_OBJ}})
+func lowerFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "lower", args, 1, [][]string{{object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1005,8 +1135,8 @@ func lowerFn(args ...object.Object) object.Object {
 }
 
 // upper("abc")
-func upperFn(args ...object.Object) object.Object {
-	err := validateArgs("upper", args, 1, [][]string{{object.STRING_OBJ}})
+func upperFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "upper", args, 1, [][]string{{object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1014,9 +1144,26 @@ func upperFn(args ...object.Object) object.Object {
 	return &object.String{Token: tok, Value: strings.ToUpper(args[0].(*object.String).Value)}
 }
 
+// wait(`sleep 10 &`)
+func waitFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "wait", args, 1, [][]string{{object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	cmd := args[0].(*object.String)
+
+	if cmd.Cmd == nil {
+		return cmd
+	}
+
+	cmd.Wait()
+	return cmd
+}
+
 // trim("abc")
-func trimFn(args ...object.Object) object.Object {
-	err := validateArgs("trim", args, 1, [][]string{{object.STRING_OBJ}})
+func trimFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "trim", args, 1, [][]string{{object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1025,8 +1172,8 @@ func trimFn(args ...object.Object) object.Object {
 }
 
 // trim_by("abc", "c")
-func trimByFn(args ...object.Object) object.Object {
-	err := validateArgs("trim_by", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+func trimByFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "trim_by", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1035,8 +1182,8 @@ func trimByFn(args ...object.Object) object.Object {
 }
 
 // index("abc", "c")
-func indexFn(args ...object.Object) object.Object {
-	err := validateArgs("index", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+func indexFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "index", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1051,8 +1198,8 @@ func indexFn(args ...object.Object) object.Object {
 }
 
 // last_index("abcc", "c")
-func lastIndexFn(args ...object.Object) object.Object {
-	err := validateArgs("last_index", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+func lastIndexFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "last_index", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1067,8 +1214,8 @@ func lastIndexFn(args ...object.Object) object.Object {
 }
 
 // slice("abcc", 0, -1)
-func sliceFn(args ...object.Object) object.Object {
-	err := validateArgs("slice", args, 3, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}, {object.NUMBER_OBJ}, {object.NUMBER_OBJ}})
+func sliceFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "slice", args, 3, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}, {object.NUMBER_OBJ}, {object.NUMBER_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1120,8 +1267,8 @@ func sliceStartAndEnd(l int, start int, end int) (int, int) {
 }
 
 // shift([1,2,3]) removes and returns first value or null if array is empty
-func shiftFn(args ...object.Object) object.Object {
-	err := validateArgs("shift", args, 1, [][]string{{object.ARRAY_OBJ}})
+func shiftFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "shift", args, 1, [][]string{{object.ARRAY_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1137,8 +1284,8 @@ func shiftFn(args ...object.Object) object.Object {
 }
 
 // reverse([1,2,3])
-func reverseFn(args ...object.Object) object.Object {
-	err := validateArgs("reverse", args, 1, [][]string{{object.ARRAY_OBJ}})
+func reverseFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "reverse", args, 1, [][]string{{object.ARRAY_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1153,8 +1300,9 @@ func reverseFn(args ...object.Object) object.Object {
 }
 
 // push([1,2,3], 4)
-func pushFn(args ...object.Object) object.Object {
-	err := validateArgs("push", args, 2, [][]string{{object.ARRAY_OBJ}, {object.NULL_OBJ, object.ARRAY_OBJ, object.NUMBER_OBJ, object.STRING_OBJ, object.HASH_OBJ}})
+func pushFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "push", args, 2, [][]string{{object.ARRAY_OBJ}, {object.NULL_OBJ,
+		object.ARRAY_OBJ, object.NUMBER_OBJ, object.STRING_OBJ, object.HASH_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1167,8 +1315,16 @@ func pushFn(args ...object.Object) object.Object {
 
 // pop([1,2,3]) removes and returns last value or null if array is empty
 // pop({"a":1, "b":2, "c":3}, "a") removes and returns {"key": value} or null if key not found
-func popFn(args ...object.Object) object.Object {
-	err := validateArgs("pop", args, 2, [][]string{{object.ARRAY_OBJ, object.HASH_OBJ}})
+func popFn(tok token.Token, args ...object.Object) object.Object {
+	// pop has 2 signatures: pop(array), and pop(hash, key)
+	var err object.Object
+	if len(args) > 0 {
+		if args[0].Type() == object.ARRAY_OBJ {
+			err = validateArgs(tok, "pop", args, 1, [][]string{{object.ARRAY_OBJ}})
+		} else if args[0].Type() == object.HASH_OBJ {
+			err = validateArgs(tok, "pop", args, 2, [][]string{{object.HASH_OBJ}})
+		}
+	}
 	if err != nil {
 		return err
 	}
@@ -1200,8 +1356,8 @@ func popFn(args ...object.Object) object.Object {
 
 // keys([1,2,3]) returns array of indices
 // keys({"a": 1, "b": 2, "c": 3}) returns array of keys
-func keysFn(args ...object.Object) object.Object {
-	err := validateArgs("keys", args, 1, [][]string{{object.ARRAY_OBJ, object.HASH_OBJ}})
+func keysFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "keys", args, 1, [][]string{{object.ARRAY_OBJ, object.HASH_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1226,8 +1382,8 @@ func keysFn(args ...object.Object) object.Object {
 }
 
 // values({"a": 1, "b": 2, "c": 3}) returns array of values
-func valuesFn(args ...object.Object) object.Object {
-	err := validateArgs("values", args, 1, [][]string{{object.HASH_OBJ}})
+func valuesFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "values", args, 1, [][]string{{object.HASH_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1242,8 +1398,8 @@ func valuesFn(args ...object.Object) object.Object {
 }
 
 // items({"a": 1, "b": 2, "c": 3}) returns array of [key, value] tuples: [[a, 1], [b, 2] [c, 3]]
-func itemsFn(args ...object.Object) object.Object {
-	err := validateArgs("items", args, 1, [][]string{{object.HASH_OBJ}})
+func itemsFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "items", args, 1, [][]string{{object.HASH_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1259,8 +1415,8 @@ func itemsFn(args ...object.Object) object.Object {
 	return &object.Array{Elements: items}
 }
 
-func joinFn(args ...object.Object) object.Object {
-	err := validateArgs("join", args, 2, [][]string{{object.ARRAY_OBJ}, {object.STRING_OBJ}})
+func joinFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "join", args, 2, [][]string{{object.ARRAY_OBJ}, {object.STRING_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1276,8 +1432,8 @@ func joinFn(args ...object.Object) object.Object {
 	return &object.String{Token: tok, Value: strings.Join(newElements, args[1].(*object.String).Value)}
 }
 
-func sleepFn(args ...object.Object) object.Object {
-	err := validateArgs("sleep", args, 1, [][]string{{object.NUMBER_OBJ}})
+func sleepFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "sleep", args, 1, [][]string{{object.NUMBER_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1287,3 +1443,81 @@ func sleepFn(args ...object.Object) object.Object {
 
 	return NULL
 }
+
+// source("fileName")
+// aka require()
+const ABS_SOURCE_DEPTH = "10"
+
+var sourceDepth, _ = strconv.Atoi(ABS_SOURCE_DEPTH)
+var sourceLevel = 0
+
+func sourceFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "source", args, 1, [][]string{{object.STRING_OBJ}})
+	if err != nil {
+		// reset the source level
+		sourceLevel = 0
+		return err
+	}
+
+	// get configured source depth if any
+	sourceDepthStr := util.GetEnvVar(globalEnv, "ABS_SOURCE_DEPTH", ABS_SOURCE_DEPTH)
+	sourceDepth, _ = strconv.Atoi(sourceDepthStr)
+
+	// limit source file inclusion depth
+	if sourceLevel >= sourceDepth {
+		// reset the source level
+		sourceLevel = 0
+		// use errObj.Message instead of errObj.Inspect() to avoid nested "ERROR: " prefixes
+		errObj := newError(tok, "maximum source file inclusion depth exceeded at %d levels", sourceDepth)
+		errObj = &object.Error{Message: errObj.Message}
+		return errObj
+	}
+	// mark this source level
+	sourceLevel++
+
+	// load the source file
+	fileName, _ := util.ExpandPath(args[0].Inspect())
+	code, error := ioutil.ReadFile(fileName)
+	if error != nil {
+		// reset the source level
+		sourceLevel = 0
+		// cannot read source file
+		return newError(tok, "cannot read source file: %s:\n%s", fileName, error.Error())
+	}
+	// parse it
+	l := lexer.New(string(code))
+	p := parser.New(l)
+	program := p.ParseProgram()
+	errors := p.Errors()
+	if len(errors) != 0 {
+		// reset the source level
+		sourceLevel = 0
+		errMsg := fmt.Sprintf("%s", " parser errors:\n")
+		for _, msg := range errors {
+			errMsg += fmt.Sprintf("%s", "\t"+msg+"\n")
+		}
+		return newError(tok, "error found in source file: %s\n%s", fileName, errMsg)
+	}
+	// invoke BeginEval() passing in the sourced program, globalEnv, and our lexer
+	// we save the current global lexer and restore it after we return from BeginEval()
+	// NB. saving the lexer allows error line numbers to be relative to any nested source files
+	savedLexer := lex
+	evaluated := BeginEval(program, globalEnv, l)
+	lex = savedLexer
+	if evaluated != nil {
+		isError := evaluated.Type() == object.ERROR_OBJ
+		if isError {
+			// reset the source level
+			sourceLevel = 0
+			// use errObj.Message instead of errObj.Inspect() to avoid nested "ERROR: " prefixes
+			evalErrMsg := evaluated.(*object.Error).Message
+			sourceErrMsg := newError(tok, "error found in source file: %s", fileName).Message
+			errObj := &object.Error{Message: fmt.Sprintf("%s\n\t%s", evalErrMsg, sourceErrMsg)}
+			return errObj
+		}
+	}
+	// restore this source level
+	sourceLevel--
+
+	return evaluated
+}
diff --git a/examples/absrc.abs b/examples/absrc.abs
new file mode 100644
index 0000000..8d4989a
--- /dev/null
+++ b/examples/absrc.abs
@@ -0,0 +1,12 @@
+## ABS init script ~/.absrc
+
+## For interactive REPL, override default prompt, history filename and size
+# if ABS_INTERACTIVE {
+#     ABS_PROMPT_LIVE_PREFIX = true
+#     ABS_PROMPT_PREFIX = "{user}@{host}:{dir}$ "
+#     ABS_HISTORY_FILE = "~/.abs_history"
+#     ABS_MAX_HISTORY_LINES = 1000
+# }
+
+## source your function libraries here
+# source("/path/to/abs/lib/library.abs")
diff --git a/examples/background-command-exit.abs b/examples/background-command-exit.abs
new file mode 100644
index 0000000..d174a88
--- /dev/null
+++ b/examples/background-command-exit.abs
@@ -0,0 +1,1 @@
+cmd = `sleep 10 &`
diff --git a/examples/background-command.abs b/examples/background-command.abs
new file mode 100644
index 0000000..ee752fc
--- /dev/null
+++ b/examples/background-command.abs
@@ -0,0 +1,33 @@
+# Simple background command
+cmd = `sleep 1; ls -la &`
+echo("Started")
+sleep(1100)
+echo("finished")
+echo(cmd)
+echo(cmd.ok)
+
+# Simple background command, we get the output by sleeping
+cmd = `sleep 1; ls -la &`
+echo("Started")
+sleep(1100)
+echo("finished")
+echo(cmd)
+echo(cmd.ok)
+
+# Background command wait
+cmd = `sleep 1; ls -la &`
+echo("Started")
+cmd.wait()
+echo("finished")
+echo(cmd)
+echo(cmd.ok)
+
+# Background command wait with error
+cmd = `sleep 1; ls la &`
+echo("Started")
+cmd.wait()
+echo("finished")
+echo(cmd)
+echo(cmd.ok)
+
+# TODO make cmd.wait() work
\ No newline at end of file
diff --git a/lexer/lexer.go b/lexer/lexer.go
index 789e5c1..d371f1a 100644
--- a/lexer/lexer.go
+++ b/lexer/lexer.go
@@ -346,19 +346,42 @@ func (l *Lexer) readIdentifier() string {
 	return l.input[position:l.position]
 }
 
+// 12
+// 12.2
+// 12e-1
+// 12e+1
+// 12e1
 func (l *Lexer) readNumber() (number string, kind token.TokenType) {
 	position := l.position
 	kind = token.NUMBER
 	hasDot := false
+	hasExponent := false
+
+	// List of character that can appear in a "number"
+	for isDigit(l.ch) || l.ch == '.' || l.ch == '+' || l.ch == '-' || l.ch == 'e' {
+		// If we have a plus / minus but there was no exponent
+		// in this number, it means we're at the end of the
+		// number and we're at an addition / subtraction.
+		if (l.ch == '+' || l.ch == '-') && !hasExponent {
+			return l.input[position:l.position], kind
+		}
+
+		// If the number contains as 'e',
+		// we're using scientific notation
+		if l.ch == 'e' {
+			hasExponent = true
+		}
 
-	for isDigit(l.ch) || l.ch == '.' {
+		// If we have a dot, let's check whether this is a range
+		// or maybe a method call (122.string())
 		if l.ch == '.' && (l.peekChar() == '.' || !isDigit(l.peekChar())) {
 			return l.input[position:l.position], token.NUMBER
 		}
 
 		if l.ch == '.' {
+			// If we have 2 dots in a number, there's a problem
 			if hasDot {
-				return "", token.ILLEGAL
+				return l.input[position : l.position+1], token.ILLEGAL
 			}
 
 			hasDot = true
@@ -367,6 +390,12 @@ func (l *Lexer) readNumber() (number string, kind token.TokenType) {
 		l.readChar()
 	}
 
+	// If the number ends with the exponent,
+	// there's a problem.
+	if l.input[l.position-1] == 'e' {
+		return l.input[position:l.position], token.ILLEGAL
+	}
+
 	return l.input[position:l.position], kind
 }
 
diff --git a/main.go b/main.go
index 8bd150b..c8e35cf 100644
--- a/main.go
+++ b/main.go
@@ -2,46 +2,20 @@ package main
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
-	"os/user"
-	"strings"
 
 	"github.com/abs-lang/abs/repl"
 )
 
-var VERSION = "1.1.0"
+var VERSION = "1.2.0"
 
 // The ABS interpreter
 func main() {
-	user, err := user.Current()
-	if err != nil {
-		panic(err)
-	}
-
 	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], "-") {
-		fmt.Printf("Hello %s, welcome to the ABS (%s) programming language!\n", user.Username, VERSION)
-		fmt.Printf("Type 'quit' when you're done, 'help' if you get lost!\n")
-		repl.Start(os.Stdin, os.Stdout)
-		return
-	}
-
-	// let's parse our argument as a file
-	code, err := ioutil.ReadFile(args[1])
-
-	if err != nil {
-		fmt.Println(err.Error())
-		os.Exit(1)
-	}
-
-	repl.Run(string(code), false)
+	// begin the REPL
+	repl.BeginRepl(args, VERSION)
 }
diff --git a/object/object.go b/object/object.go
index cbc4faf..6f85fe7 100644
--- a/object/object.go
+++ b/object/object.go
@@ -3,15 +3,17 @@ package object
 import (
 	"bytes"
 	"fmt"
+	"os/exec"
 	"sort"
 	"strconv"
 	"strings"
+	"sync"
 
 	"github.com/abs-lang/abs/ast"
 	"github.com/abs-lang/abs/token"
 )
 
-type BuiltinFunction func(args ...Object) Object
+type BuiltinFunction func(tok token.Token, args ...Object) Object
 
 type ObjectType string
 
@@ -32,6 +34,13 @@ const (
 	HASH_OBJ  = "HASH"
 )
 
+var (
+	NULL  = &Null{}
+	EOF   = &Error{Message: "EOF"}
+	TRUE  = &Boolean{Value: true}
+	FALSE = &Boolean{Value: false}
+)
+
 type HashKey struct {
 	Token token.Token
 	Type  ObjectType
@@ -134,8 +143,8 @@ func (f *Function) Inspect() string {
 // think it will only have a Value
 // property, the string itself.
 //
-// TA-DA! No, we also have an Ok
-// property that is used when running
+// TA-DA! No, we also have an Ok and Done
+// properties that are used when running
 // shell commands -- since the shell
 // will return strings.
 //
@@ -148,10 +157,21 @@ func (f *Function) Inspect() string {
 // cmd = $(curlzzzzz)
 // type(cmd) // STRING
 // cmd.ok // FALSE
+//
+// cmd = $(sleep 10 &)
+// type(cmd) // STRING
+// cmd.done // FALSE
+// cmd.wait() // ...
+// cmd.done // TRUE
 type String struct {
-	Token token.Token
-	Value string
-	Ok    *Boolean // A special property to check whether a command exited correctly
+	Token  token.Token
+	Value  string
+	Ok     *Boolean  // A special property to check whether a command exited correctly
+	Cmd    *exec.Cmd // A special property to access the underlying command
+	Stdout *bytes.Buffer
+	Stderr *bytes.Buffer
+	Done   *Boolean
+	mux    *sync.Mutex
 }
 
 func (s *String) Type() ObjectType  { return STRING_OBJ }
@@ -161,6 +181,62 @@ func (s *String) HashKey() HashKey {
 	return HashKey{Type: s.Type(), Value: s.Value}
 }
 
+// Function that ensure a mutex
+// instance is created on the
+// string
+func (s *String) mustHaveMutex() {
+	if s.mux == nil {
+		s.mux = &sync.Mutex{}
+	}
+}
+
+// To be called when the command
+// is done. Releases the internal
+// mutex.
+func (s *String) SetDone() {
+	s.mustHaveMutex()
+	s.mux.Unlock()
+}
+
+// To be called when the command
+// is starting in background, so
+// that anyone accessing it will
+// be blocked.
+func (s *String) SetRunning() {
+	s.mustHaveMutex()
+	s.mux.Lock()
+}
+
+// To be called when we want to
+// wait on the background command
+// to be done.
+func (s *String) Wait() {
+	s.mustHaveMutex()
+	s.mux.Lock()
+	s.mux.Unlock()
+}
+
+// Sets the result of the underlying command
+// on the string.
+// 3 things are set:
+// - the string itself (output of the command)
+// - str.ok
+// - str.done
+func (s *String) SetCmdResult(Ok *Boolean) {
+	s.Ok = Ok
+	var output string
+
+	if Ok.Value {
+		output = s.Stdout.String()
+	} else {
+		output = s.Stderr.String()
+	}
+
+	// trim space at both ends of out.String(); works in both linux and windows
+	s.Value = strings.TrimSpace(output)
+	s.Done = TRUE
+}
+
 type Builtin struct {
 	Token    token.Token
 	Fn       BuiltinFunction
diff --git a/repl/history.go b/repl/history.go
new file mode 100644
index 0000000..2873358
--- /dev/null
+++ b/repl/history.go
@@ -0,0 +1,119 @@
+package repl
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strconv"
+	"strings"
+
+	"github.com/abs-lang/abs/util"
+)
+
+/*
+Support for abs history file in the interactive REPL:
+
+1) The current ABS_HISTORY_FILE is loaded into the prompt.Run() cycle
+   using prompt.OptionHistory(getHistory()). This also loads the local history as well.
+   Default ABS_HISTORY_FILE is "~/.abs_history".
+2) Append each non-null, unique next line passed from prompt to the executor() to the local history.
+   NB. the live prompt history shows duplicate next lines, but they are not saved to the local history.
+3) Save the local history whenever the prompt.Run() loop exits (^D) or the executor() exits (on quit).
+   Write the local history to the ABS_HISTORY_FILE up to ABS_MAX_HISTORY_LINES (default 1000 lines).
+
+Note that ABS_HISTORY_FILE and ABS_MAX_HISTORY_LINES variables may come from the OS environment.
+*/
+
+const (
+	ABS_HISTORY_FILE      = "~/.abs_history"
+	ABS_MAX_HISTORY_LINES = "1000"
+)
+
+// Expand full path to ABS_HISTORY_FILE for current user and get ABS_MAX_HISTORY_LINES
+// 1) we look in the ABS global environment as these vars can be set by the ABS init file
+// 2) we look in the OS environment
+// 3) we use the constant defaults
+func getHistoryConfiguration() (string, int) {
+	// obtain any ABS global environment vars or OS environment vars
+	// ABS_MAX_HISTORY_LINES
+	maxHistoryLines := util.GetEnvVar(env, "ABS_MAX_HISTORY_LINES", ABS_MAX_HISTORY_LINES)
+	maxLines, err := strconv.Atoi(maxHistoryLines)
+	if err != nil {
+		maxLines, _ = strconv.Atoi(ABS_MAX_HISTORY_LINES)
+		fmt.Printf("ABS_MAX_HISTORY_LINES must be an integer: %s; using default: %d\n", maxHistoryLines, maxLines)
+	}
+	// ABS_HISTORY_FILE
+	historyFile := util.GetEnvVar(env, "ABS_HISTORY_FILE", ABS_HISTORY_FILE)
+	if maxLines > 0 {
+		// expand the ABS_HISTORY_FILE to the user's HomeDir
+		filePath, err := util.ExpandPath(historyFile)
+		if err != nil {
+			fmt.Printf("Unable to expand ABS history file path: %s\nError: %s\n", historyFile, err.Error())
+			os.Exit(99)
+		}
+		historyFile = filePath
+	}
+	return historyFile, maxLines
+}
+
+// getHistory - read the history file and split it into the local history[...] slice
+func getHistory(historyFile string, maxLines int) []string {
+	var history []string
+	if maxLines == 0 {
+		// do not open a history file for zero max lines
+		return history
+	}
+	// verify the expanded historyFile exists, if not create it now
+	fd, ok := os.OpenFile(historyFile, os.O_RDONLY|os.O_CREATE, 0666)
+	if ok != nil {
+		fmt.Printf("Cannot create or read ABS history file: %s\nError: %s\n", historyFile, ok.Error())
+		os.Exit(99)
+	}
+	fd.Close()
+	// read the file and split the lines into history[...]
+	bytes, err := ioutil.ReadFile(historyFile)
+	if err != nil {
+		return history
+	}
+	// fill the local history from the file
+	if len(bytes) > 0 {
+		history = strings.Split(string(bytes), "\n")
+	}
+	return history
+}
+
+// addToHistory - append unique next line to local history[...]
+func addToHistory(history []string, maxLines int, line string) []string {
+	if maxLines == 0 {
+		// do not save history for zero max lines
+		return []string{}
+	}
+	// do not save null lines nor duplicate the previous line in local history
+	// NB. this is not the prompt.history which shows all added lines
+	if len(line) > 0 {
+		if len(history) == 0 {
+			history = append(history, line)
+		} else if line != history[len(history)-1] {
+			history = append(history, line)
+		}
+	}
+	return history
+}
+
+// saveHistory - save the local history containing maxLines to historyFile
+func saveHistory(historyFile string, maxLines int, history []string) {
+	if maxLines == 0 {
+		// do not save a history file for zero max lines
+		return
+	}
+	if len(history) > maxLines {
+		// remove the excess lines from the front of the history slice
+		history = history[len(history)-maxLines:]
+	}
+	// write the augmented local history back out to the file
+	historyStr := strings.Join(history, "\n")
+	err := ioutil.WriteFile(historyFile, []byte(historyStr), 0664)
+	if err != nil {
+		fmt.Printf("Cannot write to ABS history file: %s\nError: %s\n", historyFile, err.Error())
+	}
+}
diff --git a/repl/init.go b/repl/init.go
new file mode 100644
index 0000000..fdfabca
--- /dev/null
+++ b/repl/init.go
@@ -0,0 +1,58 @@
+package repl
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/user"
+	"strings"
+
+	"github.com/abs-lang/abs/util"
+)
+
+// support for ABS init file
+const ABS_INIT_FILE = "~/.absrc"
+
+func getAbsInitFile(interactive bool) {
+	// get ABS_INIT_FILE from OS environment or default
+	initFile := os.Getenv("ABS_INIT_FILE")
+	if len(initFile) == 0 {
+		initFile = ABS_INIT_FILE
+	}
+	// expand the ABS_INIT_FILE to the user's HomeDir
+	filePath, err := util.ExpandPath(initFile)
+	if err != nil {
+		fmt.Printf("Unable to expand ABS init file path: %s\nError: %s\n", initFile, err.Error())
+		os.Exit(99)
+	}
+	initFile = filePath
+	// read and eval the abs init file
+	code, err := ioutil.ReadFile(initFile)
+	if err != nil {
+		// abs init file is optional -- nothing to do here
+		return
+	}
+	Run(string(code), interactive)
+}
+
+// support for user config of ABS REPL prompt string
+const ABS_PROMPT_PREFIX = "⧐  "
+
+// format ABS_PROMPT_PREFIX = "{user}@{host}:{dir} $"
+func formatLivePrefix(prefix string) string {
+	livePrefix := prefix
+	if strings.Contains(prefix, "{") {
+		userInfo, _ := user.Current()
+		user := userInfo.Username
+		host, _ := os.Hostname()
+		dir, _ := os.Getwd()
+		// shorten homedir to ~/
+		homeDir := userInfo.HomeDir
+		dir = strings.Replace(dir, homeDir, "~", 1)
+		// format the livePrefix
+		livePrefix = strings.Replace(livePrefix, "{user}", user, 1)
+		livePrefix = strings.Replace(livePrefix, "{host}", host, 1)
+		livePrefix = strings.Replace(livePrefix, "{dir}", dir, 1)
+	}
+	return livePrefix
+}
diff --git a/repl/repl.go b/repl/repl.go
index 8d02f35..ccbcfb3 100644
--- a/repl/repl.go
+++ b/repl/repl.go
@@ -3,12 +3,16 @@ package repl
 import (
 	"fmt"
 	"io"
+	"io/ioutil"
 	"os"
+	"os/user"
+	"strings"
 
 	"github.com/abs-lang/abs/evaluator"
 	"github.com/abs-lang/abs/lexer"
 	"github.com/abs-lang/abs/object"
 	"github.com/abs-lang/abs/parser"
+	"github.com/abs-lang/abs/util"
 
 	prompt "github.com/c-bata/go-prompt"
 )
@@ -23,6 +27,14 @@ var env *object.Environment
 // be available here so that other features,
 // such as suggestions, work by inspecting
 // the environment.
+
+// Support for persistent history in interactive REPL
+var (
+	historyFile string
+	maxLines    int
+	history     []string
+)
+
 func init() {
 	env = object.NewEnvironment()
 }
@@ -47,18 +59,41 @@ var LivePrefixState struct {
 }
 
 func changeLivePrefix() (string, bool) {
-	return LivePrefixState.LivePrefix, LivePrefixState.IsEnable
+	livePrefix := formatLivePrefix(LivePrefixState.LivePrefix)
+	return livePrefix, LivePrefixState.IsEnable
 }
 
 func Start(in io.Reader, out io.Writer) {
+	// get history file only when interactive REPL is running
+	historyFile, maxLines = getHistoryConfiguration()
+	history = getHistory(historyFile, maxLines)
+	// get prompt prefix template string
+	promptPrefix := util.GetEnvVar(env, "ABS_PROMPT_PREFIX", ABS_PROMPT_PREFIX)
+	// get live prompt boolean
+	livePrompt := util.GetEnvVar(env, "ABS_PROMPT_LIVE_PREFIX", "false")
+	if livePrompt == "true" {
+		LivePrefixState.LivePrefix = promptPrefix
+		LivePrefixState.IsEnable = true
+	} else {
+		if promptPrefix != formatLivePrefix(promptPrefix) {
+			// we have a template string when livePrompt mode is turned off
+			// use default static prompt instead
+			promptPrefix = ABS_PROMPT_PREFIX
+		}
+	}
+
+	// create and start the command prompt run loop
 	p := prompt.New(
 		executor,
 		completer,
-		prompt.OptionPrefix("⧐  "),
+		prompt.OptionPrefix(promptPrefix),
 		prompt.OptionLivePrefix(changeLivePrefix),
 		prompt.OptionTitle("abs-repl"),
+		prompt.OptionHistory(history),
 	)
 	p.Run()
+	// we get here on ^D from the prompt
+	saveHistory(historyFile, maxLines, history)
 }
 
 // The executor simply reads what
@@ -68,8 +103,8 @@ func Start(in io.Reader, out io.Writer) {
 // or evaluate code.
 func executor(line string) {
 	if line == "quit" {
-		fmt.Printf("%s", "Adios!")
-		fmt.Printf("%s", "\n")
+		fmt.Printf("%s\n", "Adios!")
+		saveHistory(historyFile, maxLines, history)
 		os.Exit(0)
 	}
 
@@ -88,6 +123,9 @@ func executor(line string) {
 		return
 	}
 
+	// record this line for posterity
+	history = addToHistory(history, maxLines, line)
+
 	Run(line, true)
 }
 
@@ -138,3 +176,49 @@ func printParserErrors(errors []string) {
 		fmt.Printf("%s", "\t"+msg+"\n")
 	}
 }
+
+// BeginRepl (args) -- the REPL, both interactive and script modes begin here
+// This allows us to prime the global env with ABS_INTERACTIVE = true/false,
+// load the builtin Fns names for the use of command completion, and
+// load the ABS_INIT_FILE into the global env
+func BeginRepl(args []string, version string) {
+	// if we're called without arguments, this is interactive REPL, otherwise a script
+	var interactive bool
+	if len(args) == 1 || strings.HasPrefix(args[1], "-") {
+		interactive = true
+		env.Set("ABS_INTERACTIVE", evaluator.TRUE)
+	} else {
+		interactive = false
+		env.Set("ABS_INTERACTIVE", evaluator.FALSE)
+	}
+
+	// get abs init file
+	// user may test ABS_INTERACTIVE to decide what code to run
+	getAbsInitFile(interactive)
+
+	if interactive {
+		// preload the ABS global env with the builtin Fns names
+		for k, v := range evaluator.Fns {
+			env.Set(k, v)
+		}
+		// launch the interactive REPL
+		user, err := user.Current()
+		if err != nil {
+			panic(err)
+		}
+		fmt.Printf("Hello %s, welcome to the ABS (%s) programming language!\n", user.Username, version)
+		fmt.Printf("Type 'quit' when you're done, 'help' if you get lost!\n")
+		Start(os.Stdin, os.Stdout)
+	} else {
+		// this is a script
+		// let's parse our argument as a file and run it
+		code, err := ioutil.ReadFile(args[1])
+		if err != nil {
+			fmt.Println(err.Error())
+			os.Exit(99)
+		}
+
+		Run(string(code), false)
+	}
+
+}
diff --git a/util/util.go b/util/util.go
index d3d870e..511db3d 100644
--- a/util/util.go
+++ b/util/util.go
@@ -1,6 +1,13 @@
 package util
 
-import "strconv"
+import (
+	"os"
+	"os/user"
+	"path/filepath"
+	"strconv"
+
+	"github.com/abs-lang/abs/object"
+)
 
 // Checks whether the element e is in the
 // list of strings s
@@ -18,3 +25,33 @@ func IsNumber(s string) bool {
 
 	return err == nil
 }
+
+// ExpandPath (path) resolves leading "~/" to user's HomeDir
+// returns expanded path, error
+func ExpandPath(path string) (string, error) {
+	if len(path) == 0 || path[0] != '~' {
+		return path, nil
+	}
+	usr, err := user.Current()
+	if err != nil {
+		return "", err
+	}
+	return filepath.Join(usr.HomeDir, path[1:]), nil
+}
+
+// GetEnvVar (varName, defaultVal)
+// Return the varName value from the ABS env, or OS env, or default value in that order
+func GetEnvVar(env *object.Environment, varName, defaultVal string) string {
+	var ok bool
+	var value string
+	valueObj, ok := env.Get(varName)
+	if ok {
+		value = valueObj.Inspect()
+	} else {
+		value = os.Getenv(varName)
+		if len(value) == 0 {
+			value = defaultVal
+		}
+	}
+	return value
+}


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index 87a9406..d8c18c6 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -3,6 +3,7 @@ package evaluator
 import (
 	"flag"
 	"fmt"
+	"runtime"
 	"strings"
 	"testing"
 
@@ -48,6 +49,9 @@ func TestEvalNumberExpression(t *testing.T) {
 		input    string
 		expected float64
 	}{
+		{"1e1", 10},
+		{"1e-1", 0.1},
+		{"1e+1", 10},
 		{"5.5", 5.5},
 		{"1.1 + 2.1", 3.2},
 		{"5.5 + 2.2", 7.7},
@@ -500,12 +504,11 @@ b, c = [1, 2]; b`, 1},
 		{`a = 10 + 1 + 2
 		b, c = [1, 2]; a`, 13},
 		{`
-		tz = $(echo "10/20")
+		tz = "10/20"
 		a, b = tz.split("/")
 		a.int()
 				`, 10},
 	}
-
 	for _, tt := range tests {
 		evaluated := testEval(tt.input)
 		switch expected := tt.expected.(type) {
@@ -664,6 +667,8 @@ func TestBuiltinFunctions(t *testing.T) {
 		{`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).suffix("/evaluator")`, true}, // Little trick to get travis to run this test, as the base path is not /go/src/
+		{`cwd = cd(); cwd == pwd()`, true},
+		{`cwd = cd("path/to/nowhere"); cwd == pwd()`, false},
 		{`rand(1)`, 0},
 		{`int(10)`, 10},
 		{`int(10.5)`, 10},
@@ -777,6 +782,26 @@ c")`, []string{"a", "b", "c"}},
 		{`"  A great movie  ".trim()`, "A great movie"},
 		{`"  A great movie  ".trim_by(" A")`, "great movie"},
 		{`sleep(1000)`, nil},
+		{`1.round()`, 1},
+		{`1.round(2)`, 1.00},
+		{`1.23.round(1)`, 1.2},
+		{`1.66.round(1)`, 1.7},
+		{`"1.23".round(1)`, 1.2},
+		{`"1.66".round(1)`, 1.7},
+		{`1.floor()`, 1},
+		{`1.floor()`, 1},
+		{`1.23.floor()`, 1},
+		{`1.66.floor()`, 1},
+		{`"1.23".floor()`, 1},
+		{`"1.66".floor()`, 1},
+		{`1.ceil()`, 1},
+		{`1.ceil()`, 1},
+		{`1.23.ceil()`, 2},
+		{`1.66.ceil()`, 2},
+		{`"1.23".ceil()`, 2},
+		{`"1.66".ceil()`, 2},
+		{`sleep(0.01)`, nil},
+		{`$()`, ""},
 	}
 	for _, tt := range tests {
 		evaluated := testEval(tt.input)
@@ -995,7 +1020,7 @@ func TestInExpressions(t *testing.T) {
 			errObj, ok := evaluated.(*object.Error)
 
 			if !ok {
-				t.Errorf("no error object returned. got=%T(%+v)", evaluated)
+				t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)
 				continue
 			}
 			logErrorWithPosition(t, errObj.Message, expected)
@@ -1011,13 +1036,12 @@ func TestBuiltinProperties(t *testing.T) {
 	}{
 		{`"a".ok`, false},
 		{`"a".inv`, "invalid property 'inv' on type STRING"},
-		{"a = $(date);\na.ok", true},
+		{"a = $(echo hello);\na.ok", true},
 		{`{}.a`, nil},
 		{`{"a": 1}.a`, 1},
 		{`{1: 1}.1`, "unusable as hash key: NUMBER"},
 		{`[].a`, "invalid property 'a' on type ARRAY"},
 	}
-
 	for _, tt := range tests {
 		evaluated := testEval(tt.input)
 
@@ -1081,22 +1105,43 @@ func TestBuiltinProperties(t *testing.T) {
 }
 
 func TestCommand(t *testing.T) {
-	tests := []struct {
+	type testLine struct {
 		input    string
 		expected interface{}
-	}{
-		{`a = "A"; b = "B"; eee = "-e"; $(echo $eee -n $a$a$b$b$c$c)`, "AABB"},
-		{`$(echo -n "123")`, "123"},
-		{`$(echo -n hello world)`, "hello world"},
-		{`$(echo hello world | xargs echo -n)`, "hello world"},
-		{`$(echo \$CONTEXT)`, "abs"},
-		{"a = 'A'; b = 'B'; eee = '-e'; `echo $eee -n $a$a$b$b$c$c`", "AABB"},
-		{"`echo -n '123'`", "123"},
-		{"`echo -n hello world`", "hello world"},
-		{"`echo hello world | xargs echo -n`", "hello world"},
-		{"`echo \\$CONTEXT`", "abs"},
 	}
-
+	var tests []testLine
+	if runtime.GOOS == "windows" {
+		// cmd.exe commands
+		tests = []testLine{
+			{`a = "A"; b = "B"; $(echo $a$a$b$b$c$c)`, "AABB"},
+			{`$(echo 123)`, "123"},
+			{`$(echo hello world)`, "hello world"},
+			{"a = 'A'; b = 'B'; `echo $a$a$b$b$c$c`", "AABB"},
+			{"`echo 123`", "123"},
+			{"`echo hello world`", "hello world"},
+		}
+	} else {
+		// bash commands
+		tests = []testLine{
+			{`a = "A"; b = "B"; eee = "-e"; $(echo $eee -n $a$a$b$b$c$c)`, "AABB"},
+			{`$(echo -n "123")`, "123"},
+			{`$(echo -n hello world)`, "hello world"},
+			{`$(echo hello world | xargs echo -n)`, "hello world"},
+			{`$(echo \$CONTEXT)`, "abs"},
+			{"a = 'A'; b = 'B'; eee = '-e'; `echo $eee -n $a$a$b$b$c$c`", "AABB"},
+			{"`echo -n '123'`", "123"},
+			{"`echo -n hello world`", "hello world"},
+			{"`echo hello world | xargs echo -n`", "hello world"},
+			{"`echo \\$CONTEXT`", "abs"},
+			{"`sleep 0.01`", ""},
+			{"`sleep 0.01`.done", true},
+			{"`sleep 0.01`.ok", true},
+			{"`sleep 0.01 &`", ""},
+			{"`sleep 0.01 &`.done", false},
+			{"`sleep 0.01 &`.ok", false},
+			{"`sleep 0.01 &`.wait().ok", false},
+		}
+	}
 	for _, tt := range tests {
 		evaluated := testEval(tt.input)
 
diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go
index cb2af61..fd36057 100644
--- a/lexer/lexer_test.go
+++ b/lexer/lexer_test.go
@@ -76,6 +76,12 @@ nullo
 &^>><<
 $111
 '123'
+12+12
+12e10
+12e+10
+12e-10
+12e
+1.2.3
 `
 
 	tests := []struct {
@@ -282,6 +288,16 @@ $111
 		{token.BIT_LSHIFT, "<<"},
 		{token.ILLEGAL, "$111"},
 		{token.STRING, "123"},
+		{token.NUMBER, "12"},
+		{token.PLUS, "+"},
+		{token.NUMBER, "12"},
+		{token.NUMBER, "12e10"},
+		{token.NUMBER, "12e+10"},
+		{token.NUMBER, "12e-10"},
+		{token.ILLEGAL, "12e"},
+		{token.ILLEGAL, "1.2."},
+		{token.DOT, "."},
+		{token.NUMBER, "3"},
 		{token.EOF, ""},
 	}
 
diff --git a/tests/test-absrc.abs b/tests/test-absrc.abs
new file mode 100644
index 0000000..da92aa1
--- /dev/null
+++ b/tests/test-absrc.abs
@@ -0,0 +1,8 @@
+## ABS init script 
+## For interactive REPL, set prompt and override default history filename and size
+if ABS_INTERACTIVE {
+    ABS_PROMPT_LIVE_PREFIX = true
+    ABS_PROMPT_PREFIX = "{user}@{host}:{dir}$ "
+    ABS_HISTORY_FILE = "~/.abs_hist"
+    ABS_MAX_HISTORY_LINES = 500
+}
diff --git a/tests/test-builtins.abs b/tests/test-builtins.abs
new file mode 100644
index 0000000..26e391b
--- /dev/null
+++ b/tests/test-builtins.abs
@@ -0,0 +1,3 @@
+echo(cd())
+echo(cd("path/to/nowhere"))
+echo(pop(1,2,3))
diff --git a/tests/test-source-file-x.abs b/tests/test-source-file-x.abs
new file mode 100644
index 0000000..d4de10e
--- /dev/null
+++ b/tests/test-source-file-x.abs
@@ -0,0 +1,2 @@
+# source the next level
+source("tests/test-source-file-y.abs")
diff --git a/tests/test-source-file-y.abs b/tests/test-source-file-y.abs
new file mode 100644
index 0000000..e12fe27
--- /dev/null
+++ b/tests/test-source-file-y.abs
@@ -0,0 +1,4 @@
+# source an unknown file
+
+source("tests/test-source-file-z.abs")
+
diff --git a/tests/test-source-file.abs b/tests/test-source-file.abs
new file mode 100644
index 0000000..cdf254c
--- /dev/null
+++ b/tests/test-source-file.abs
@@ -0,0 +1,14 @@
+# source some files
+
+# source("tests/test-hash-funcs.abs")
+# echo("sourced hash h = %v", h)
+# return [h, true]
+
+# src("tests/test-assign-index.abs")
+# echo("sourced hash h = %v", h)
+# echo("sourced array a = %v", a)
+
+# test recursion at given depth
+ABS_SOURCE_DEPTH = 5
+source("tests/test-source-file-x.abs")
+