Repository: abs-lang/abs
Pull request #141 (2019-01-28 20:29:56)

Implement issue #104 array/hash element assignment
ref. issue #104.

Added new functionality:
1) Can assign to individual indexes `array[index]` or `hash["key"]` or hash properties `hash.key`
2) This includes compound operators such as `+=`. 
3) Both arrays and hashes can be extended via `array += array` or `hash += hash`. 
4) Arrays can be extended by assigning to `array[index]` beyond end of array. 
5) Hashes can be extended by assigning to `hash["newkey"]` or `hash.newkey`

See `tests/test-assign-index.abs`.



Patch:
diff --git a/README.md b/README.md
index ce4a31d..20bd1c8 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
 
 # The ABS programming language
 
-ABS is a scripting language that works best when you're on
+ABS is a scripting language that works best when you're scripting on
 your terminal. It tries to combine the elegance of languages
 such as Python, or Ruby, to the convenience of Bash.
 
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 17441cb..73a18fb 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -10,7 +10,7 @@
   <a href="https://github.com/abs-lang/abs"><img alt="Version" src="https://img.shields.io/github/release-pre/abs-lang/abs.svg"></a>
 </p>
 
-ABS is a scripting language that works best when you're on
+ABS is a scripting language that works best when you're scripting on
 your terminal. It tries to combine the elegance of languages
 such as Python, or Ruby, to the convenience of Bash.
 
diff --git a/docs/misc/technical-details.md b/docs/misc/technical-details.md
index 2a41468..a7e7c38 100644
--- a/docs/misc/technical-details.md
+++ b/docs/misc/technical-details.md
@@ -33,7 +33,7 @@ development, and `make test` will run all tests.
 ## 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
@@ -96,7 +96,7 @@ Exit code: 99
 ```
 ### String tests
 String handling tests can be run from `abs tests/test-strings.abs`
-```
+```bash
 $ abs tests/test-strings.abs
 =====================
 >>> Testing string with expanded LFs:
@@ -144,6 +144,65 @@ 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).
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/types/array.md b/docs/types/array.md
index 7a4fcfe..8fed1ca 100644
--- a/docs/types/array.md
+++ b/docs/types/array.md
@@ -39,6 +39,36 @@ x += [3]
 x # [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:
 
@@ -124,19 +154,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 +161,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/hash.md b/docs/types/hash.md
index 7462c46..daf8335 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,10 +8,58 @@ 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 existing keys in the left side will be replaced with the same key on 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}
+```
+
+If the left side is a `hash["key"]` or `hash.key` and the right side is a hash, then the resulting hash will have a new nested hash at the `hash.new`. This includes `hash["newkey"]` or `hash.newkey` as well.
+```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
 
diff --git a/docs/types/string.md b/docs/types/string.md
index 5bcdeb7..3fedcd4 100644
--- a/docs/types/string.md
+++ b/docs/types/string.md
@@ -178,17 +178,17 @@ 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)
 
 Checks whether `str` is present in the string:
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index 13e9c59..cf305d8 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -116,15 +116,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)
 
@@ -173,6 +193,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]) {
@@ -237,6 +258,57 @@ 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
+	} else {
+		return newError(pex.Token, "can only assign to hash property, got %s", leftObj.Type())
+	}
+	return NULL
+}
+
 func evalAssignment(as *ast.AssignStatement, env *object.Environment) object.Object {
 	val := Eval(as.Value, env)
 	if isError(val) {
@@ -246,6 +318,7 @@ 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]
@@ -264,7 +337,15 @@ func evalAssignment(as *ast.AssignStatement, env *object.Environment) object.Obj
 
 			env.Set(name.String(), NULL)
 		}
-
+		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
@@ -324,6 +405,8 @@ 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 left.Type() == object.HASH_OBJ && right.Type() == object.HASH_OBJ:
+		return evalHashInfixExpression(tok, operator, left, right)
 	case operator == "in" && right.Type() == object.ARRAY_OBJ:
 		return evalInExpression(left, right)
 	case operator == "==":
@@ -389,7 +472,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}
@@ -456,6 +538,7 @@ func evalStringInfixExpression(
 	operator string,
 	left, right object.Object,
 ) object.Object {
+
 	if operator == "+" {
 		leftVal := left.(*object.String).Value
 		rightVal := right.(*object.String).Value
@@ -491,6 +574,27 @@ 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 {
diff --git a/object/object.go b/object/object.go
index c721e52..55e8589 100644
--- a/object/object.go
+++ b/object/object.go
@@ -3,6 +3,7 @@ package object
 import (
 	"bytes"
 	"fmt"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -242,6 +243,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, ", "))
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
 }


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index a6f1aa5..f7ee990 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -1323,3 +1323,90 @@ 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)
+	}
+}
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)