Repository: abs-lang/abs
Pull request #147 (2019-01-31 16:54:18)

Issue 36 implement hash standard functions
ref. issue #36.

Added new functionality:
1) Hash standard functions `keys()`, `values()`, `items()`, and `pop(key)`.
2) Bonus: `if "str" in "string"` (pythonista :-)
3) Docs and go tests
4) `tests/test-hash-funcs.abs`

Note that I restructured `functions.go`. I moved all the anonymous function bodies out of the `getFns()` map initializer because this is way too much nested inline code that is too easy to break. Each function is now a real definition such as `func lenFn()` that is ref'd in the `Fn: lenFn` key of the map. This also makes it possible for my VSCode editor to find all the builtin function defs and refs in the search tools.

Paying off the technical debt now before we really start expanding the builtin functions.

odino: Thanks mate!

Patch:
diff --git a/docs/syntax/operators.md b/docs/syntax/operators.md
index be78d14..56987be 100644
--- a/docs/syntax/operators.md
+++ b/docs/syntax/operators.md
@@ -100,12 +100,14 @@ a /= 2 # a is now 5
 
 ## in
 
-Checks whether a value is in an array:
+Checks whether a value is in an array or a substring within a string:
 
 ``` bash
 1 in [1, 2, 3] # true
 9 in [1, 2, 3] # false
 9 in 9 # unknown operator: NUMBER in NUMBER
+"str" in "string" # true
+"xyz" in "string" # false
 ```
 
 ## **
diff --git a/docs/types/array.md b/docs/types/array.md
index 8fed1ca..00fea46 100644
--- a/docs/types/array.md
+++ b/docs/types/array.md
@@ -39,6 +39,23 @@ x += [3]
 x # [1, 2, 3]
 ```
 
+In a similar way, we can make a **shallow** copy of an array using the `+` operator with an empty array. Be careful, the empty array must be on the left side of the `+` operator.
+
+```bash
+a = [1, 2, 3]
+a   # [1, 2, 3]
+
+# shallow copy an array using the + operator with an empty array
+# note well that the empty array must be on the left side of the +
+b = [] + a
+b   # [1, 2, 3]
+
+# modify the shallow copy without changing the original
+b[0] = 99
+b   # [99, 2, 3]
+a   # [1, 2, 3]
+```
+
 It is also possible to modify an existing array element using `array[index]` assignment. This also works with compound operators such as `+=` :
 ```bash
 a = [1, 2, 3, 4]
@@ -145,7 +162,7 @@ Returns the first element that returns `true` when applied to the function `f`:
 ["hello", 0, 1, 2].find(f(x){type(x) == "NUMBER"}) # 0
 ```
 
-### find(f)
+### filter(f)
 
 Returns a new array with only the elements that returned
 `true` when applied to the function `f`:
diff --git a/docs/types/hash.md b/docs/types/hash.md
index daf8335..a9a081e 100644
--- a/docs/types/hash.md
+++ b/docs/types/hash.md
@@ -37,17 +37,36 @@ h.y = 20
 h # {a: 88, b: 2, c: 3, x: 10, y: 20}
 ```
 
-It is also possible to extend a hash using the `+=` operator with another hash. Note that existing keys in the left side will be replaced with the same key on the right side.
+It is also possible to extend a hash using the `+=` operator with another hash. Note that any existing keys on the left side will be replaced with the same key from the right side.
+
 ```bash
 h = {"a": 1, "b": 2, "c": 3}
-h # {a: 1, b: 2, c: 3}
+h   # {a: 1, b: 2, c: 3}
 
 # extending a hash by += compound operator
 h += {"c": 33, "d": 4, "e": 5}
-h # {a: 1, b: 2, c: 33, d: 4, e: 5}
+h   # {a: 1, b: 2, c: 33, d: 4, e: 5}
 ```
 
-If the left side is a `hash["key"]` or `hash.key` and the right side is a hash, then the resulting hash will have a new nested hash at the `hash.new`. This includes `hash["newkey"]` or `hash.newkey` as well.
+In a similar way, we can make a **shallow** copy of a hash using the `+` operator with an empty hash. Be careful, the empty hash must be on the left side of the `+` operator.
+
+```bash
+a = {"a": 1, "b": 2, "c": 3}
+a   # {a: 1, b: 2, c: 3}
+
+# shallow copy a hash using the + operator with an empty hash
+# note well that the empty hash must be on the left side of the +
+b = {} + a
+b   # {a: 1, b: 2, c: 3}
+
+# modify the shallow copy without changing the original
+b.a = 99
+b   # {a: 99, b: 2, c: 3}
+a   # {a: 1, b: 2, c: 3}
+```
+
+If the left side is a `hash["key"]` or `hash.key` and the right side is a hash, then the resulting hash will have a new nested hash at `hash.newkey`. This includes `hash["newkey"]` or `hash.newKey` as well.
+
 ```bash
 h = {"a": 1, "b": 2, "c": 3}
 h # {a: 1, b: 2, c: 3}
@@ -64,17 +83,66 @@ h # {a: 1, b: 2, c: {x: 10, y: 20}, z: {xx: 11, yy: 21}}
 ## Supported functions
 
 ### str()
-
 Returns the string representation of the hash:
 
 ``` bash
-{"k": "v"}.str() # "{k: v}"
+h = {"k": "v"}
+h.str() # "{k: v}"
+str(h)  # "{k: v}"
+```
+
+### keys()
+Returns an array of keys to the hash. 
+
+Note well that only the first level keys are returned.
+
+``` bash
+h = {"a": 1, "b": 2, "c": 3}
+h.keys() # [a, b, c]
+keys(h) # [a, b, c]
+```
+
+### values()
+Returns an array of values in the hash. 
+
+Note well that only the first level values are returned.
+
+``` bash
+h = {"a": 1, "b": 2, "c": 3}
+h.values()  # [1, 2, 3]
+values(h)   # [1, 2, 3]
+```
+
+### items()
+Returns an array of [key, value] tuples for each item in the hash.
+
+Note well that only the first level items are returned.
+
+``` bash
+h = {"a": 1, "b": 2, "c": 3}
+h.items()   # [[a, 1], [b, 2], [c, 3]]
+items(h)    # [[a, 1], [b, 2], [c, 3]]
+```
+
+### pop(k)
+Removes and returns the matching `{"key": value}` item from the hash. If the key does not exist `hash.pop("key")` returns `null`.
+
+Note well that only the first level items can be popped.
+
+``` bash
+h = {"a": 1, "b": 2, "c": {"x": 10, "y":20}}
+
+h.pop("a")  # {a: 1}
+h   # {b: 2, c: {x: 10, y: 20}}
+
+pop(h, "c")  # {c: {x: 10, y: 20}}
+h   # {b: 2}
+
+pop(h, "d") # null
+h   # {b: 2}
+
 ```
 
-Note that hashes are set to receive a substantial
-boost in the future, as most hash-related
-functions didn't make the cut to ABS' first public
-release (see [#36](https://github.com/abs-lang/abs/issues/36)).
 
 ## Next
 
diff --git a/docs/types/string.md b/docs/types/string.md
index 3fedcd4..a44fba5 100644
--- a/docs/types/string.md
+++ b/docs/types/string.md
@@ -47,6 +47,13 @@ a value that evaluates to `false` when casted to boolean:
 !!"" # false
 ```
 
+To test for the existence of substrings within strings use the `in` operator:
+
+``` bash
+"str" in "string"   # true
+"xyz" in "string"   # false
+```
+
 ## Special characters embedded in strings
 Double and single quoted strings behave differently if the string contains escaped special ASCII line control characters such as `LF "\n"`, `CR "\r"`, and `TAB "\t"`. 
 
@@ -191,6 +198,11 @@ Parses the string as JSON, returning a [hash](/types/hash):
 
 ### contains(str)
 
+> This function is deprecated and might be removed in future versions.
+>
+> Use the "in" operator instead: `"str" in "string"`
+
+
 Checks whether `str` is present in the string:
 
 ``` bash
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index faa9439..c190f5a 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -303,10 +303,8 @@ func evalPropertyAssignment(pex *ast.PropertyExpression, expr object.Object, env
 		pair := object.HashPair{Key: prop, Value: expr}
 		hashObject.Pairs[hashed] = pair
 		return NULL
-	} else {
-		return newError(pex.Token, "can only assign to hash property, got %s", leftObj.Type())
 	}
-	return NULL
+	return newError(pex.Token, "can only assign to hash property, got %s", leftObj.Type())
 }
 
 func evalAssignment(as *ast.AssignStatement, env *object.Environment) object.Object {
@@ -407,7 +405,7 @@ func evalInfixExpression(
 		return evalArrayInfixExpression(tok, operator, left, right)
 	case left.Type() == object.HASH_OBJ && right.Type() == object.HASH_OBJ:
 		return evalHashInfixExpression(tok, operator, left, right)
-	case operator == "in" && right.Type() == object.ARRAY_OBJ:
+	case operator == "in" && (right.Type() == object.ARRAY_OBJ || right.Type() == object.STRING_OBJ):
 		return evalInExpression(left, right)
 	case operator == "==":
 		return nativeBoolToBooleanObject(left == right)
@@ -559,6 +557,10 @@ func evalStringInfixExpression(
 		return &object.Boolean{Token: tok, Value: strings.ToLower(left.(*object.String).Value) == strings.ToLower(right.(*object.String).Value)}
 	}
 
+	if operator == "in" {
+		return evalInExpression(left, right)
+	}
+
 	return newError(tok, "unknown operator: %s %s %s", left.Type(), operator, right.Type())
 }
 
@@ -601,28 +603,34 @@ func evalInExpression(
 	left, right object.Object,
 ) object.Object {
 	var found bool
-	array := right.(*object.Array)
 
-	switch needle := left.(type) {
-	case *object.String:
-		for _, v := range array.Elements {
-			if v.Inspect() == needle.Value && v.Type() == object.STRING_OBJ {
-				found = true
-				break // Let's get outta here!
+	switch rightObj := right.(type) {
+	case *object.Array:
+		switch needle := left.(type) {
+		case *object.String:
+			for _, v := range rightObj.Elements {
+				if v.Inspect() == needle.Value && v.Type() == object.STRING_OBJ {
+					found = true
+					break // Let's get outta here!
+				}
 			}
-		}
-	case *object.Number:
-		for _, v := range array.Elements {
-			// Quite ghetto but also the easiest way out
-			// Instead of doing type checking on the argument,
-			// we received back its string representation.
-			// If they match, we then check that its type was
-			// integer.
-			if v.Inspect() == strconv.Itoa(int(needle.Value)) && v.Type() == object.NUMBER_OBJ {
-				found = true
-				break // Let's get outta here!
+		case *object.Number:
+			for _, v := range rightObj.Elements {
+				// Quite ghetto but also the easiest way out
+				// Instead of doing type checking on the argument,
+				// we received back its string representation.
+				// If they match, we then check that its type was
+				// integer.
+				if v.Inspect() == strconv.Itoa(int(needle.Value)) && v.Type() == object.NUMBER_OBJ {
+					found = true
+					break // Let's get outta here!
+				}
 			}
 		}
+	case *object.String:
+		if left.Type() == object.STRING_OBJ {
+			found = strings.Contains(right.Inspect(), left.Inspect())
+		}
 	}
 
 	return &object.Boolean{Token: tok, Value: found}
diff --git a/evaluator/functions.go b/evaluator/functions.go
index 6c7b7ea..b247e7f 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -26,1030 +26,1242 @@ func init() {
 	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(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(tok, "argument %d to %s(...) is not supported (got: %s, allowed: %s)", i, name, args[i].Inspect(), strings.Join(t, ", "))
-		}
-	}
-
-	return nil
-}
+/*
+Here be the hairy map to all the Builtin Functions ... ARRRGH, matey
+*/
 
 func getFns() map[string]*object.Builtin {
 	return map[string]*object.Builtin{
 		// len(var:"hello")
 		"len": &object.Builtin{
 			Types: []string{object.STRING_OBJ, object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("len", args, 1, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				switch arg := args[0].(type) {
-				case *object.Array:
-					return &object.Number{Token: tok, Value: float64(len(arg.Elements))}
-				case *object.String:
-					return &object.Number{Token: tok, Value: float64(len(arg.Value))}
-				default:
-					return newError(tok, "argument to `len` not supported, got %s", args[0].Type())
-				}
-			},
+			Fn:    lenFn,
 		},
 		// rand(max:20)
 		"rand": &object.Builtin{
 			Types: []string{object.NUMBER_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("rand", args, 1, [][]string{{object.NUMBER_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				arg := args[0].(*object.Number)
-				r, e := rand.Int(rand.Reader, big.NewInt(int64(arg.Value)))
-
-				if e != nil {
-					return newError(tok, "error occurred while calling 'rand(%v)': %s", arg.Value, e.Error())
-				}
-
-				return &object.Number{Token: tok, Value: float64(r.Int64())}
-			},
+			Fn:    randFn,
 		},
 		// exit(code:0)
 		"exit": &object.Builtin{
 			Types: []string{object.NUMBER_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("exit", args, 1, [][]string{{object.NUMBER_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				arg := args[0].(*object.Number)
-				os.Exit(int(arg.Value))
-				return arg
-			},
+			Fn:    exitFn,
 		},
 		// flag("my-flag")
-		// 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
 		"flag": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("flag", args, 1, [][]string{{object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				// flag we're trying to retrieve
-				name := args[0].(*object.String)
-				found := false
-
-				// Let's loop through all the arguments
-				// passed to the script
-				// This is O(n) but again, performance
-				// is not a big deal in ABS
-				for _, v := range os.Args {
-					// If the flag was found in the previous
-					// argument...
-					if found {
-						// ...and the next one is another flag
-						// means we're done parsing
-						// eg. --flag1 --flag2
-						if strings.HasPrefix(v, "-") {
-							break
-						}
-
-						// else return the next argument
-						// eg --flag1 something --flag2
-						return &object.String{Token: tok, Value: v}
-					}
-
-					// try to parse the flag as key=value
-					parts := strings.SplitN(v, "=", 2)
-					// let's just take the left-side of the flag
-					left := parts[0]
-
-					// if the left side of the current argument corresponds
-					// to the flag we're looking for (both in the form of "--flag" and "-flag")...
-					// ..BINGO!
-					if (len(left) > 1 && left[1:] == name.Value) || (len(left) > 2 && left[2:] == name.Value) {
-						if len(parts) > 1 {
-							return &object.String{Token: tok, Value: parts[1]}
-						} else {
-							found = true
-						}
-					}
-				}
-
-				// If the flag was found but we got here
-				// it means no value was assigned to it,
-				// so let's default to true
-				if found {
-					return &object.Boolean{Token: tok, Value: true}
-				}
-
-				// else a flag that's not found is NULL
-				return NULL
-			},
+			Fn:    flagFn,
 		},
 		// pwd()
 		"pwd": &object.Builtin{
 			Types: []string{},
-			Fn: func(args ...object.Object) object.Object {
-				dir, err := os.Getwd()
-				if err != nil {
-					return newError(tok, err.Error())
-				}
-				return &object.String{Token: tok, Value: dir}
-			},
+			Fn:    pwdFn,
 		},
 		// echo(arg:"hello")
 		"echo": &object.Builtin{
 			Types: []string{},
-			Fn: func(args ...object.Object) object.Object {
-				if len(args) == 0 {
-					// allow echo() without crashing
-					fmt.Println("")
-					return NULL
-				}
-				var arguments []interface{} = make([]interface{}, len(args)-1)
-				for i, d := range args {
-					if i > 0 {
-						arguments[i-1] = d.Inspect()
-					}
-				}
-
-				fmt.Printf(args[0].Inspect(), arguments...)
-				fmt.Println("")
-
-				return NULL
-			},
+			Fn:    echoFn,
 		},
 		// int(string:"123")
 		"int": &object.Builtin{
 			Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("int", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				switch arg := args[0].(type) {
-				case *object.Number:
-					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(tok, "int(...) can only be called on strings which represent numbers, '%s' given", arg.Value)
-					}
-
-					return &object.Number{Token: tok, Value: float64(int64(i))}
-				default:
-					// we will never reach here
-					return newError(tok, "argument to `int` not supported, got %s", args[0].Type())
-				}
-			},
+			Fn:    intFn,
 		},
 		// number(string:"1.23456")
 		"number": &object.Builtin{
 			Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("number", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				switch arg := args[0].(type) {
-				case *object.Number:
-					return arg
-				case *object.String:
-					i, err := strconv.ParseFloat(arg.Value, 64)
-
-					if err != nil {
-						return newError(tok, "number(...) can only be called on strings which represent numbers, '%s' given", arg.Value)
-					}
-
-					return &object.Number{Token: tok, Value: i}
-				default:
-					// we will never reach here
-					return newError(tok, "argument to `number` not supported, got %s", args[0].Type())
-				}
-			},
+			Fn:    numberFn,
 		},
 		// is_number(string:"1.23456")
 		"is_number": &object.Builtin{
 			Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("number", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				switch arg := args[0].(type) {
-				case *object.Number:
-					return &object.Boolean{Token: tok, Value: true}
-				case *object.String:
-					return &object.Boolean{Token: tok, Value: util.IsNumber(arg.Value)}
-				default:
-					// we will never reach here
-					return newError(tok, "argument to `is_number` not supported, got %s", args[0].Type())
-				}
-			},
+			Fn:    isNumberFn,
 		},
 		// stdin()
 		"stdin": &object.Builtin{
-			Next: func(pos int) (int, object.Object) {
-				v := scanner.Scan()
-
-				if !v {
-					return pos, EOF
-				}
-
-				return pos, &object.String{Token: tok, Value: scanner.Text()}
-			},
+			Next:  stdinNextFn,
 			Types: []string{},
-			Fn: func(args ...object.Object) object.Object {
-				v := scanner.Scan()
-
-				if !v {
-					return EOF
-				}
-
-				return &object.String{Token: tok, Value: scanner.Text()}
-			},
+			Fn:    stdinFn,
 		},
 		// env(variable:"PWD")
 		"env": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("env", args, 1, [][]string{{object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				arg := args[0].(*object.String)
-				return &object.String{Token: tok, Value: os.Getenv(arg.Value)}
-			},
+			Fn:    envFn,
 		},
 		// arg(position:1)
 		"arg": &object.Builtin{
 			Types: []string{object.NUMBER_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("arg", args, 1, [][]string{{object.NUMBER_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				arg := args[0].(*object.Number)
-				i := arg.Int()
-
-				if int(i) > len(os.Args)-1 {
-					return &object.String{Token: tok, Value: ""}
-				}
-
-				return &object.String{Token: tok, Value: os.Args[i]}
-			},
+			Fn:    argFn,
 		},
 		// type(variable:"hello")
 		"type": &object.Builtin{
 			Types: []string{},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("type", args, 1, [][]string{})
-				if err != nil {
-					return err
-				}
-
-				return &object.String{Token: tok, Value: string(args[0].Type())}
-			},
+			Fn:    typeFn,
 		},
 		// split(string:"hello")
 		"split": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("split", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				s := args[0].(*object.String)
-				sep := args[1].(*object.String)
-
-				parts := strings.Split(s.Value, sep.Value)
-				length := len(parts)
-				elements := make([]object.Object, length, length)
-
-				for k, v := range parts {
-					elements[k] = &object.String{Token: tok, Value: v}
-				}
-
-				return &object.Array{Elements: elements}
-			},
+			Fn:    splitFn,
 		},
 		// lines(string:"a\nb")
 		"lines": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("lines", args, 1, [][]string{{object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				s := args[0].(*object.String)
-				parts := strings.FieldsFunc(s.Value, func(r rune) bool {
-					return r == '\n' || r == '\r' || r == '\f'
-				})
-				length := len(parts)
-				elements := make([]object.Object, length, length)
-
-				for k, v := range parts {
-					elements[k] = &object.String{Token: tok, Value: v}
-				}
-
-				return &object.Array{Elements: elements}
-			},
+			Fn:    linesFn,
 		},
 		// "{}".json()
-		//
 		// Converts a valid JSON document to an ABS hash.
-		//
-		// 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
-		// allow to do things like:
-		//
-		// x = 10
-		// '{"key": x}'.json()["key"] // 10
-		//
-		// Also, we're instantiating a new lexer & parser from
-		// scratch, so this is a tad slow.
 		"json": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("json", args, 1, [][]string{{object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				s := args[0].(*object.String)
-				str := strings.TrimSpace(s.Value)
-				env := object.NewEnvironment()
-				l := lexer.New(str)
-				p := parser.New(l)
-				var node ast.Node
-				ok := false
-
-				// JSON types:
-				// - objects
-				// - arrays
-				// - number
-				// - string
-				// - null
-				// - bool
-				switch str[0] {
-				case '{':
-					node, ok = p.ParseHashLiteral().(*ast.HashLiteral)
-				case '[':
-					node, ok = p.ParseArrayLiteral().(*ast.ArrayLiteral)
-				}
-
-				if str[0] == '"' && str[len(str)-1] == '"' {
-					node, ok = p.ParseStringLiteral().(*ast.StringLiteral)
-				}
-
-				if util.IsNumber(str) {
-					node, ok = p.ParseNumberLiteral().(*ast.NumberLiteral)
-				}
-
-				if str == "false" || str == "true" {
-					node, ok = p.ParseBoolean().(*ast.Boolean)
-				}
-
-				if str == "null" {
-					return NULL
-				}
-
-				if ok {
-					return Eval(node, env)
-				}
-
-				return newError(tok, "argument to `json` must be a valid JSON object, got '%s'", s.Value)
-			},
+			Fn:    jsonFn,
 		},
 		// "a %s".fmt(b)
 		"fmt": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				// err := validateArgs("fmt", args, 1, [][]string{{object.STRING_OBJ}})
-				// if err != nil {
-				// return err
-				// }
-
-				list := []interface{}{}
-
-				for _, s := range args[1:] {
-					list = append(list, s.Inspect())
-				}
-
-				return &object.String{Token: tok, Value: fmt.Sprintf(args[0].(*object.String).Value, list...)}
-			},
+			Fn:    fmtFn,
 		},
 		// sum(array:[1, 2, 3])
 		"sum": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("sum", args, 1, [][]string{{object.ARRAY_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				arr := args[0].(*object.Array)
-				if arr.Empty() {
-					return &object.Number{Token: tok, Value: float64(0)}
-				}
-
-				if !arr.Homogeneous() {
-					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(tok, "sum(...) can only be called on arrays of numbers, got %s", arr.Inspect())
-				}
-
-				var sum float64 = 0
-
-				for _, v := range arr.Elements {
-					elem := v.(*object.Number)
-					sum += elem.Value
-				}
-
-				return &object.Number{Token: tok, Value: sum}
-			},
+			Fn:    sumFn,
 		},
 		// sort(array:[1, 2, 3])
 		"sort": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("sort", args, 1, [][]string{{object.ARRAY_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				arr := args[0].(*object.Array)
-				elements := arr.Elements
-
-				if len(elements) == 0 {
-					return arr
-				}
-
-				if !arr.Homogeneous() {
-					return newError(tok, "argument to `sort` must be an homogeneous array (elements of the same type), got %s", arr.Inspect())
-				}
-
-				switch elements[0].(type) {
-				case *object.Number:
-					a := []float64{}
-					for _, v := range elements {
-						a = append(a, v.(*object.Number).Value)
-					}
-					sort.Float64s(a)
-
-					o := []object.Object{}
-
-					for _, v := range a {
-						o = append(o, &object.Number{Token: tok, Value: v})
-					}
-					return &object.Array{Elements: o}
-				case *object.String:
-					a := []string{}
-					for _, v := range elements {
-						a = append(a, v.(*object.String).Value)
-					}
-					sort.Strings(a)
-
-					o := []object.Object{}
-
-					for _, v := range a {
-						o = append(o, &object.String{Token: tok, Value: v})
-					}
-					return &object.Array{Elements: o}
-				default:
-					return newError(tok, "cannot sort an array with given elements elements (%s)", arr.Inspect())
-				}
-			},
+			Fn:    sortFn,
 		},
 		// map(array:[1, 2, 3], function:f(x) { x + 1 })
 		"map": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("map", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				arr := args[0].(*object.Array)
-				length := len(arr.Elements)
-				newElements := make([]object.Object, length, length)
-				copy(newElements, arr.Elements)
-
-				for k, v := range arr.Elements {
-					evaluated := applyFunction(tok, args[1], []object.Object{v})
-
-					if isError(evaluated) {
-						return evaluated
-					}
-					newElements[k] = evaluated
-				}
-
-				return &object.Array{Elements: newElements}
-			},
+			Fn:    mapFn,
 		},
 		// some(array:[1, 2, 3], function:f(x) { x == 2 })
 		"some": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("some", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				var result bool
-
-				arr := args[0].(*object.Array)
-
-				for _, v := range arr.Elements {
-					r := applyFunction(tok, args[1], []object.Object{v})
-
-					if isTruthy(r) {
-						result = true
-						break
-					}
-				}
-
-				return &object.Boolean{Token: tok, Value: result}
-			},
+			Fn:    someFn,
 		},
 		// every(array:[1, 2, 3], function:f(x) { x == 2 })
 		"every": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("every", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				result := true
-
-				arr := args[0].(*object.Array)
-
-				for _, v := range arr.Elements {
-					r := applyFunction(tok, args[1], []object.Object{v})
-
-					if !isTruthy(r) {
-						result = false
-					}
-				}
-
-				return &object.Boolean{Token: tok, Value: result}
-			},
+			Fn:    everyFn,
 		},
 		// find(array:[1, 2, 3], function:f(x) { x == 2 })
 		"find": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("find", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				arr := args[0].(*object.Array)
-
-				for _, v := range arr.Elements {
-					r := applyFunction(tok, args[1], []object.Object{v})
-
-					if isTruthy(r) {
-						return v
-					}
-				}
-
-				return NULL
-			},
+			Fn:    findFn,
 		},
 		// filter(array:[1, 2, 3], function:f(x) { x == 2 })
 		"filter": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("filter", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				result := []object.Object{}
-				arr := args[0].(*object.Array)
-
-				for _, v := range arr.Elements {
-					evaluated := applyFunction(tok, args[1], []object.Object{v})
-
-					if isError(evaluated) {
-						return evaluated
-					}
-
-					if isTruthy(evaluated) {
-						result = append(result, v)
-					}
-				}
-
-				return &object.Array{Elements: result}
-			},
+			Fn:    filterFn,
 		},
 		// contains("str", "tr")
 		"contains": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ, object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("contains", args, 2, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}, {object.STRING_OBJ, object.NUMBER_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				switch arg := args[0].(type) {
-				case *object.String:
-					needle, ok := args[1].(*object.String)
-
-					if ok {
-						return &object.Boolean{Token: tok, Value: strings.Contains(arg.Value, needle.Value)}
-					}
-				case *object.Array:
-					var found bool
-
-					switch needle := args[1].(type) {
-					case *object.String:
-						for _, v := range arg.Elements {
-							if v.Inspect() == needle.Value && v.Type() == object.STRING_OBJ {
-								found = true
-								break // Let's get outta here!
-							}
-						}
-
-						return &object.Boolean{Token: tok, Value: found}
-					case *object.Number:
-						for _, v := range arg.Elements {
-							// Quite ghetto but also the easiest way out
-							// Instead of doing type checking on the argument,
-							// we received back its string representation.
-							// If they match, we then check that its type was
-							// integer.
-							if v.Inspect() == strconv.Itoa(int(needle.Value)) && v.Type() == object.NUMBER_OBJ {
-								found = true
-								break // Let's get outta here!
-							}
-						}
-
-						return &object.Boolean{Token: tok, Value: found}
-					}
-				}
-
-				return &object.Boolean{Token: tok, Value: false}
-			},
+			Fn:    containsFn,
 		},
 		// str(1)
 		"str": &object.Builtin{
 			Types: []string{},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("str", args, 1, [][]string{})
-				if err != nil {
-					return err
-				}
-
-				return &object.String{Token: tok, Value: args[0].Inspect()}
-			},
+			Fn:    strFn,
 		},
 		// any("abc", "b")
 		"any": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("any", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				return &object.Boolean{Token: tok, Value: strings.ContainsAny(args[0].(*object.String).Value, args[1].(*object.String).Value)}
-			},
+			Fn:    anyFn,
 		},
 		// prefix("abc", "a")
 		"prefix": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("prefix", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				return &object.Boolean{Token: tok, Value: strings.HasPrefix(args[0].(*object.String).Value, args[1].(*object.String).Value)}
-			},
+			Fn:    prefixFn,
 		},
 		// suffix("abc", "a")
 		"suffix": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("suffix", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				return &object.Boolean{Token: tok, Value: strings.HasSuffix(args[0].(*object.String).Value, args[1].(*object.String).Value)}
-			},
+			Fn:    suffixFn,
 		},
 		// repeat("abc", 3)
 		"repeat": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("repeat", args, 2, [][]string{{object.STRING_OBJ}, {object.NUMBER_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				return &object.String{Token: tok, Value: strings.Repeat(args[0].(*object.String).Value, int(args[1].(*object.Number).Value))}
-			},
+			Fn:    repeatFn,
 		},
 		// replace("abc", "b", "f", -1)
 		"replace": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("replace", args, 4, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}, {object.STRING_OBJ}, {object.NUMBER_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				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))}
-			},
+			Fn:    replaceFn,
 		},
 		// title("some thing")
 		"title": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("title", args, 1, [][]string{{object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				return &object.String{Token: tok, Value: strings.Title(args[0].(*object.String).Value)}
-			},
+			Fn:    titleFn,
 		},
 		// lower("ABC")
 		"lower": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("lower", args, 1, [][]string{{object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				return &object.String{Token: tok, Value: strings.ToLower(args[0].(*object.String).Value)}
-			},
+			Fn:    lowerFn,
 		},
 		// upper("abc")
 		"upper": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("upper", args, 1, [][]string{{object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				return &object.String{Token: tok, Value: strings.ToUpper(args[0].(*object.String).Value)}
-			},
+			Fn:    upperFn,
 		},
 		// trim("abc")
 		"trim": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("trim", args, 1, [][]string{{object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				return &object.String{Token: tok, Value: strings.Trim(args[0].(*object.String).Value, " ")}
-			},
+			Fn:    trimFn,
 		},
 		// trim_by("abc", "c")
 		"trim_by": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("trim_by", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				return &object.String{Token: tok, Value: strings.Trim(args[0].(*object.String).Value, args[1].(*object.String).Value)}
-			},
+			Fn:    trimByFn,
 		},
 		// index("abc", "c")
 		"index": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("index", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				i := strings.Index(args[0].(*object.String).Value, args[1].(*object.String).Value)
-
-				if i == -1 {
-					return NULL
-				}
-
-				return &object.Number{Token: tok, Value: float64(i)}
-			},
+			Fn:    indexFn,
 		},
 		// last_index("abcc", "c")
 		"last_index": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("last_index", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				i := strings.LastIndex(args[0].(*object.String).Value, args[1].(*object.String).Value)
-
-				if i == -1 {
-					return NULL
-				}
-
-				return &object.Number{Token: tok, Value: float64(i)}
-			},
+			Fn:    lastIndexFn,
 		},
 		// slice("abcc", 0, -1)
 		"slice": &object.Builtin{
 			Types: []string{object.STRING_OBJ, object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("slice", args, 3, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}, {object.NUMBER_OBJ}, {object.NUMBER_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				start := int(args[1].(*object.Number).Value)
-				end := int(args[2].(*object.Number).Value)
-
-				switch arg := args[0].(type) {
-				case *object.String:
-					s := arg.Value
-					start, end := sliceStartAndEnd(len(s), start, end)
-
-					return &object.String{Token: tok, Value: s[start:end]}
-				case *object.Array:
-					start, end := sliceStartAndEnd(len(arg.Elements), start, end)
-
-					return &object.Array{Elements: arg.Elements[start:end]}
-				}
-
-				return NULL
-			},
+			Fn:    sliceFn,
 		},
 		// shift([1,2,3])
 		"shift": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("shift", args, 1, [][]string{{object.ARRAY_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				array := args[0].(*object.Array)
-				if len(array.Elements) == 0 {
-					return NULL
-				}
-				e := array.Elements[0]
-				array.Elements = append(array.Elements[:0], array.Elements[1:]...)
-
-				return e
-			},
+			Fn:    shiftFn,
 		},
 		// reverse([1,2,3])
 		"reverse": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("reverse", args, 1, [][]string{{object.ARRAY_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				array := args[0].(*object.Array)
-
-				for i, j := 0, len(array.Elements)-1; i < j; i, j = i+1, j-1 {
-					array.Elements[i], array.Elements[j] = array.Elements[j], array.Elements[i]
-				}
-
-				return array
-			},
+			Fn:    reverseFn,
 		},
 		// push([1,2,3], 4)
 		"push": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(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}})
-				if err != nil {
-					return err
-				}
-
-				array := args[0].(*object.Array)
-				array.Elements = append(array.Elements, args[1])
-
-				return array
-			},
+			Fn:    pushFn,
 		},
 		// pop([1,2,3], 4)
 		"pop": &object.Builtin{
-			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("pop", args, 1, [][]string{{object.ARRAY_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				array := args[0].(*object.Array)
-				elem := array.Elements[len(array.Elements)-1]
-				array.Elements = array.Elements[0 : len(array.Elements)-1]
-
-				return elem
-			},
+			Types: []string{object.ARRAY_OBJ, object.HASH_OBJ},
+			Fn:    popFn,
 		},
-		// keys([1,2,3])
+		// keys([1,2,3]) returns array of indices
+		// keys({"a": 1, "b": 2, "c": 3}) returns array of keys
 		"keys": &object.Builtin{
-			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("keys", args, 1, [][]string{{object.ARRAY_OBJ}})
-				if err != nil {
-					return err
-				}
-
-				arr := args[0].(*object.Array)
-				length := len(arr.Elements)
-				newElements := make([]object.Object, length, length)
-
-				for k, _ := range arr.Elements {
-					newElements[k] = &object.Number{Token: tok, Value: float64(k)}
-				}
-
-				return &object.Array{Elements: newElements}
-			},
+			Types: []string{object.ARRAY_OBJ, object.HASH_OBJ},
+			Fn:    keysFn,
+		},
+		// values({"a": 1, "b": 2, "c": 3}) returns array of values
+		"values": &object.Builtin{
+			Types: []string{object.HASH_OBJ},
+			Fn:    valuesFn,
+		},
+		// items({"a": 1, "b": 2, "c": 3}) returns array of [key, value] tuples: [[a, 1], [b, 2] [c, 3]]
+		"items": &object.Builtin{
+			Types: []string{object.HASH_OBJ},
+			Fn:    itemsFn,
 		},
 		// join([1,2,3], "-")
 		"join": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
-			Fn: func(args ...object.Object) object.Object {
-				err := validateArgs("join", args, 2, [][]string{{object.ARRAY_OBJ}, {object.STRING_OBJ}})
-				if err != nil {
-					return err
-				}
+			Fn:    joinFn,
+		},
+	}
+}
 
-				arr := args[0].(*object.Array)
-				length := len(arr.Elements)
-				newElements := make([]string, length, length)
+/*
+Here be the actual Builtin Functions
+*/
 
-				for k, v := range arr.Elements {
-					newElements[k] = v.Inspect()
-				}
+// 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 {
+		return newError(tok, "wrong number of arguments to %s(...): got=%d, want=%d", name, len(args), size)
+	}
 
-				return &object.String{Token: tok, Value: strings.Join(newElements, args[1].(*object.String).Value)}
-			},
-		},
+	for i, t := range types {
+		if !util.Contains(t, string(args[i].Type())) {
+			return newError(tok, "argument %d to %s(...) is not supported (got: %s, allowed: %s)", i, name, args[i].Inspect(), strings.Join(t, ", "))
+		}
+	}
+
+	return nil
+}
+
+// len(var:"hello")
+func lenFn(args ...object.Object) object.Object {
+	err := validateArgs("len", args, 1, [][]string{{object.STRING_OBJ, object.ARRAY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	switch arg := args[0].(type) {
+	case *object.Array:
+		return &object.Number{Token: tok, Value: float64(len(arg.Elements))}
+	case *object.String:
+		return &object.Number{Token: tok, Value: float64(len(arg.Value))}
+	default:
+		return newError(tok, "argument to `len` not supported, got %s", args[0].Type())
 	}
 }
 
-// Clamps starts and end arguments to the slice
-// function. When you slice "abc" you can have
-// start=10 and end -20...
-func sliceStartAndEnd(l int, start int, end int) (int, int) {
-	if end == 0 {
-		end = l
+// rand(max:20)
+func randFn(args ...object.Object) object.Object {
+	err := validateArgs("rand", args, 1, [][]string{{object.NUMBER_OBJ}})
+	if err != nil {
+		return err
 	}
 
-	if start > l {
-		start = l
+	arg := args[0].(*object.Number)
+	r, e := rand.Int(rand.Reader, big.NewInt(int64(arg.Value)))
+
+	if e != nil {
+		return newError(tok, "error occurred while calling 'rand(%v)': %s", arg.Value, e.Error())
 	}
 
-	if start < 0 {
-		newStart := l + start
-		if newStart < 0 {
-			start = 0
-		} else {
-			start = newStart
+	return &object.Number{Token: tok, Value: float64(r.Int64())}
+}
+
+// exit(code:0)
+func exitFn(args ...object.Object) object.Object {
+	err := validateArgs("exit", args, 1, [][]string{{object.NUMBER_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	arg := args[0].(*object.Number)
+	os.Exit(int(arg.Value))
+	return arg
+}
+
+// flag("my-flag")
+func flagFn(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}})
+	if err != nil {
+		return err
+	}
+
+	// flag we're trying to retrieve
+	name := args[0].(*object.String)
+	found := false
+
+	// Let's loop through all the arguments
+	// passed to the script
+	// This is O(n) but again, performance
+	// is not a big deal in ABS
+	for _, v := range os.Args {
+		// If the flag was found in the previous
+		// argument...
+		if found {
+			// ...and the next one is another flag
+			// means we're done parsing
+			// eg. --flag1 --flag2
+			if strings.HasPrefix(v, "-") {
+				break
+			}
+
+			// else return the next argument
+			// eg --flag1 something --flag2
+			return &object.String{Token: tok, Value: v}
+		}
+
+		// try to parse the flag as key=value
+		parts := strings.SplitN(v, "=", 2)
+		// let's just take the left-side of the flag
+		left := parts[0]
+
+		// if the left side of the current argument corresponds
+		// to the flag we're looking for (both in the form of "--flag" and "-flag")...
+		// ..BINGO!
+		if (len(left) > 1 && left[1:] == name.Value) || (len(left) > 2 && left[2:] == name.Value) {
+			if len(parts) > 1 {
+				return &object.String{Token: tok, Value: parts[1]}
+			} else {
+				found = true
+			}
 		}
 	}
 
-	if end > l || start > end {
-		end = l
+	// If the flag was found but we got here
+	// it means no value was assigned to it,
+	// so let's default to true
+	if found {
+		return &object.Boolean{Token: tok, Value: true}
 	}
 
-	return start, end
+	// else a flag that's not found is NULL
+	return NULL
+}
+
+// pwd()
+func pwdFn(args ...object.Object) object.Object {
+	dir, err := os.Getwd()
+	if err != nil {
+		return newError(tok, err.Error())
+	}
+	return &object.String{Token: tok, Value: dir}
+}
+
+// echo(arg:"hello")
+func echoFn(args ...object.Object) object.Object {
+	if len(args) == 0 {
+		// allow echo() without crashing
+		fmt.Println("")
+		return NULL
+	}
+	var arguments []interface{} = make([]interface{}, len(args)-1)
+	for i, d := range args {
+		if i > 0 {
+			arguments[i-1] = d.Inspect()
+		}
+	}
+
+	fmt.Printf(args[0].Inspect(), arguments...)
+	fmt.Println("")
+
+	return NULL
+}
+
+// int(string:"123")
+func intFn(args ...object.Object) object.Object {
+	err := validateArgs("int", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	switch arg := args[0].(type) {
+	case *object.Number:
+		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(tok, "int(...) can only be called on strings which represent numbers, '%s' given", arg.Value)
+		}
+
+		return &object.Number{Token: tok, Value: float64(int64(i))}
+	default:
+		// we will never reach here
+		return newError(tok, "argument to `int` not supported, got %s", args[0].Type())
+	}
+}
+
+// number(string:"1.23456")
+func numberFn(args ...object.Object) object.Object {
+	err := validateArgs("number", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	switch arg := args[0].(type) {
+	case *object.Number:
+		return arg
+	case *object.String:
+		i, err := strconv.ParseFloat(arg.Value, 64)
+
+		if err != nil {
+			return newError(tok, "number(...) can only be called on strings which represent numbers, '%s' given", arg.Value)
+		}
+
+		return &object.Number{Token: tok, Value: i}
+	default:
+		// we will never reach here
+		return newError(tok, "argument to `number` not supported, got %s", args[0].Type())
+	}
+}
+
+// is_number(string:"1.23456")
+func isNumberFn(args ...object.Object) object.Object {
+	err := validateArgs("number", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	switch arg := args[0].(type) {
+	case *object.Number:
+		return &object.Boolean{Token: tok, Value: true}
+	case *object.String:
+		return &object.Boolean{Token: tok, Value: util.IsNumber(arg.Value)}
+	default:
+		// we will never reach here
+		return newError(tok, "argument to `is_number` not supported, got %s", args[0].Type())
+	}
+}
+
+// stdin() -- implemented with 2 functions
+func stdinFn(args ...object.Object) object.Object {
+	v := scanner.Scan()
+
+	if !v {
+		return EOF
+	}
+
+	return &object.String{Token: tok, Value: scanner.Text()}
+}
+func stdinNextFn(pos int) (int, object.Object) {
+	v := scanner.Scan()
+
+	if !v {
+		return pos, EOF
+	}
+
+	return pos, &object.String{Token: tok, Value: scanner.Text()}
+}
+
+// env(variable:"PWD")
+func envFn(args ...object.Object) object.Object {
+	err := validateArgs("env", args, 1, [][]string{{object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	arg := args[0].(*object.String)
+	return &object.String{Token: tok, Value: os.Getenv(arg.Value)}
+}
+
+// arg(position:1)
+func argFn(args ...object.Object) object.Object {
+	err := validateArgs("arg", args, 1, [][]string{{object.NUMBER_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	arg := args[0].(*object.Number)
+	i := arg.Int()
+
+	if int(i) > len(os.Args)-1 {
+		return &object.String{Token: tok, Value: ""}
+	}
+
+	return &object.String{Token: tok, Value: os.Args[i]}
+}
+
+// type(variable:"hello")
+func typeFn(args ...object.Object) object.Object {
+	err := validateArgs("type", args, 1, [][]string{})
+	if err != nil {
+		return err
+	}
+
+	return &object.String{Token: tok, Value: string(args[0].Type())}
+}
+
+// split(string:"hello")
+func splitFn(args ...object.Object) object.Object {
+	err := validateArgs("split", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	s := args[0].(*object.String)
+	sep := args[1].(*object.String)
+
+	parts := strings.Split(s.Value, sep.Value)
+	length := len(parts)
+	elements := make([]object.Object, length, length)
+
+	for k, v := range parts {
+		elements[k] = &object.String{Token: tok, Value: v}
+	}
+
+	return &object.Array{Elements: elements}
+}
+
+// lines(string:"a\nb")
+func linesFn(args ...object.Object) object.Object {
+	err := validateArgs("lines", args, 1, [][]string{{object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	s := args[0].(*object.String)
+	parts := strings.FieldsFunc(s.Value, func(r rune) bool {
+		return r == '\n' || r == '\r' || r == '\f'
+	})
+	length := len(parts)
+	elements := make([]object.Object, length, length)
+
+	for k, v := range parts {
+		elements[k] = &object.String{Token: tok, Value: v}
+	}
+
+	return &object.Array{Elements: elements}
+}
+
+// "{}".json()
+// Converts a valid JSON document to an ABS hash.
+func jsonFn(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
+	// allow to do things like:
+	//
+	// x = 10
+	// '{"key": x}'.json()["key"] // 10
+	//
+	// 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}})
+	if err != nil {
+		return err
+	}
+
+	s := args[0].(*object.String)
+	str := strings.TrimSpace(s.Value)
+	env := object.NewEnvironment()
+	l := lexer.New(str)
+	p := parser.New(l)
+	var node ast.Node
+	ok := false
+
+	// JSON types:
+	// - objects
+	// - arrays
+	// - number
+	// - string
+	// - null
+	// - bool
+	switch str[0] {
+	case '{':
+		node, ok = p.ParseHashLiteral().(*ast.HashLiteral)
+	case '[':
+		node, ok = p.ParseArrayLiteral().(*ast.ArrayLiteral)
+	}
+
+	if str[0] == '"' && str[len(str)-1] == '"' {
+		node, ok = p.ParseStringLiteral().(*ast.StringLiteral)
+	}
+
+	if util.IsNumber(str) {
+		node, ok = p.ParseNumberLiteral().(*ast.NumberLiteral)
+	}
+
+	if str == "false" || str == "true" {
+		node, ok = p.ParseBoolean().(*ast.Boolean)
+	}
+
+	if str == "null" {
+		return NULL
+	}
+
+	if ok {
+		return Eval(node, env)
+	}
+
+	return newError(tok, "argument to `json` must be a valid JSON object, got '%s'", s.Value)
+
+}
+
+// "a %s".fmt(b)
+func fmtFn(args ...object.Object) object.Object {
+	list := []interface{}{}
+
+	for _, s := range args[1:] {
+		list = append(list, s.Inspect())
+	}
+
+	return &object.String{Token: tok, Value: fmt.Sprintf(args[0].(*object.String).Value, list...)}
+}
+
+// sum(array:[1, 2, 3])
+func sumFn(args ...object.Object) object.Object {
+	err := validateArgs("sum", args, 1, [][]string{{object.ARRAY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	arr := args[0].(*object.Array)
+	if arr.Empty() {
+		return &object.Number{Token: tok, Value: float64(0)}
+	}
+
+	if !arr.Homogeneous() {
+		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(tok, "sum(...) can only be called on arrays of numbers, got %s", arr.Inspect())
+	}
+
+	var sum float64 = 0
+
+	for _, v := range arr.Elements {
+		elem := v.(*object.Number)
+		sum += elem.Value
+	}
+
+	return &object.Number{Token: tok, Value: sum}
+}
+
+// sort(array:[1, 2, 3])
+func sortFn(args ...object.Object) object.Object {
+	err := validateArgs("sort", args, 1, [][]string{{object.ARRAY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	arr := args[0].(*object.Array)
+	elements := arr.Elements
+
+	if len(elements) == 0 {
+		return arr
+	}
+
+	if !arr.Homogeneous() {
+		return newError(tok, "argument to `sort` must be an homogeneous array (elements of the same type), got %s", arr.Inspect())
+	}
+
+	switch elements[0].(type) {
+	case *object.Number:
+		a := []float64{}
+		for _, v := range elements {
+			a = append(a, v.(*object.Number).Value)
+		}
+		sort.Float64s(a)
+
+		o := []object.Object{}
+
+		for _, v := range a {
+			o = append(o, &object.Number{Token: tok, Value: v})
+		}
+		return &object.Array{Elements: o}
+	case *object.String:
+		a := []string{}
+		for _, v := range elements {
+			a = append(a, v.(*object.String).Value)
+		}
+		sort.Strings(a)
+
+		o := []object.Object{}
+
+		for _, v := range a {
+			o = append(o, &object.String{Token: tok, Value: v})
+		}
+		return &object.Array{Elements: o}
+	default:
+		return newError(tok, "cannot sort an array with given elements elements (%s)", arr.Inspect())
+	}
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+
+	arr := args[0].(*object.Array)
+	length := len(arr.Elements)
+	newElements := make([]object.Object, length, length)
+	copy(newElements, arr.Elements)
+
+	for k, v := range arr.Elements {
+		evaluated := applyFunction(tok, args[1], []object.Object{v})
+
+		if isError(evaluated) {
+			return evaluated
+		}
+		newElements[k] = evaluated
+	}
+
+	return &object.Array{Elements: newElements}
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+
+	var result bool
+
+	arr := args[0].(*object.Array)
+
+	for _, v := range arr.Elements {
+		r := applyFunction(tok, args[1], []object.Object{v})
+
+		if isTruthy(r) {
+			result = true
+			break
+		}
+	}
+
+	return &object.Boolean{Token: tok, Value: result}
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+
+	result := true
+
+	arr := args[0].(*object.Array)
+
+	for _, v := range arr.Elements {
+		r := applyFunction(tok, args[1], []object.Object{v})
+
+		if !isTruthy(r) {
+			result = false
+		}
+	}
+
+	return &object.Boolean{Token: tok, Value: result}
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+
+	arr := args[0].(*object.Array)
+
+	for _, v := range arr.Elements {
+		r := applyFunction(tok, args[1], []object.Object{v})
+
+		if isTruthy(r) {
+			return v
+		}
+	}
+
+	return NULL
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+
+	result := []object.Object{}
+	arr := args[0].(*object.Array)
+
+	for _, v := range arr.Elements {
+		evaluated := applyFunction(tok, args[1], []object.Object{v})
+
+		if isError(evaluated) {
+			return evaluated
+		}
+
+		if isTruthy(evaluated) {
+			result = append(result, v)
+		}
+	}
+
+	return &object.Array{Elements: result}
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+
+	switch arg := args[0].(type) {
+	case *object.String:
+		needle, ok := args[1].(*object.String)
+
+		if ok {
+			return &object.Boolean{Token: tok, Value: strings.Contains(arg.Value, needle.Value)}
+		}
+	case *object.Array:
+		var found bool
+
+		switch needle := args[1].(type) {
+		case *object.String:
+			for _, v := range arg.Elements {
+				if v.Inspect() == needle.Value && v.Type() == object.STRING_OBJ {
+					found = true
+					break // Let's get outta here!
+				}
+			}
+
+			return &object.Boolean{Token: tok, Value: found}
+		case *object.Number:
+			for _, v := range arg.Elements {
+				// Quite ghetto but also the easiest way out
+				// Instead of doing type checking on the argument,
+				// we received back its string representation.
+				// If they match, we then check that its type was
+				// integer.
+				if v.Inspect() == strconv.Itoa(int(needle.Value)) && v.Type() == object.NUMBER_OBJ {
+					found = true
+					break // Let's get outta here!
+				}
+			}
+
+			return &object.Boolean{Token: tok, Value: found}
+		}
+	}
+
+	return &object.Boolean{Token: tok, Value: false}
+}
+
+// str(1)
+func strFn(args ...object.Object) object.Object {
+	err := validateArgs("str", args, 1, [][]string{})
+	if err != nil {
+		return err
+	}
+
+	return &object.String{Token: tok, Value: args[0].Inspect()}
+}
+
+// any("abc", "b")
+func anyFn(args ...object.Object) object.Object {
+	err := validateArgs("any", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return &object.Boolean{Token: tok, Value: strings.ContainsAny(args[0].(*object.String).Value, args[1].(*object.String).Value)}
+}
+
+// prefix("abc", "a")
+func prefixFn(args ...object.Object) object.Object {
+	err := validateArgs("prefix", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return &object.Boolean{Token: tok, Value: strings.HasPrefix(args[0].(*object.String).Value, args[1].(*object.String).Value)}
+}
+
+// suffix("abc", "a")
+func suffixFn(args ...object.Object) object.Object {
+	err := validateArgs("suffix", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return &object.Boolean{Token: tok, Value: strings.HasSuffix(args[0].(*object.String).Value, args[1].(*object.String).Value)}
+}
+
+// repeat("abc", 3)
+func repeatFn(args ...object.Object) object.Object {
+	err := validateArgs("repeat", args, 2, [][]string{{object.STRING_OBJ}, {object.NUMBER_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return &object.String{Token: tok, Value: strings.Repeat(args[0].(*object.String).Value, int(args[1].(*object.Number).Value))}
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+
+	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")
+func titleFn(args ...object.Object) object.Object {
+	err := validateArgs("title", args, 1, [][]string{{object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return &object.String{Token: tok, Value: strings.Title(args[0].(*object.String).Value)}
+}
+
+// lower("ABC")
+func lowerFn(args ...object.Object) object.Object {
+	err := validateArgs("lower", args, 1, [][]string{{object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return &object.String{Token: tok, Value: strings.ToLower(args[0].(*object.String).Value)}
+}
+
+// upper("abc")
+func upperFn(args ...object.Object) object.Object {
+	err := validateArgs("upper", args, 1, [][]string{{object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return &object.String{Token: tok, Value: strings.ToUpper(args[0].(*object.String).Value)}
+}
+
+// trim("abc")
+func trimFn(args ...object.Object) object.Object {
+	err := validateArgs("trim", args, 1, [][]string{{object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return &object.String{Token: tok, Value: strings.Trim(args[0].(*object.String).Value, " ")}
+}
+
+// trim_by("abc", "c")
+func trimByFn(args ...object.Object) object.Object {
+	err := validateArgs("trim_by", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	return &object.String{Token: tok, Value: strings.Trim(args[0].(*object.String).Value, args[1].(*object.String).Value)}
+}
+
+// index("abc", "c")
+func indexFn(args ...object.Object) object.Object {
+	err := validateArgs("index", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	i := strings.Index(args[0].(*object.String).Value, args[1].(*object.String).Value)
+
+	if i == -1 {
+		return NULL
+	}
+
+	return &object.Number{Token: tok, Value: float64(i)}
+}
+
+// last_index("abcc", "c")
+func lastIndexFn(args ...object.Object) object.Object {
+	err := validateArgs("last_index", args, 2, [][]string{{object.STRING_OBJ}, {object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	i := strings.LastIndex(args[0].(*object.String).Value, args[1].(*object.String).Value)
+
+	if i == -1 {
+		return NULL
+	}
+
+	return &object.Number{Token: tok, Value: float64(i)}
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+
+	start := int(args[1].(*object.Number).Value)
+	end := int(args[2].(*object.Number).Value)
+
+	switch arg := args[0].(type) {
+	case *object.String:
+		s := arg.Value
+		start, end := sliceStartAndEnd(len(s), start, end)
+
+		return &object.String{Token: tok, Value: s[start:end]}
+	case *object.Array:
+		start, end := sliceStartAndEnd(len(arg.Elements), start, end)
+
+		return &object.Array{Elements: arg.Elements[start:end]}
+	}
+
+	return NULL
+}
+
+// Clamps start and end arguments to the slice
+// function. When you slice "abc" you can have
+// start 10 and end -20...
+func sliceStartAndEnd(l int, start int, end int) (int, int) {
+	if end == 0 {
+		end = l
+	}
+
+	if start > l {
+		start = l
+	}
+
+	if start < 0 {
+		newStart := l + start
+		if newStart < 0 {
+			start = 0
+		} else {
+			start = newStart
+		}
+	}
+
+	if end > l || start > end {
+		end = l
+	}
+
+	return start, end
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+
+	array := args[0].(*object.Array)
+	if len(array.Elements) == 0 {
+		return NULL
+	}
+	e := array.Elements[0]
+	array.Elements = append(array.Elements[:0], array.Elements[1:]...)
+
+	return e
+}
+
+// reverse([1,2,3])
+func reverseFn(args ...object.Object) object.Object {
+	err := validateArgs("reverse", args, 1, [][]string{{object.ARRAY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	array := args[0].(*object.Array)
+
+	for i, j := 0, len(array.Elements)-1; i < j; i, j = i+1, j-1 {
+		array.Elements[i], array.Elements[j] = array.Elements[j], array.Elements[i]
+	}
+
+	return array
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+
+	array := args[0].(*object.Array)
+	array.Elements = append(array.Elements, args[1])
+
+	return array
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+	if len(args) < 1 {
+		return NULL
+	}
+	switch arg := args[0].(type) {
+	case *object.Array:
+		if len(arg.Elements) > 0 {
+			elem := arg.Elements[len(arg.Elements)-1]
+			arg.Elements = arg.Elements[0 : len(arg.Elements)-1]
+			return elem
+		}
+	case *object.Hash:
+		if len(args) == 2 {
+			key := args[1].(object.Hashable)
+			hashKey := key.HashKey()
+			item, ok := arg.Pairs[hashKey]
+			if ok {
+				pairs := make(map[object.HashKey]object.HashPair)
+				pairs[hashKey] = item
+				delete(arg.Pairs, hashKey)
+				return &object.Hash{Pairs: pairs}
+			}
+		}
+	}
+	return NULL
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+	switch arg := args[0].(type) {
+	case *object.Array:
+		length := len(arg.Elements)
+		newElements := make([]object.Object, length, length)
+		for k := range arg.Elements {
+			newElements[k] = &object.Number{Token: tok, Value: float64(k)}
+		}
+		return &object.Array{Elements: newElements}
+	case *object.Hash:
+		pairs := arg.Pairs
+		keys := []object.Object{}
+		for _, pair := range pairs {
+			key := pair.Key
+			keys = append(keys, key)
+		}
+		return &object.Array{Elements: keys}
+	}
+	return NULL
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+	hash := args[0].(*object.Hash)
+	pairs := hash.Pairs
+	values := []object.Object{}
+	for _, pair := range pairs {
+		value := pair.Value
+		values = append(values, value)
+	}
+	return &object.Array{Elements: values}
+}
+
+// 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}})
+	if err != nil {
+		return err
+	}
+	hash := args[0].(*object.Hash)
+	pairs := hash.Pairs
+	items := []object.Object{}
+	for _, pair := range pairs {
+		key := pair.Key
+		value := pair.Value
+		item := &object.Array{Elements: []object.Object{key, value}}
+		items = append(items, item)
+	}
+	return &object.Array{Elements: items}
+}
+
+func joinFn(args ...object.Object) object.Object {
+	err := validateArgs("join", args, 2, [][]string{{object.ARRAY_OBJ}, {object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	arr := args[0].(*object.Array)
+	length := len(arr.Elements)
+	newElements := make([]string, length, length)
+
+	for k, v := range arr.Elements {
+		newElements[k] = v.Inspect()
+	}
+
+	return &object.String{Token: tok, Value: strings.Join(newElements, args[1].(*object.String).Value)}
 }


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index fc15be9..b8d1f50 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -968,6 +968,10 @@ func TestInExpressions(t *testing.T) {
 		{`"1" in [1]`, false},
 		{`1 in [1, 2]`, true},
 		{`"hello" in [1, 2]`, false},
+		{`"str" in "string"`, true},
+		{`"xyz" in "string"`, false},
+		{`"abc" in ["abc", "def"]`, true},
+		{`"xyz" in ["abc", "def"]`, false},
 	}
 
 	for _, tt := range tests {
@@ -1413,3 +1417,30 @@ func TestEvalAssignIndex(t *testing.T) {
 		testStringObject(t, evaluated, tt.expected)
 	}
 }
+
+func TestHashFunctions(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected string
+	}{
+		{`
+		h = {"a": 1, "b": 2, "c": {"x": 10, "y":20}}
+		hk = h.keys()
+		hk = keys(h)
+		hv = h.values()
+		hv = values(h)
+		hi = h.items()
+		hi = items(h)
+		hp = h.pop("a")
+		hp = pop(h, "c")
+		hp = h.pop("d")
+		str(h)
+		`, "{b: 2}",
+		},
+	}
+
+	for _, tt := range tests {
+		evaluated := testEval(tt.input)
+		testStringObject(t, evaluated, tt.expected)
+	}
+}
diff --git a/tests/test-hash-funcs.abs b/tests/test-hash-funcs.abs
new file mode 100644
index 0000000..6303331
--- /dev/null
+++ b/tests/test-hash-funcs.abs
@@ -0,0 +1,59 @@
+echo("=====================")
+echo('Test hash functions')
+echo('>>> h = {"a": 1, "b": 2, "c": {"x": 10, "y":20}}')
+h = {"a": 1, "b": 2, "c": {"x": 10, "y":20}}
+echo(h)
+
+echo('>>> hk = h.keys()')
+hk = h.keys()
+echo(hk)
+echo('>>> hk = keys(h)')
+hk = keys(h)
+echo(hk)
+
+echo('>>> hv = h.values()')
+hv = h.values()
+echo(hv)
+echo('>>> hv = values(h)')
+hv = values(h)
+echo(hv)
+
+echo('>>> hi = h.items()')
+hi = h.items()
+echo(hi)
+echo('>>> hi = items(h)')
+hi = items(h)
+echo(hi)
+
+echo('>>> hp = h.pop("a")')
+hp = h.pop("a")
+echo(hp)
+echo('>>> h')
+echo(h)
+echo('>>> hp = pop(h, "c")')
+hp = pop(h, "c")
+echo(hp)
+echo('>>> h')
+echo(h)
+echo('>>> hp = h.pop("d")')
+hp = h.pop("d")
+echo(hp)
+echo('>>> h')
+echo(h)
+
+
+
+
+# Error tests
+
+# echo("=====================")
+# echo('Error: assign to non-hash property')
+# echo('s = "string"')
+# s = "string"
+# echo('s.ok = true')
+# s.ok = true
+# echo(s.ok)
+
+
+
+