Repository: abs-lang/abs
Pull request #146 (2019-01-31 05:50:24)

v1.1.0


Patch:
diff --git a/.travis.yml b/.travis.yml
index 56cafbd..32efb17 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,9 +12,12 @@ before_script:
 - go get github.com/c-bata/go-prompt
 - go get -v github.com/mattn/go-colorable
 - go get -v github.com/mattn/go-tty
+- go get github.com/mattn/goveralls
 - go get -d -v ./...
 
 env:
   - CONTEXT=abs
 
-script: go test ./... -vet=off
\ No newline at end of file
+script: 
+ - go test ./... -vet=off -v -covermode=count -coverprofile=coverage.out
+ - $GOPATH/bin/goveralls -service=travis-ci -coverprofile=coverage.out
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..cbaaef4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,88 @@
+## Contributing to the ABS language
+
+## A little explnatexplanationion first...
+
+ABS uses version branches to keep track of changes, meaning
+that you will see branches such as [1.0.x](https://github.com/abs-lang/abs/tree/1.0.x) and [1.1.x](https://github.com/abs-lang/abs/tree/1.1.x) in the
+repository.
+
+Since we follow [semver](https://semver.org/),
+when a new feature is released we don't backport it but simply
+create a new version branch, such as `1.2.x`. Bugs, instead,
+might be backported from `1.1.0` to, for example, `1.0.x` and we
+will have a new [release](https://github.com/abs-lang/abs/releases),
+say `1.0.1` for the `1.0.x` version branch.
+
+The infamous `master` branch, instead, is used as a base for
+the documentation and latest releases: when we publish a new release,
+the version branch gets also merged into `master`, making sure
+`master` now has all the latest changes we've released to the
+public. This is important since [abs-lang.org](https://www.abs-lang.org/)
+gets built whenever new commits land on master, so it would
+be problematic to merge PRs directly into master -- the website
+and documentation would get instantly updated while the changes 
+we merged haven't been released yet.
+
+Why is this important? When you want to contribute to ABS you
+should try to figure out against what branch you want changes
+to be incorporated. You can open a PR directly to `master`, but
+it's likely a member of our team will then change your PR's
+base branch to the right version branch. If you do that beforehand,
+that'll help us getting things reviewed, and merged, faster :)
+
+## Hacking on ABS
+
+The best way to start hacking on ABS is to clone the repository
+and run a `make build`: this will build a docker container with all
+the necessary dependencies for developing locally (for experienced
+Gophers: you might want to skip this altogether as your environment
+will probably work perfectly).
+
+With `make run` you can get inside a container built for ABS'
+development, and `make test` will run all tests.
+
+We're planning to switch over to `go mod` in the near future to make
+it easier to contribute to ABS and will keep updating this page
+with additional directions on how to develop on ABS locally.
+
+## Pull Requests status checks
+
+When you send a PR, it will be automatically tested through
+[travis-ci](https://travis-ci.com/abs-lang/abs): remember
+we strive to support `linux`, `osx` and `windows`, so sometimes
+changes that work locally might trigger failures on travis --
+don't be afraid of a red build, it happens to everyone! Once
+you find the culprit, push another change to your PR and, once
+tests are green, we're ready to merge!
+
+In order to get something merged, this is the broad set
+of conventions we follow:
+
+* PR from external contributors must be reviewed and approved by a member of the ABS team
+* builds for the PR must be green on travis
+
+...and that's it, we're not very formal!
+
+## Issues? Roadmap?
+
+You can have a look at the list of [open issues on GitHub](https://github.com/abs-lang/abs/issues)
+in order to get an idea what we'd like some help on: at the same
+time, if you want to propose a change to the language itself, feel
+free to open an issue and we'd be delighted to have a discussion
+around your ideas!
+
+We also plan [milestones](https://github.com/abs-lang/abs/milestones)
+ahead of time so you can get an idea of what's going to land in each
+minor / major release by looking at the GitHub milestones. We try
+to release early and release often, meaning we're constantly reviewing
+our plan and might need to delay releasing a particular feature as
+it would hold back the release of other interesting features.
+
+A piece of advice: if you `+1` the issues you care about, there's a
+higher change we'll have a look at them in our next release ;-)
+
+We also encourage you to start discussions around the language
+or submit RFCs: ABS is nothing without a community that shapes
+its direction. [This](https://github.com/abs-lang/abs/issues/124)
+is an example of the community proposing a change and the ABS
+team happily `+1` it.
\ No newline at end of file
diff --git a/README.md b/README.md
index 20bd1c8..3b09781 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,18 @@
   <a href="https://travis-ci.com/abs-lang/abs"><img alt="Travis Status" src="https://travis-ci.com/abs-lang/abs.svg?branch=master"></a>
   <a href="https://github.com/abs-lang/abs"><img alt="License" src="https://img.shields.io/github/license/abs-lang/abs.svg"></a>
   <a href="https://github.com/abs-lang/abs"><img alt="Version" src="https://img.shields.io/github/release-pre/abs-lang/abs.svg"></a>
+  <img alt="undefined" src="https://img.shields.io/github/release-date/abs-lang/abs.svg?style=flat">
+  <img alt="undefined" src="https://img.shields.io/github/downloads/abs-lang/abs/total.svg?style=flat">
+  <br />
+  <img alt="undefined" src="https://img.shields.io/badge/platform-linux | macosx | windows-red.svg">
+  <img alt="undefined"  src="https://img.shields.io/github/last-commit/abs-lang/abs.svg?style=flat">
+  <a href='https://coveralls.io/github/abs-lang/abs'><img src='https://coveralls.io/repos/github/abs-lang/abs/badge.svg' alt='Coverage Status' /></a>
+  <br />
+  <img alt="undefined" src="https://img.shields.io/github/contributors/abs-lang/abs.svg?style=flat">
+  <img alt="undefined" src="https://img.shields.io/github/issues/abs-lang/abs.svg?style=flat">
+  <img alt="undefined" src="https://img.shields.io/github/issues-pr-closed/abs-lang/abs.svg?style=flat">
+  <img alt="undefined" src="https://img.shields.io/github/stars/abs-lang/abs.svg?style=social">
+
 </p>
 
 # The ABS programming language
diff --git a/ast/ast.go b/ast/ast.go
index 862f3ba..99de537 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -51,10 +51,12 @@ func (p *Program) String() string {
 
 // Statements
 type AssignStatement struct {
-	Token token.Token // the token.ASSIGN token
-	Name  *Identifier
-	Names []Expression
-	Value Expression
+	Token    token.Token // the token.ASSIGN token
+	Name     *Identifier
+	Names    []Expression
+	Index    *IndexExpression    // support assignment to indexed expressions: a[0] = 1, h["a"] = 1
+	Property *PropertyExpression // support assignment to hash properties: h.a = 1
+	Value    Expression
 }
 
 func (as *AssignStatement) statementNode()       {}
@@ -64,6 +66,16 @@ func (as *AssignStatement) String() string {
 
 	if as.Name != nil {
 		out.WriteString(as.Name.String())
+	} else if len(as.Names) > 0 {
+		out.WriteString(as.Names[0].String())
+		for i := 1; i < len(as.Names); i++ {
+			out.WriteString(", ")
+			out.WriteString(as.Names[i].String())
+		}
+	} else if as.Index != nil {
+		out.WriteString(as.Index.String())
+	} else if as.Property != nil {
+		out.WriteString(as.Property.String())
 	}
 
 	out.WriteString(" = ")
diff --git a/docs/README.md b/docs/README.md
index 73a18fb..83eab24 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -8,6 +8,17 @@
   <a href="https://travis-ci.com/abs-lang/abs"><img alt="Travis Status" src="https://travis-ci.com/abs-lang/abs.svg?branch=master"></a>
   <a href="https://github.com/abs-lang/abs"><img alt="License" src="https://img.shields.io/github/license/abs-lang/abs.svg"></a>
   <a href="https://github.com/abs-lang/abs"><img alt="Version" src="https://img.shields.io/github/release-pre/abs-lang/abs.svg"></a>
+  <img alt="undefined" src="https://img.shields.io/github/release-date/abs-lang/abs.svg?style=flat">
+  <img alt="undefined" src="https://img.shields.io/github/downloads/abs-lang/abs/total.svg?style=flat">
+  <br />
+  <img alt="undefined" src="https://img.shields.io/badge/platform-linux | macosx | windows-red.svg">
+  <img alt="undefined"  src="https://img.shields.io/github/last-commit/abs-lang/abs.svg?style=flat">
+  <a href='https://coveralls.io/github/abs-lang/abs'><img src='https://coveralls.io/repos/github/abs-lang/abs/badge.svg' alt='Coverage Status' /></a>
+  <br />
+  <img alt="undefined" src="https://img.shields.io/github/contributors/abs-lang/abs.svg?style=flat">
+  <img alt="undefined" src="https://img.shields.io/github/issues/abs-lang/abs.svg?style=flat">
+  <img alt="undefined" src="https://img.shields.io/github/issues-pr-closed/abs-lang/abs.svg?style=flat">
+  <img alt="undefined" src="https://img.shields.io/github/stars/abs-lang/abs.svg?style=social">
 </p>
 
 ABS is a scripting language that works best when you're scripting on
diff --git a/docs/installer.sh b/docs/installer.sh
index 7e7e807..cb9f134 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.0.2
+VERSION=1.1.0
 
 echo "Trying to detect the details of your architecture."
 echo ""
diff --git a/docs/misc/technical-details.md b/docs/misc/technical-details.md
index 2a41468..218259e 100644
--- a/docs/misc/technical-details.md
+++ b/docs/misc/technical-details.md
@@ -21,19 +21,14 @@ interpreter without relying on many external dependencies
 
 ## Development & contributing
 
-The best way to start developing *for* ABS is to clone the repository
-and run a `make build`: this will build a docker container with all
-the necessary dependencies for developing locally (for experienced
-Gophers: you might want to skip this altogether as your environment
-will probably work perfectly).
-
-With `make run` you can get inside a container built for ABS'
-development, and `make test` will run all tests.
+Please see [github.com/abs-lang/abs/blob/master/CONTRIBUTING.md](https://github.com/abs-lang/abs/blob/master/CONTRIBUTING.md)
 
 ## Testing
+
 ### Interpreter Error Location
+
 You can run the interpreter error location tests by invoking this bash script: `tests/test-abs.sh`. This script iterates over the `test-parser.abs` and `test-eval.abs` test scripts.
-```
+```bash
 $ tests/test-abs.sh
 =======================================
 Test Parser
@@ -94,9 +89,12 @@ ERROR: index operator not supported: f(x) {x} on HASH
 	[19:20]	    {"name": "Abs"}[f(x) {x}];  
 Exit code: 99
 ```
+
 ### String tests
-String handling tests can be run from `abs tests/test-strings.abs`
-```
+
+String handling tests can be run from `abs tests/test-strings.abs`:
+
+```bash
 $ abs tests/test-strings.abs
 =====================
 >>> Testing string with expanded LFs:
@@ -144,9 +142,70 @@ echo(ss)
 a\nb\nc
 ```
 
+### Array and hash assignment tests 
+
+Array and hash assignment tests can be run from `abs tests/test-assign-index.abs `:
+
+```bash
+=====================
+Test assignment to array indexed objects
+>>> a = [1, 2, 3, 4]
+[1, 2, 3, 4]
+>>> a[0] = 99
+[99, 2, 3, 4]
+>>> a[1] += 10
+[99, 12, 3, 4]
+>>> a += [88]
+[99, 12, 3, 4, 88]
+>>> a[2] = "string"
+[99, 12, string, 4, 88]
+>>> a[6] = 66
+[99, 12, string, 4, 88, null, 66]
+>>> a[5] = 55
+[99, 12, string, 4, 88, 55, 66]
+=====================
+Test assignment to hash indexed objects
+>>> h = {"a": 1, "b": 2, "c": 3}
+{a: 1, b: 2, c: 3}
+>>> h["a"] = 99
+{a: 99, b: 2, c: 3}
+>>> h["a"] += 1
+{a: 100, b: 2, c: 3}
+>>> h += {"c": 33, "d": 44, "e": 55}
+{a: 100, b: 2, c: 33, d: 44, e: 55}
+h["z"] = {"x": 10, "y": 20}
+{a: 100, b: 2, c: 33, d: 44, e: 55, z: {x: 10, y: 20}}
+h["1.23"] = "string"
+{1.23: string, a: 100, b: 2, c: 33, d: 44, e: 55, z: {x: 10, y: 20}}
+h.d = 99
+{1.23: string, a: 100, b: 2, c: 33, d: 99, e: 55, z: {x: 10, y: 20}}
+h.d += 1
+{1.23: string, a: 100, b: 2, c: 33, d: 100, e: 55, z: {x: 10, y: 20}}
+h.z.x = 66
+{1.23: string, a: 100, b: 2, c: 33, d: 100, e: 55, z: {x: 66, y: 20}}
+h.f = 88
+{1.23: string, a: 100, b: 2, c: 33, d: 100, e: 55, f: 88, z: {x: 66, y: 20}}
+=====================
+Error: assign to non-hash property
+s = "string"
+s.ok = true
+ERROR: can only assign to hash property, got STRING
+	[66:2]	s.ok = true
+=====================
+Error: add number to null hash property
+h.g += 1
+ERROR: type mismatch: NULL + NUMBER
+	[72:5]	h.g += 1
+=====================
+Error: add number to null hash element
+>>> h["g"] += 1
+ERROR: type mismatch: NULL + NUMBER
+	[78:8]	h["g"] += 1
+```
+
 ## Roadmap
 
-We're currently working on [1.1](https://github.com/abs-lang/abs/milestone/6).
+We're currently working on [1.2](https://github.com/abs-lang/abs/milestone/9).
 
 ## Next
 
diff --git a/docs/syntax/assignments.md b/docs/syntax/assignments.md
index 7989445..270d423 100644
--- a/docs/syntax/assignments.md
+++ b/docs/syntax/assignments.md
@@ -1,14 +1,12 @@
 # Assignments
 
-Just like about any other language, assignments are pretty
-straightforward:
+Just like about any other language, assignments are pretty straightforward:
 
 ``` bash
 x = "hello world"
 ```
 
-Array destructuring is supported, meaning you can
-set multiple variables based on an array:
+Array destructuring is supported, meaning you can set multiple variables based on an array:
 
 ``` bash
 x, y, z = ["hello world", 99, {}]
@@ -17,15 +15,58 @@ y # 99
 z # {}
 ```
 
-If the number of variables you're trying to set is longer
-than the array, the extra variables will be set to
-null:
+If the number of variables you're trying to set is longer than the array, the extra variables will be set to null:
 
 ``` bash
 x, y = [1]
 y # null
 ```
 
+An individual array element may be assigned to via its `array[index]`. This includes compound operators such as `+=`. Also an array can be extended by assigning to an index beyond its current length.
+```bash
+a = [1, 2, 3, 4]
+a # [1, 2, 3, 4]
+
+# index assignment
+a[0] = 99
+a # [99, 2, 3, 4]
+
+# compound assignment
+a[0] += 1
+a # [100, 2, 3, 4]
+
+# extending an array; note intervening nulls are created if needed
+a[5] = 55
+a # [100, 2, 3, 4, null, 55]
+a[4] = 44
+a # [100, 2, 3, 4, 44, 55]
+```
+
+An individual hash element may be assigned to via its `hash["key"]` index or its property `hash.key`. This includes compound operators such as `+=`. Note that a new key may be created as well using `hash["newkey"]` or `hash.newkey`.
+```bash
+h = {"a": 1, "b": 2, "c": 3}
+h # {a: 1, b: 2, c: 3}
+
+# index assignment
+h["a"] = 99
+h # {a: 99, b: 2, c: 3}
+
+# property assignment
+h.a # 99
+h.a = 88
+h # {a: 88, b: 2, c: 3}
+
+# compound operator assignment to property
+h.a += 1
+h.a # 89
+h # {a: 88, b: 2, c: 3}
+
+# create new keys via index or property
+h["x"] = 10
+h.y = 20
+h # {a: 88, b: 2, c: 3, x: 10, y: 20}
+```
+
 ABS doesn't have block-specific scopes, so any new variable
 declared in a block is automatically available outside as well:
 
@@ -49,7 +90,7 @@ echo(x) # Error: x is not defined
 ```
 
 Worth to note that if a variable gets re-defined within these expressions,
-it will temporarely assume its new value, but will rollback to the original
+it will temporarily assume its new value, but will rollback to the original
 one once the expression is over:
 
 ``` bash
diff --git a/docs/syntax/for.md b/docs/syntax/for.md
index 90a50b7..b26563e 100644
--- a/docs/syntax/for.md
+++ b/docs/syntax/for.md
@@ -56,12 +56,16 @@ echo(y) # 9
 ## In form
 
 The "in" form of the `for` loops allows you to iterate over
-an array:
+an array or an hash:
 
 ``` bash
 for x in [1, 2, 3] {
     # x is 1, 2, 3
 }
+
+for x in {"a": 1, "b": 2, "c": 3} {
+    # x is 1, 2, 3
+}
 ```
 
 Both key and values are available in the loop:
@@ -71,6 +75,11 @@ for k, v in [1, 2, 3] {
     # k is 0, 1, 2
     # v is 1, 2, 3
 }
+
+for k, v in {"a": 1, "b": 2, "c": 3} {
+    # k is a, b, c
+    # v is 1, 2, 3
+}
 ```
 
 In terms of scoping, the "in" form follows the same rules
@@ -79,13 +88,13 @@ as the standard one, meaning that:
 ``` bash
 k = "hello world"
 
-for v in [1, 2, 3] {
+for k, v in [1, 2, 3] {
     # k is 0, 1, 2
     # v is 1, 2, 3
 }
 
-echo(v) # "hello world"
-echo(k) # v is not defined
+echo(k) # "hello world"
+echo(v) # v is not defined
 ```
 
 ## Next
diff --git a/docs/syntax/operators.md b/docs/syntax/operators.md
index 66843e5..9168749 100644
--- a/docs/syntax/operators.md
+++ b/docs/syntax/operators.md
@@ -100,12 +100,16 @@ a /= 2 # a is now 5
 
 ## in
 
-Checks whether a value is in an array:
+Membership test operator (find whether a needle is in the haystack):
 
 ``` 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
+"x" in {"x": 1} # true
+"y" in {"x": 1} # false
 ```
 
 ## **
@@ -248,14 +252,23 @@ Even though there is no double negation operator, using
 
 ## ~
 
-The tilde (meaning "around") is used to do a case-insensitive
-comparison between strings:
+The tilde (meaning "around") is used to do similarity comparisons
+(x "is almost" y).
+
+In case of strings, it will compare them case-insensitively
 
 ``` bash
 "hello" == "HELLO" # false
 "hello" ~ "HELLO" # true
 ```
 
+In case of integers, it will compare their integer values:
+
+``` bash
+1 == 1.5 # false
+1 ~ 1.5 # true
+```
+
 When in front of a number, it will instead be used as a
 bitwise NOT:
 
diff --git a/docs/syntax/system-commands.md b/docs/syntax/system-commands.md
index 6353a78..ce0b1c0 100644
--- a/docs/syntax/system-commands.md
+++ b/docs/syntax/system-commands.md
@@ -4,11 +4,11 @@ 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 with `$()`, which resembles 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"
+date = `date` # "Sun Apr 1 04:30:59 +01 1995"
 ```
 
 As you can see, the return value of a command is a simple
@@ -31,6 +31,12 @@ ls = $(ls -la)
 if ls.ok {
     echo("hello world")
 }
+
+# or
+
+if `ls -la`.ok {
+    echo("hello world")
+}
 ```
 
 You can also replace parts of the command with variables
@@ -50,9 +56,10 @@ $(echo $PWD) # "" since the ABS variable PWD doesn't exist
 $(echo \$PWD) # "/go/src/github.com/abs-lang/abs"
 ```
 
-Currently, commands need to be on their own line, meaning
-that you will not be able to have additional code
-on the same line. This will throw an error:
+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.
+This will throw an error:
 
 ``` bash
 $(sleep 10); echo("hello world")
diff --git a/docs/types/array.md b/docs/types/array.md
index 7a4fcfe..00fea46 100644
--- a/docs/types/array.md
+++ b/docs/types/array.md
@@ -39,6 +39,53 @@ 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]
+a # [1, 2, 3, 4]
+
+# index assignment
+a[0] = 99
+a # [99, 2, 3, 4]
+
+# compound assignment
+a[0] += 1
+a # [100, 2, 3, 4]
+```
+
+An array can also be extended by using an index beyond the end of the existing array. Note that intervening array elements will be set to `null`. This means that they can be set to another value later:
+```bash
+a = [1, 2, 3, 4]
+a # [1, 2, 3, 4]
+
+# indexes beyond end of array expand the array
+a[4] = 99
+a # [1, 2, 3, 4, 99]
+a[6] = 66
+a # [1, 2, 3, 4, 99, null, 66]
+
+# assign to a null element
+a[5] = 55
+a # [1, 2, 3, 4, 99, 55, 66]
+```
+
 An array is defined as "homogeneous" when all its elements
 are of a single type:
 
@@ -115,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`:
@@ -124,19 +171,6 @@ Returns a new array with only the elements that returned
 ["hello", 0, 1, 2].filter(f(x){type(x) == "NUMBER"}) # [0, 1, 2]
 ```
 
-### json()
-
-Parses the string as JSON, returning an [hash](/types/hash):
-
-``` bash
-"{}".json() # {}
-```
-
-Note that currently only JSON objects are supported,
-and if the objects contain floats this method will
-return an error. Support for floats is coming (see [#29](https://github.com/abs-lang/abs/issues/29))
-as well as being able to parse all valid JSON expressions (see [#54](https://github.com/abs-lang/abs/issues/54)).
-
 ### contains(e)
 
 > This function is deprecated and might be removed in future versions.
@@ -144,8 +178,8 @@ as well as being able to parse all valid JSON expressions (see [#54](https://git
 > Use the "in" operator instead: 3 in [1, 2, 3]
 
 Checks whether `e` is present in the array. `e` can only be
-a string or number and the array needs to be a heterogeneous array
-of strings or number:
+a string or number and the array needs to be a homogeneous array
+of strings or numbers:
 
 ``` bash
 [1, 2, 3].contains(3) # true
diff --git a/docs/types/builtin-function.md b/docs/types/builtin-function.md
index e475bd3..ea76e03 100644
--- a/docs/types/builtin-function.md
+++ b/docs/types/builtin-function.md
@@ -166,6 +166,14 @@ Type 'quit' when you're done, 'help' if you get lost!
 STRING
 ```
 
+### sleep(ms)
+
+Halts the process for as many `ms` you specified:
+
+``` bash
+sleep(1000) # sleeps for 1 second
+```
+
 ## Next
 
 That's about it for this section!
diff --git a/docs/types/hash.md b/docs/types/hash.md
index 7462c46..eb2a74f 100644
--- a/docs/types/hash.md
+++ b/docs/types/hash.md
@@ -1,8 +1,6 @@
 # Hash
 
-Hashes represent a list of key-value pairs
-that can conveniently be accessed with `O(1)`
-cost:
+Hashes represent a list of key-value pairs that can conveniently be accessed with `O(1)` cost:
 
 ``` bash
 h = {"key": "val"}
@@ -10,25 +8,151 @@ h.key # "val"
 h["key"] # "val"
 ```
 
-Note that the `x.y` form is the preferred one, as it's more coincise
-and mimics other programming languages.
+Note that the `hash.key` hash property form is the preferred one, as it's more coincise and mimics other programming languages.
 
-Accessing an index that does not exist returns null.
+Accessing a key that does not exist returns null.
+
+An individual hash element may be assigned to via its `hash["key"]`
+index or its property `hash.key`. This includes compound operators 
+such as `+=`. Note that a new key may be created as well using `hash["newkey"]` or `hash.newkey`:
+
+```bash
+h = {"a": 1, "b": 2, "c": 3}
+h # {a: 1, b: 2, c: 3}
+
+# index assignment
+h["a"] = 99
+h # {a: 99, b: 2, c: 3}
+
+# property assignment
+h.a # 99
+h.a = 88
+h # {a: 88, b: 2, c: 3}
+
+# compound operator assignment to property
+h.a += 1
+h.a # 89
+h # {a: 88, b: 2, c: 3}
+
+# create new keys via index or property
+h["x"] = 10
+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 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}
+
+# extending a hash by += compound operator
+h += {"c": 33, "d": 4, "e": 5}
+h   # {a: 1, b: 2, c: 33, d: 4, e: 5}
+```
+
+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}
+
+# nested hash assigned to hash.key
+h.c = {"x": 10, "y": 20}
+h # {a: 1, b: 2, c: {x: 10, y: 20}}
+
+# nested hash assigned to hash.newkey
+h.z = {"xx": 11, "yy": 21}
+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 5bcdeb7..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"`. 
 
@@ -178,19 +185,24 @@ Splits a string by newline:
 
 ### json()
 
-Parses the string as JSON, returning an [hash](/types/hash):
+Parses the string as JSON, returning a [hash](/types/hash):
 
 ``` bash
-"{}".json() # {}
+⧐  s = '{"a": 1, "b": "string", "c": true, "d": {"x": 10, "y": 20}}'
+⧐  h = s.json()
+⧐  h
+{a: 1, b: string, c: true, d: {x: 10, y: 20}}
+⧐  h.d
+{x: 10, y: 20}
 ```
 
-Note that currently only JSON objects are supported,
-and if the objects contain floats this method will
-return an error. Support for floats is coming (see [#29](https://github.com/abs-lang/abs/issues/29))
-as well as being able to parse all valid JSON expressions (see [#54](https://github.com/abs-lang/abs/issues/54)).
-
 ### 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 0087992..364094b 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -115,15 +115,35 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 		if isError(left) {
 			return left
 		}
-
 		right := Eval(node.Right, env)
 		if isError(right) {
 			return right
 		}
-
-		expr := evalInfixExpression(node.Token, node.Operator[:len(node.Operator)-1], left, right)
+		// multi-character operators like "+=" and "**=" are reduced to "+" or "**" for evalInfixExpression()
+		op := node.Operator
+		if len(op) >= 2 {
+			op = op[:len(op)-1]
+		}
+		// get the result of the infix operation
+		expr := evalInfixExpression(node.Token, op, left, right)
+		if isError(expr) {
+			return expr
+		}
+		switch nodeLeft := node.Left.(type) {
+		case *ast.Identifier:
+			env.Set(nodeLeft.String(), expr)
+			return NULL
+		case *ast.IndexExpression:
+			// support index assignment expressions: a[0] += 1, h["a"] += 1
+			return evalIndexAssignment(nodeLeft, expr, env)
+		case *ast.PropertyExpression:
+			// support assignment to hash property: h.a += 1
+			return evalPropertyAssignment(nodeLeft, expr, env)
+		}
+		// otherwise
 		env.Set(node.Left.String(), expr)
 		return NULL
+
 	case *ast.IfExpression:
 		return evalIfExpression(node, env)
 
@@ -172,6 +192,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 
 	case *ast.PropertyExpression:
 		return evalPropertyExpression(node, env)
+
 	case *ast.ArrayLiteral:
 		elements := evalExpressions(node.Elements, env)
 		if len(elements) == 1 && isError(elements[0]) {
@@ -236,6 +257,55 @@ func evalBlockStatement(
 	return result
 }
 
+// support index assignment expressions: a[0] = 1, h["a"] = 1
+func evalIndexAssignment(iex *ast.IndexExpression, expr object.Object, env *object.Environment) object.Object {
+	leftObj := Eval(iex.Left, env)
+	index := Eval(iex.Index, env)
+	if leftObj.Type() == object.ARRAY_OBJ {
+		arrayObject := leftObj.(*object.Array)
+		idx := index.(*object.Number).Int()
+		elems := arrayObject.Elements
+		if idx < 0 {
+			return newError(iex.Token, "index out of range: %d", idx)
+		}
+		if idx >= len(elems) {
+			// expand the array by appending Null objects
+			for i := len(elems); i <= idx; i++ {
+				elems = append(elems, NULL)
+			}
+			arrayObject.Elements = elems
+		}
+		elems[idx] = expr
+		return NULL
+	}
+	if leftObj.Type() == object.HASH_OBJ {
+		hashObject := leftObj.(*object.Hash)
+		key, ok := index.(object.Hashable)
+		if !ok {
+			return newError(iex.Token, "unusable as hash key: %s", index.Type())
+		}
+		hashed := key.HashKey()
+		pair := object.HashPair{Key: index, Value: expr}
+		hashObject.Pairs[hashed] = pair
+		return NULL
+	}
+	return NULL
+}
+
+// support assignment to hash property: h.a = 1
+func evalPropertyAssignment(pex *ast.PropertyExpression, expr object.Object, env *object.Environment) object.Object {
+	leftObj := Eval(pex.Object, env)
+	if leftObj.Type() == object.HASH_OBJ {
+		hashObject := leftObj.(*object.Hash)
+		prop := &object.String{Token: pex.Token, Value: pex.Property.String()}
+		hashed := prop.HashKey()
+		pair := object.HashPair{Key: prop, Value: expr}
+		hashObject.Pairs[hashed] = pair
+		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 {
 	val := Eval(as.Value, env)
 	if isError(val) {
@@ -245,25 +315,45 @@ func evalAssignment(as *ast.AssignStatement, env *object.Environment) object.Obj
 	// regular assignment x = 0
 	if as.Name != nil {
 		env.Set(as.Name.Value, val)
+		return nil
 	}
 
 	// destructuring x, y = [1, 2]
 	if len(as.Names) > 0 {
-		if val.Type() != object.ARRAY_OBJ {
-			return newError(as.Token, "wrong assignment, expected identifier or array destructuring, got %s (%s)", val.Type(), val.Inspect())
-		}
-
-		elements := val.(*object.Array).Elements
-
-		for i, name := range as.Names {
-			if i < len(elements) {
-				env.Set(name.String(), elements[i])
-				continue
+		switch v := val.(type) {
+		case *object.Array:
+			elements := v.Elements
+			for i, name := range as.Names {
+				if i < len(elements) {
+					env.Set(name.String(), elements[i])
+					continue
+				}
+
+				env.Set(name.String(), NULL)
 			}
-
-			env.Set(name.String(), NULL)
+		case *object.Hash:
+			for _, name := range as.Names {
+				x, ok := v.GetPair(name.String())
+
+				if ok {
+					env.Set(name.String(), x.Value)
+				} else {
+					env.Set(name.String(), NULL)
+				}
+			}
+		default:
+			return newError(as.Token, "wrong assignment, expected identifier or array destructuring, got %s (%s)", val.Type(), val.Inspect())
 		}
 
+		return nil
+	}
+	// support assignment to indexed expressions: a[0] = 1, h["a"] = 1
+	if as.Index != nil {
+		return evalIndexAssignment(as.Index, val, env)
+	}
+	// support assignment to hash property h.a = 1
+	if as.Property != nil {
+		return evalPropertyAssignment(as.Property, val, env)
 	}
 
 	return nil
@@ -323,7 +413,9 @@ func evalInfixExpression(
 		return evalStringInfixExpression(tok, operator, left, right)
 	case left.Type() == object.ARRAY_OBJ && right.Type() == object.ARRAY_OBJ:
 		return evalArrayInfixExpression(tok, operator, left, right)
-	case operator == "in" && right.Type() == object.ARRAY_OBJ:
+	case left.Type() == object.HASH_OBJ && right.Type() == object.HASH_OBJ:
+		return evalHashInfixExpression(tok, operator, left, right)
+	case operator == "in":
 		return evalInExpression(left, right)
 	case operator == "==":
 		return nativeBoolToBooleanObject(left == right)
@@ -388,7 +480,6 @@ func evalNumberInfixExpression(
 ) object.Object {
 	leftVal := left.(*object.Number).Value
 	rightVal := right.(*object.Number).Value
-
 	switch operator {
 	case "+":
 		return &object.Number{Token: tok, Value: leftVal + rightVal}
@@ -437,6 +528,8 @@ func evalNumberInfixExpression(
 		return &object.Number{Token: tok, Value: float64(uint64(leftVal) << uint64(rightVal))}
 	case "^":
 		return &object.Number{Token: tok, Value: float64(int64(leftVal) ^ int64(rightVal))}
+	case "~":
+		return &object.Boolean{Token: tok, Value: int64(leftVal) == int64(rightVal)}
 	// A range results in an array of integers from left to right
 	case "..":
 		a := make([]object.Object, 0)
@@ -455,6 +548,7 @@ func evalStringInfixExpression(
 	operator string,
 	left, right object.Object,
 ) object.Object {
+
 	if operator == "+" {
 		leftVal := left.(*object.String).Value
 		rightVal := right.(*object.String).Value
@@ -473,6 +567,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())
 }
 
@@ -490,32 +588,66 @@ func evalArrayInfixExpression(
 	return newError(tok, "unknown operator: %s %s %s", left.Type(), operator, right.Type())
 }
 
+func evalHashInfixExpression(
+	tok token.Token,
+	operator string,
+	left, right object.Object,
+) object.Object {
+	leftHashObject := left.(*object.Hash)
+	rightHashObject := right.(*object.Hash)
+	if operator == "+" {
+		leftVal := leftHashObject.Pairs
+		rightVal := rightHashObject.Pairs
+		for _, rightPair := range rightVal {
+			key := rightPair.Key
+			hashed := key.(object.Hashable).HashKey()
+			leftVal[hashed] = object.HashPair{Key: key, Value: rightPair.Value}
+		}
+		return &object.Hash{Token: tok, Pairs: leftVal}
+	}
+
+	return newError(tok, "unknown operator: %s %s %s", left.Type(), operator, right.Type())
+}
+
 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())
+		}
+	case *object.Hash:
+		if left.Type() == object.STRING_OBJ {
+			_, ok := rightObj.GetPair(left.(*object.String).Value)
+			found = ok
+		}
+	default:
+		return newError(tok, "'in' operator not supported on %s", right.Type())
 	}
 
 	return &object.Boolean{Token: tok, Value: found}
@@ -653,28 +785,28 @@ func evalForInExpression(
 			i.Reset()
 		}()
 
-		return loopIterable(i.Next, env, fie, 0)
+		return loopIterable(i.Next, env, fie)
 	case *object.Builtin:
 		if i.Next == nil {
 			return newError(fie.Token, "builtin function cannot be used in loop")
 		}
 
-		return loopIterable(i.Next, env, fie, 0)
+		return loopIterable(i.Next, env, fie)
 	default:
 		return newError(fie.Token, "'%s' is a %s, not an iterable, cannot be used in for loop", i.Inspect(), i.Type())
 	}
 }
 
-func loopIterable(next func(int) (int, object.Object), env *object.Environment, fie *ast.ForInExpression, pos int) object.Object {
-	k, v := next(pos)
+func loopIterable(next func() (object.Object, object.Object), env *object.Environment, fie *ast.ForInExpression) object.Object {
+	k, v := next()
 
-	if k < 0 || v == EOF {
+	if k == nil || v == EOF {
 		return NULL
 	}
 
 	// set the special k v variables in the
 	// environment
-	env.Set(fie.Key, &object.Number{Token: fie.Token, Value: float64(k)})
+	env.Set(fie.Key, k)
 	env.Set(fie.Value, v)
 	err := Eval(fie.Block, env)
 
@@ -682,8 +814,8 @@ func loopIterable(next func(int) (int, object.Object), env *object.Environment,
 		return err
 	}
 
-	if k >= 0 {
-		return loopIterable(next, env, fie, pos+1)
+	if k != nil {
+		return loopIterable(next, env, fie)
 	}
 
 	return NULL
diff --git a/evaluator/functions.go b/evaluator/functions.go
index 6c7b7ea..44730fa 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -9,6 +9,7 @@ import (
 	"sort"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/abs-lang/abs/ast"
 	"github.com/abs-lang/abs/lexer"
@@ -20,1036 +21,1269 @@ import (
 
 var scanner *bufio.Scanner
 var tok token.Token
+var scannerPosition int
 
 func init() {
 	scanner = bufio.NewScanner(os.Stdin)
 	tok = token.Token{Type: token.FUNCTION, Position: 0, Literal: "BuiltinFunction"}
 }
 
-// Utility function that validates arguments passed to builtin
-// functions.
-func validateArgs(name string, args []object.Object, size int, types [][]string) object.Object {
-	if len(args) != size {
-		return newError(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,
+		},
+		// sleep(3000)
+		"sleep": &object.Builtin{
+			Types: []string{object.NUMBER_OBJ},
+			Fn:    sleepFn,
+		},
+	}
+}
 
-				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
 }
 
-// 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
+// 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
 	}
 
-	if start > l {
-		start = l
+	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())
 	}
+}
 
-	if start < 0 {
-		newStart := l + start
-		if newStart < 0 {
-			start = 0
-		} else {
-			start = newStart
-		}
+// 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 end > l || start > end {
-		end = 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())
 	}
 
-	return start, end
+	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 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
+}
+
+// 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() (object.Object, object.Object) {
+	v := scanner.Scan()
+
+	if !v {
+		return nil, EOF
+	}
+
+	defer func() {
+		scannerPosition += 1
+	}()
+	return &object.Number{Value: float64(scannerPosition)}, &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)}
+}
+
+func sleepFn(args ...object.Object) object.Object {
+	err := validateArgs("sleep", args, 1, [][]string{{object.NUMBER_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	ms := args[0].(*object.Number)
+	time.Sleep(time.Duration(ms.Value) * time.Millisecond)
+
+	return NULL
 }
diff --git a/lexer/lexer.go b/lexer/lexer.go
index 9e0aff5..789e5c1 100644
--- a/lexer/lexer.go
+++ b/lexer/lexer.go
@@ -253,6 +253,10 @@ func (l *Lexer) NextToken() token.Token {
 		tok.Type = token.STRING
 		tok.Position = l.position
 		tok.Literal = l.readString('\'')
+	case '`':
+		tok.Type = token.COMMAND
+		tok.Position = l.position
+		tok.Literal = l.readString('`')
 	case '$':
 		if l.peekChar() == '(' {
 			tok.Type = token.COMMAND
diff --git a/main.go b/main.go
index 51f4162..8bd150b 100644
--- a/main.go
+++ b/main.go
@@ -10,7 +10,7 @@ import (
 	"github.com/abs-lang/abs/repl"
 )
 
-var VERSION = "1.0.2"
+var VERSION = "1.1.0"
 
 // The ABS interpreter
 func main() {
diff --git a/object/object.go b/object/object.go
index c721e52..cbc4faf 100644
--- a/object/object.go
+++ b/object/object.go
@@ -3,6 +3,7 @@ package object
 import (
 	"bytes"
 	"fmt"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -47,7 +48,7 @@ type Object interface {
 }
 
 type Iterable interface {
-	Next(int) (int, Object)
+	Next() (Object, Object)
 	Reset()
 }
 
@@ -163,7 +164,7 @@ func (s *String) HashKey() HashKey {
 type Builtin struct {
 	Token    token.Token
 	Fn       BuiltinFunction
-	Next     func(int) (int, Object)
+	Next     func() (Object, Object)
 	Types    []string
 	Iterable bool
 }
@@ -178,14 +179,14 @@ type Array struct {
 }
 
 func (ao *Array) Type() ObjectType { return ARRAY_OBJ }
-func (ao *Array) Next(pos int) (int, Object) {
+func (ao *Array) Next() (Object, Object) {
 	position := ao.position
 	if len(ao.Elements) > position {
 		ao.position = position + 1
-		return position, ao.Elements[position]
+		return &Number{Value: float64(position)}, ao.Elements[position]
 	}
 
-	return -1, nil
+	return nil, nil
 }
 func (ao *Array) Reset() {
 	ao.position = 0
@@ -230,11 +231,18 @@ type HashPair struct {
 }
 
 type Hash struct {
-	Token token.Token
-	Pairs map[HashKey]HashPair
+	Token    token.Token
+	Pairs    map[HashKey]HashPair
+	Position int
 }
 
 func (h *Hash) Type() ObjectType { return HASH_OBJ }
+func (h *Hash) GetPair(key string) (HashPair, bool) {
+	record, ok := h.Pairs[HashKey{Type: "STRING", Value: key}]
+
+	return record, ok
+}
+
 func (h *Hash) Inspect() string {
 	var out bytes.Buffer
 
@@ -242,6 +250,8 @@ func (h *Hash) Inspect() string {
 	for _, pair := range h.Pairs {
 		pairs = append(pairs, fmt.Sprintf("%s: %s", pair.Key.Inspect(), pair.Value.Inspect()))
 	}
+	// create stable key ordered output
+	sort.Strings(pairs)
 
 	out.WriteString("{")
 	out.WriteString(strings.Join(pairs, ", "))
@@ -249,3 +259,35 @@ func (h *Hash) Inspect() string {
 
 	return out.String()
 }
+
+// Pretty convoluted logic here we could
+// refactor.
+// First we sort the hash keys alphabetically
+// and then we loop through them until
+// we reach the required position within
+// the loop.
+func (h *Hash) Next() (Object, Object) {
+	curPosition := 0
+	pairs := make(map[string]HashPair)
+	var keys []string
+	for _, v := range h.Pairs {
+		pairs[v.Key.Inspect()] = v
+		keys = append(keys, v.Key.Inspect())
+	}
+
+	sort.Strings(keys)
+
+	for _, k := range keys {
+		if h.Position == curPosition {
+			h.Position += 1
+			return pairs[k].Key, pairs[k].Value
+		}
+
+		curPosition += 1
+	}
+
+	return nil, nil
+}
+func (h *Hash) Reset() {
+	h.Position = 0
+}
diff --git a/parser/parser.go b/parser/parser.go
index 9ea250c..f1ad0fe 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -72,6 +72,12 @@ type Parser struct {
 	curToken  token.Token
 	peekToken token.Token
 
+	// support assignment to index expressions: a[0] = 1, h["a"] = 1
+	prevIndexExpression *ast.IndexExpression
+
+	// support assignment to hash property h.a = 1
+	prevPropertyExpression *ast.PropertyExpression
+
 	prefixParseFns map[token.TokenType]prefixParseFn
 	infixParseFns  map[token.TokenType]infixParseFn
 }
@@ -188,7 +194,6 @@ func (p *Parser) peekError(tok token.Token) {
 
 func (p *Parser) noPrefixParseFnError(tok token.Token) {
 	msg := fmt.Sprintf("no prefix parse function for '%s' found", tok.Literal)
-	// match := fmt.Sprintf("%s", t)
 	p.reportError(msg, tok)
 }
 
@@ -256,8 +261,10 @@ func (p *Parser) parseDestructuringIdentifiers() []ast.Expression {
 	return list
 }
 
-// x = y
-// x, y = [z, zz]
+// assign to variable: x = y
+// destructuring assignment: x, y = [z, zz]
+// assign to index expressions: a[0] = 1, h["a"] = 1
+// assign to hash property expressions: h.a = 1
 func (p *Parser) parseAssignStatement() ast.Statement {
 	stmt := &ast.AssignStatement{}
 
@@ -275,8 +282,28 @@ func (p *Parser) parseAssignStatement() ast.Statement {
 			p.Rewind(lexerPosition)
 			return nil
 		}
-	} else {
+	} else if p.curTokenIs(token.IDENT) {
 		stmt.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
+	} else if p.curTokenIs(token.ASSIGN) {
+		stmt.Token = p.curToken
+		if p.prevIndexExpression != nil {
+			// support assignment to indexed expressions: a[0] = 1, h["a"] = 1
+			stmt.Index = p.prevIndexExpression
+			p.nextToken()
+			stmt.Value = p.parseExpression(LOWEST)
+			// consume the IndexExpression
+			p.prevIndexExpression = nil
+			return stmt
+		}
+		if p.prevPropertyExpression != nil {
+			// support assignment to hash properties: h.a = 1
+			stmt.Property = p.prevPropertyExpression
+			p.nextToken()
+			stmt.Value = p.parseExpression(LOWEST)
+			// consume the PropertyExpression
+			p.prevPropertyExpression = nil
+			return stmt
+		}
 	}
 
 	if !p.peekTokenIs(token.ASSIGN) {
@@ -292,7 +319,6 @@ func (p *Parser) parseAssignStatement() ast.Statement {
 	if p.peekTokenIs(token.SEMICOLON) {
 		p.nextToken()
 	}
-
 	return stmt
 }
 
@@ -458,14 +484,10 @@ func (p *Parser) parseDottedExpression(object ast.Expression) ast.Expression {
 		exp.Arguments = p.parseExpressionList(token.RPAREN)
 		return exp
 	} else {
+		// support assignment to hash property h.a = 1
 		exp := &ast.PropertyExpression{Token: t, Object: object}
-
-		if !p.curTokenIs(token.IDENT) {
-			msg := fmt.Sprintf("property needs to be an identifier, got '%s'", p.curToken.Literal)
-			p.reportError(msg, p.curToken)
-		}
-
 		exp.Property = p.parseIdentifier()
+		p.prevPropertyExpression = exp
 		return exp
 	}
 }
@@ -775,6 +797,8 @@ func (p *Parser) parseIndexExpression(left ast.Expression) ast.Expression {
 	if !p.expectPeek(token.RBRACKET) {
 		return nil
 	}
+	// support assignment to index expression: a[0] = 1
+	p.prevIndexExpression = exp
 
 	return exp
 }
diff --git a/token/token.go b/token/token.go
index 91e959a..2fcecb9 100644
--- a/token/token.go
+++ b/token/token.go
@@ -1,7 +1,5 @@
 package token
 
-// import "github.com/abs-lang/abs/lexer"
-
 type TokenType string
 
 const (


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index a6f1aa5..87a9406 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -127,6 +127,9 @@ func TestEvalBooleanExpression(t *testing.T) {
 		{`"a" == "b"`, false},
 		{`"a" ~ "b"`, false},
 		{`"a" ~ "A"`, true},
+		{`1 ~ 1`, true},
+		{`1 ~ 1.5`, true},
+		{`2 ~ 1.5`, false},
 		{"true", true},
 		{"false", false},
 		{"1 < 2", true},
@@ -295,6 +298,8 @@ func TestForInExpressions(t *testing.T) {
 		{"a = 0; for k, x in 1..10 { a = a + 1}; a", 10},
 		{"a = 0; for x in 1 { a = a + 1}; a", "'1' is a NUMBER, not an iterable, cannot be used in for loop"},
 		{"a = 0; for x in 1..10 { a = a + 1}; a", 10},
+		{`a = 0; for k, v in {"a": 10} { a = v}; a`, 10},
+		{`a = ""; b = "abc"; for k, v in {"a": 1, "b": 2, "c": 3} { a += k}; a == b`, true},
 		{`a = 0; for k, v in ["x", "y", "z"] { a = a + k}; a`, 3},
 		{`for k, v in ["x", "y", "z"] {}; k`, "identifier not found: k"},
 		{`for k, v in ["x", "y", "z"] {}; v`, "identifier not found: v"},
@@ -306,16 +311,19 @@ func TestForInExpressions(t *testing.T) {
 
 	for _, tt := range tests {
 		evaluated := testEval(tt.input)
-		integer, ok := tt.expected.(int)
-		if ok {
-			testNumberObject(t, evaluated, float64(integer))
-		} else {
+
+		switch ev := tt.expected.(type) {
+		case int:
+			testNumberObject(t, evaluated, float64(ev))
+		case bool:
+			testBooleanObject(t, evaluated, ev)
+		default:
 			errObj, ok := evaluated.(*object.Error)
 			if !ok {
 				t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)
 				continue
 			}
-			logErrorWithPosition(t, errObj.Message, tt.expected)
+			logErrorWithPosition(t, errObj.Message, ev)
 		}
 	}
 }
@@ -484,7 +492,9 @@ func TestAssignStatements(t *testing.T) {
 		{"a = 5; b = a; b;", 5},
 		{"a = 5; b = a; c = a + b + 5; c;", 15},
 		{"a, b, c = [1]; a;", 1},
+		{`a, b, c = {"a": 1}; a;`, 1},
 		{"a, b, c = [1]; b;", nil},
+		{`a, b, c = {"a": 1}; b;`, nil},
 		{`a = 10 + 1 + 2
 b, c = [1, 2]; b`, 1},
 		{`a = 10 + 1 + 2
@@ -766,6 +776,7 @@ c")`, []string{"a", "b", "c"}},
 		{`"A great movie".upper()`, "A GREAT MOVIE"},
 		{`"  A great movie  ".trim()`, "A great movie"},
 		{`"  A great movie  ".trim_by(" A")`, "great movie"},
+		{`sleep(1000)`, nil},
 	}
 	for _, tt := range tests {
 		evaluated := testEval(tt.input)
@@ -965,6 +976,13 @@ 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},
+		{`"x" in {"x": 0}`, true},
+		{`"y" in {"x": 0}`, false},
+		{`"y" in 12`, "'in' operator not supported on NUMBER"},
 	}
 
 	for _, tt := range tests {
@@ -973,7 +991,16 @@ func TestInExpressions(t *testing.T) {
 		switch expected := tt.expected.(type) {
 		case bool:
 			testBooleanObject(t, evaluated, bool(expected))
+		default:
+			errObj, ok := evaluated.(*object.Error)
+
+			if !ok {
+				t.Errorf("no error object returned. got=%T(%+v)", evaluated)
+				continue
+			}
+			logErrorWithPosition(t, errObj.Message, expected)
 		}
+
 	}
 }
 
@@ -1063,6 +1090,11 @@ func TestCommand(t *testing.T) {
 		{`$(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"},
 	}
 
 	for _, tt := range tests {
@@ -1323,3 +1355,117 @@ func testNullObject(t *testing.T, obj object.Object) bool {
 	}
 	return true
 }
+
+func TestEvalStringSpecialChars(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected string
+	}{
+		{`
+		s = "a\nb\nc"
+		s
+		`,
+			`a
+b
+c`,
+		},
+		{`
+		s = "a\tb\tc"
+		s
+		`, `a	b	c`,
+		},
+		{`
+		s = fmt("a\\nb\\nc\n%s\n", "x\ny\nz")
+		s
+		`, `a\\nb\\nc
+x
+y
+z
+`,
+		},
+		{`
+		a = split("a\nb\nc", "\n")
+		str(a)
+		`, `[a, b, c]`,
+		},
+		{`
+		a = split("a\nb\nc", "\n")
+		s = join(a, "\n")
+		s
+		`, `a
+b
+c`,
+		},
+	}
+
+	for _, tt := range tests {
+		evaluated := testEval(tt.input)
+		testStringObject(t, evaluated, tt.expected)
+	}
+}
+
+func TestEvalAssignIndex(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected string
+	}{
+		{`
+		a = [1, 2, 3, 4]
+		a[0] = 99
+		a[1] += 10
+		a += [88]
+		a[2] = "string"
+		a[6] = 66
+		a[5] = 55
+		str(a)
+		`,
+			"[99, 12, string, 4, 88, 55, 66]",
+		},
+		{`
+		h = {"a": 1, "b": 2, "c": 3}
+		h["a"] = 99
+		h["a"] += 1
+		h += {"c": 33, "d": 44, "e": 55}
+		h["z"] = {"x": 10, "y": 20}
+		h["1.23"] = "string"
+		h.d = 99
+		h.d += 1
+		h.z.x = 66
+		h.f = 88
+		str(h)
+		`, "{1.23: string, a: 100, b: 2, c: 33, d: 100, e: 55, f: 88, z: {x: 66, y: 20}}",
+		},
+	}
+
+	for _, tt := range tests {
+		evaluated := testEval(tt.input)
+		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/lexer/lexer_test.go b/lexer/lexer_test.go
index 75fb0cc..cb2af61 100644
--- a/lexer/lexer_test.go
+++ b/lexer/lexer_test.go
@@ -39,6 +39,7 @@ for x = 0; x < 10; x = x + 1 {
 "foo bar"
 [1, 2];
 $(echo "()");
+` + "`echo '()'`" + `;
 {"foo": "bar"}
 $(curl icanhazip.com -X POST)
 $(ls *.go);
@@ -213,6 +214,8 @@ $111
 		{token.SEMICOLON, ";"},
 		{token.COMMAND, `echo "()"`},
 		{token.SEMICOLON, ";"},
+		{token.COMMAND, `echo '()'`},
+		{token.SEMICOLON, ";"},
 		{token.LBRACE, "{"},
 		{token.STRING, "foo"},
 		{token.COLON, ":"},
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 143fcb5..5308765 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -987,6 +987,34 @@ func TestCommandParsing(t *testing.T) {
 	testCommand(t, command, "curl icanhazip.com -X POST")
 }
 
+func TestBacktickParsing(t *testing.T) {
+	input := "`curl icanhazip.com -X POST`"
+
+	l := lexer.New(input)
+	p := New(l)
+	program := p.ParseProgram()
+	checkParserErrors(t, p)
+
+	if len(program.Statements) != 1 {
+		t.Fatalf("program.Statements does not contain %d statements. got=%d\n",
+			1, len(program.Statements))
+	}
+
+	stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
+	if !ok {
+		t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T",
+			program.Statements[0])
+	}
+
+	command, ok := stmt.Expression.(*ast.CommandExpression)
+	if !ok {
+		t.Fatalf("stmt.Expression is not ast.CommandExpression. got=%T",
+			stmt.Expression)
+	}
+
+	testCommand(t, command, "curl icanhazip.com -X POST")
+}
+
 func TestFunctionParameterParsing(t *testing.T) {
 	tests := []struct {
 		input          string
diff --git a/tests/test-assign-index.abs b/tests/test-assign-index.abs
new file mode 100644
index 0000000..257ba34
--- /dev/null
+++ b/tests/test-assign-index.abs
@@ -0,0 +1,82 @@
+echo("=====================")
+echo('Test assignment to array indexed objects')
+echo('>>> a = [1, 2, 3, 4]')
+a = [1, 2, 3, 4]
+echo(a)
+echo('>>> a[0] = 99')
+a[0] = 99
+echo(a)
+echo('>>> a[1] += 10')
+a[1] += 10
+echo(a)
+echo('>>> a += [88]')
+a += [88]
+echo(a)
+echo('>>> a[2] = "string"')
+a[2] = "string"
+echo(a)
+echo('>>> a[6] = 66')
+a[6] = 66
+echo(a)
+echo('>>> a[5] = 55')
+a[5] = 55
+echo(a)
+
+echo("=====================")
+echo('Test assignment to hash indexed objects')
+echo('>>> h = {"a": 1, "b": 2, "c": 3}')
+h = {"a":1, "b":2, "c": 3}
+echo(h)
+echo('>>> h["a"] = 99')
+h["a"] = 99
+echo(h)
+echo('>>> h["a"] += 1')
+h["a"] += 1
+echo(h)
+echo('>>> h += {"c": 33, "d": 44, "e": 55}')
+h += {"c": 33, "d": 44, "e": 55}
+echo(h)
+echo('h["z"] = {"x": 10, "y": 20}')
+h["z"] = {"x": 10, "y": 20}
+echo(h)
+echo('h["1.23"] = "string"')
+h["1.23"] = "string"
+echo(h)
+echo('h.d = 99')
+h.d = 99
+echo(h)
+echo('h.d += 1')
+h.d += 1
+echo(h)
+echo('h.z.x = 66')
+h.z.x = 66
+echo(h)
+echo('h.f = 88')
+h.f = 88
+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)
+
+echo("=====================")
+echo('Error: add number to null hash property')
+echo('h.g += 1')
+h.g += 1
+echo(h)
+
+echo("=====================")
+echo('Error: add number to null hash element')
+echo('>>> h["g"] += 1')
+h["g"] += 1
+echo(h)
+
+
+
diff --git a/tests/test-echo.abs b/tests/test-echo.abs
new file mode 100644
index 0000000..a420df1
--- /dev/null
+++ b/tests/test-echo.abs
@@ -0,0 +1,47 @@
+# tests/tests-echo.abs
+
+echo("=====================")
+msg = 'echo() mixed LFs and escaped LFs'
+ta = 'echo("a\\nb\\nc\n%s\n", "x\ny\nz")'
+echo(">>> Testing %s:", msg)
+echo("%s", ta)
+echo("a\\nb\\nc\n%s\n", "x\ny\nz")
+
+echo("=====================")
+msg = 'echo() multiple escapes'
+ta = 'echo("hel\\\\lo")'
+echo(">>> Testing %s:", msg)
+echo("%s", ta)
+echo("hel\\\\lo")
+
+echo("=====================")
+msg = 'echo() split and join expanded LFs'
+ta = 's = split("a\nb\nc", "\n")'
+tb = 'echo(s)'
+tc = 'ss = join(s, "\n")'
+td = 'echo(ss)'
+echo(">>> Testing %s:", msg)
+echo("%s", ta)
+s = split("a\nb\nc", "\n")
+echo("%s", tb)
+echo(s)
+echo("%s", tc)
+ss = join(s, "\n")
+echo("%s", td)
+echo(ss)
+
+echo("=====================")
+msg = 'echo split and join with literal LFs'
+ta = 's = split(\'a\nb\nc\', \'\n\')'
+tb = 'echo(s)'
+tc = 'ss = join(s, \'\n\')'
+td = 'echo(ss)'
+echo(">>> Testing %s:", msg)
+echo("%s", ta)
+s = split('a\nb\nc', '\n')
+echo("%s", tb)
+echo(s)
+echo("%s", tc)
+ss = join(s, '\n')
+echo("%s", td)
+echo(ss)
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)
+
+
+
+