Repository: abs-lang/abs
Pull request #129 (2019-01-22 20:35:44)

1.0 version
PR that keeps track of all changes going to `1.0.0`.


Patch:
diff --git a/ast/ast.go b/ast/ast.go
index cc97a97..862f3ba 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -245,11 +245,19 @@ func (me *MethodExpression) String() string {
 	return out.String()
 }
 
-type IfExpression struct {
-	Token       token.Token // The 'if' token
+// A scenario is used to
+// represent a path within an
+// IF block (if x = 2 { return x}).
+// It has a condition (x = 2)
+// and a consenquence (return x).
+type Scenario struct {
 	Condition   Expression
 	Consequence *BlockStatement
-	Alternative *BlockStatement
+}
+
+type IfExpression struct {
+	Token     token.Token // The 'if' token
+	Scenarios []*Scenario
 }
 
 func (ie *IfExpression) expressionNode()      {}
@@ -257,14 +265,16 @@ func (ie *IfExpression) TokenLiteral() string { return ie.Token.Literal }
 func (ie *IfExpression) String() string {
 	var out bytes.Buffer
 
-	out.WriteString("if")
-	out.WriteString(ie.Condition.String())
-	out.WriteString(" ")
-	out.WriteString(ie.Consequence.String())
+	for i, s := range ie.Scenarios {
+		if i != 0 {
+			out.WriteString("else")
+			out.WriteString(" ")
+		}
 
-	if ie.Alternative != nil {
-		out.WriteString("else ")
-		out.WriteString(ie.Alternative.String())
+		out.WriteString("if")
+		out.WriteString(s.Condition.String())
+		out.WriteString(" ")
+		out.WriteString(s.Consequence.String())
 	}
 
 	return out.String()
diff --git a/docs/installer.sh b/docs/installer.sh
index 117b507..98f01e6 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=preview-4
+VERSION=1.0.0
 
 echo "Trying to detect the details of your architecture."
 echo ""
diff --git a/docs/misc/error.md b/docs/misc/error.md
index ba56eba..a1e4909 100644
--- a/docs/misc/error.md
+++ b/docs/misc/error.md
@@ -12,7 +12,7 @@ When you are running ABS interactively in the Run, Eval, Print Loop (REPL) the l
 
 However, when you are running ABS over a script file (even a small one) locating errors requires more help from the interpreter. ABS now provides `[line:column]` positions as well as the error line itself following the error message.
 
-For example, a file with syntax errors might look like this when the first syntax error is in line 4 somewhere around column 11.
+For example, a file with syntax errors might look like this when the first syntax error is in line 4 at column 5.
 ```
 $ cat examples/error-parse.abs
 # there are multiple parser errors in this file
@@ -30,20 +30,20 @@ c/c = $(command)
 b %% c
 
 $ abs examples/error-parse.abs
- parser errors:
+  parser errors:
 	no prefix parse function for '=' found
-	[4:11]	m.a = 'abc'
+	[4:5]	m.a = 'abc'
 	no prefix parse function for '=' found
-	[7:16]	d/d = $(command);
+	[7:5]	d/d = $(command);
 	no prefix parse function for '=' found
-	[10:16]	c/c = $(command)
+	[10:5]	c/c = $(command)
 	no prefix parse function for '%' found
-	[13:6]	b %% c
+	[13:4]	b %% c
 
 $ echo $?
 99
 ```
-Furthermore, a file with evaluation errors might look like this when the first error encountered is in line 2 somewhere around column 11:
+Furthermore, a file with evaluation errors might look like this when the first error encountered is in line 2 at column 3:
 ```
 $ cat examples/error-eval.abs
 # there is an evaluation error on line 2
@@ -52,7 +52,7 @@ echo("should not reach here")
 
 $ abs examples/error-eval.abs
 ERROR: type mismatch: NUMBER + STRING
-	[2:11]	1 + "hello"
+	[2:3]	1 + "hello"
 
 $ echo $?
 99
diff --git a/docs/misc/technical-details.md b/docs/misc/technical-details.md
index 2d6260d..2a41468 100644
--- a/docs/misc/technical-details.md
+++ b/docs/misc/technical-details.md
@@ -31,59 +31,59 @@ With `make run` you can get inside a container built for ABS'
 development, and `make test` will run all tests.
 
 ## Testing
-You can run the interpreter error location tests by invoking this bash script: `tests/test-abs.sh`.
+### 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.
 ```
 $ tests/test-abs.sh
-
 =======================================
 Test Parser
 tests/test-parser.abs
  parser errors:
 	no prefix parse function for '=' found
-	[4:11]	m.a = 'abc'
+	[4:5]	m.a = 'abc'
 	no prefix parse function for '=' found
-	[7:16]	d/d = $(command);
+	[7:5]	d/d = $(command);
 	no prefix parse function for '=' found
-	[10:16]	c/c = $(command)
+	[10:5]	c/c = $(command)
 	no prefix parse function for '%' found
-	[13:6]	b %% c
+	[13:4]	b %% c
 	no prefix parse function for '&&' found
-	[22:4]	&&||!-/*5;
-	no prefix parse function for 'OR' found
-	[22:5]	&&||!-/*5;
+	[22:1]	&&||!-/*5;
+	no prefix parse function for '||' found
+	[22:3]	&&||!-/*5;
 	no prefix parse function for '/' found
-	[22:8]	&&||!-/*5;
+	[22:7]	&&||!-/*5;
 	no prefix parse function for '<=>' found
-	[25:3]	<=>
-	expected next token to be ], got , instead
-	[44:3]	[1, 2];
+	[25:2]	<=>
+	expected next token to be NUMBER, got , instead
+	[44:2]	[1, 2];
 	no prefix parse function for ',' found
-	[44:5]	[1, 2];
+	[44:3]	[1, 2];
 	no prefix parse function for ']' found
-	[44:7]	[1, 2];
+	[44:6]	[1, 2];
 	no prefix parse function for '%' found
 	[68:2]	~%
 	no prefix parse function for '-=' found
-	[70:2]	-=
+	[70:1]	-=
 	no prefix parse function for '/=' found
-	[72:2]	/=
+	[72:1]	/=
 	no prefix parse function for '%=' found
-	[74:2]	%=
+	[74:1]	%=
 	no prefix parse function for '^' found
-	[79:4]	&^>><<
+	[79:2]	&^>><<
 	no prefix parse function for '<<' found
-	[79:6]	&^>><<
+	[79:5]	&^>><<
 	Illegal token '$111'
-	[80:4]	$111
-	no prefix parse function for 'ILLEGAL' found
-	[76:7]	1.str()
+	[80:1]	$111
+	no prefix parse function for '$111' found
+	[80:1]	$111
 Exit code: 99
 
 =======================================
 Test Eval()
 tests/test-eval.abs
 ERROR: type mismatch: STRING + NUMBER
-	[8:35]	    s = s + 1   # this is a comment
+	[8:11]	    s = s + 1   # this is a comment
 Exit code: 99
 
 ERROR: invalid property 'junk' on type ARRAY
@@ -91,13 +91,62 @@ ERROR: invalid property 'junk' on type ARRAY
 Exit code: 99
 
 ERROR: index operator not supported: f(x) {x} on HASH
-	[19:29]	    {"name": "Abs"}[f(x) {x}];  
+	[19:20]	    {"name": "Abs"}[f(x) {x}];  
 Exit code: 99
-
 ```
+### String tests
+String handling tests can be run from `abs tests/test-strings.abs`
+```
+$ abs tests/test-strings.abs
+=====================
+>>> Testing string with expanded LFs:
+echo("a\nb\nc")
+a
+b
+c
+=====================
+>>> Testing string with expanded TABs:
+echo("a\tb\tc")
+a	b	c
+=====================
+>>> Testing string with expanded CRs:
+echo("a\rb\rc")
+c
+=====================
+>>> Testing string with mixed expanded LFs and escaped LFs:
+echo("a\\nb\\nc\n%s\n", "x\ny\nz")
+a\\nb\\nc
+x
+y
+z
+
+=====================
+>>> Testing string with multiple escapes:
+echo("hel\\\\lo")
+hel\\\\lo
+=====================
+>>> Testing split and join strings with expanded LFs:
+s = split("a\nb\nc", "\n")
+echo(s)
+[a, b, c]
+ss = join(s, "\n")
+echo(ss)
+a
+b
+c
+=====================
+>>> Testing split and join strings with literal LFs:
+s = split('a\nb\nc', '\n')
+echo(s)
+[a, b, c]
+ss = join(s, '\n')
+echo(ss)
+a\nb\nc
+```
+
 ## Roadmap
 
-We're currently working on [1.0](https://github.com/abs-lang/abs/milestone/5).
+We're currently working on [1.1](https://github.com/abs-lang/abs/milestone/6).
 
 ## Next
 
diff --git a/docs/syntax/if.md b/docs/syntax/if.md
index 8070ae6..1b5e1e7 100644
--- a/docs/syntax/if.md
+++ b/docs/syntax/if.md
@@ -8,13 +8,15 @@ if x > 0 {
 }
 ```
 
-as well as `else` alternatives:
+as well as `else` and `else if` alternatives:
 
 ``` bash
 if x > 0 {
-    echo("hello world")
+    echo("x is high")
+} else if x < 0 {
+    echo("x is low")
 } else {
-    echo("hello globe")
+    echo("x is actually zero!")
 }
 ```
 
@@ -27,9 +29,6 @@ if (x > 0) {
 }
 ```
 
-Note that `else if` clauses are not supported,
-although they are planned (see [#27](https://github.com/abs-lang/abs/issues/27)).
-
 ## Next
 
 That's about it for this section!
diff --git a/docs/syntax/operators.md b/docs/syntax/operators.md
index a37b9ea..66843e5 100644
--- a/docs/syntax/operators.md
+++ b/docs/syntax/operators.md
@@ -98,6 +98,16 @@ a = 10
 a /= 2 # a is now 5
 ```
 
+## in
+
+Checks whether a value is in an array:
+
+``` bash
+1 in [1, 2, 3] # true
+9 in [1, 2, 3] # false
+9 in 9 # unknown operator: NUMBER in NUMBER
+```
+
 ## **
 
 Mathematical exponentiation:
diff --git a/docs/types/array.md b/docs/types/array.md
index 1501ed1..7a4fcfe 100644
--- a/docs/types/array.md
+++ b/docs/types/array.md
@@ -139,6 +139,10 @@ as well as being able to parse all valid JSON expressions (see [#54](https://git
 
 ### contains(e)
 
+> This function is deprecated and might be removed in future versions.
+>
+> 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:
diff --git a/docs/types/string.md b/docs/types/string.md
index 7574dde..5bcdeb7 100644
--- a/docs/types/string.md
+++ b/docs/types/string.md
@@ -47,6 +47,72 @@ a value that evaluates to `false` when casted to boolean:
 !!"" # 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"`. 
+
+If the string is double quoted these characters will be expanded to their ASCII codes. On the other hand, if the string is single quoted, these characters will be considered as escaped literals. 
+
+This means, for example, that double quoted LFs will cause line feeds to appear in the output:
+
+```bash
+⧐  echo("a\nb\nc")
+a
+b
+c
+⧐  
+```
+Conversely, single quoted LFs will appear as escaped literal strings:
+
+```bash
+⧐  echo('a\nb\nc')
+a\nb\nc
+⧐  
+```
+
+And if you need to mix escaped and unescaped special characters, then you can do this with double escapes within double quoted strings:
+```bash
+⧐  echo("a\\nb\nc")
+a\\nb
+c
+⧐  
+```
+### Working with special characters in string functions
+Special characters also work with `split()` and `join()` and other string functions as well.
+
+1) Double quoted expanded special characters:
+```bash
+⧐  s = split("a\nb\nc", "\n")
+⧐  echo(s)
+[a, b, c]
+⧐  ss = join(s, "\n")
+⧐  echo(ss)
+a
+b
+c
+⧐ 
+```
+
+2) Single quoted literal special characters:
+```bash
+⧐  s = split('a\nb\nc', '\n')
+⧐  echo(s)
+[a, b, c]
+⧐  ss = join(s, '\n')
+⧐  echo(ss)
+a\nb\nc
+⧐  
+```
+
+3) Double quoted, double escaped special characters:
+```bash
+⧐  s = split("a\\nb\\nc", "\\n")
+⧐  echo(s)
+[a, b, c]
+⧐  ss = join(s, "\\n")
+⧐  echo(ss)
+a\\nb\\nc
+⧐  
+```
 ## Supported functions
 
 ### len()
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index 72a27fa..13e9c59 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"os/exec"
 	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/abs-lang/abs/ast"
@@ -317,15 +318,14 @@ func evalInfixExpression(
 		}
 
 		return right
-	case operator == "+=":
-		fmt.Printf("%s %s %s", operator, left, right)
-		return nil
 	case left.Type() == object.NUMBER_OBJ && right.Type() == object.NUMBER_OBJ:
 		return evalNumberInfixExpression(tok, operator, left, right)
 	case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ:
 		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:
+		return evalInExpression(left, right)
 	case operator == "==":
 		return nativeBoolToBooleanObject(left == right)
 	case operator == "!=":
@@ -491,22 +491,54 @@ func evalArrayInfixExpression(
 	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!
+			}
+		}
+	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!
+			}
+		}
+	}
+
+	return &object.Boolean{Token: tok, Value: found}
+}
+
 func evalIfExpression(
 	ie *ast.IfExpression,
 	env *object.Environment,
 ) object.Object {
-	condition := Eval(ie.Condition, env)
-	if isError(condition) {
-		return condition
-	}
+	for _, scenario := range ie.Scenarios {
+		condition := Eval(scenario.Condition, env)
 
-	if isTruthy(condition) {
-		return Eval(ie.Consequence, env)
-	} else if ie.Alternative != nil {
-		return Eval(ie.Alternative, env)
-	} else {
-		return NULL
+		if isError(condition) {
+			return condition
+		}
+
+		if isTruthy(condition) {
+			return Eval(scenario.Consequence, env)
+		}
 	}
+
+	return NULL
 }
 
 func evalWhileExpression(
diff --git a/evaluator/functions.go b/evaluator/functions.go
index cce1178..6c7b7ea 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -176,6 +176,11 @@ func getFns() map[string]*object.Builtin {
 		"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 {
@@ -254,9 +259,7 @@ func getFns() map[string]*object.Builtin {
 				case *object.Number:
 					return &object.Boolean{Token: tok, Value: true}
 				case *object.String:
-					_, err := strconv.ParseFloat(arg.Value, 64)
-
-					return &object.Boolean{Token: tok, Value: err == nil}
+					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())
@@ -389,10 +392,6 @@ func getFns() map[string]*object.Builtin {
 		//
 		// Also, we're instantiating a new lexer & parser from
 		// scratch, so this is a tad slow.
-		//
-		// This method is incomplete as it currently does not
-		// support most JSON types, but rather just objects,
-		// ie. "[1, 2, 3]".json() won't work.
 		"json": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
 			Fn: func(args ...object.Object) object.Object {
@@ -402,13 +401,45 @@ func getFns() map[string]*object.Builtin {
 				}
 
 				s := args[0].(*object.String)
+				str := strings.TrimSpace(s.Value)
 				env := object.NewEnvironment()
-				l := lexer.New(s.Value)
+				l := lexer.New(str)
 				p := parser.New(l)
-				hl, ok := p.ParseHashLiteral().(*ast.HashLiteral)
+				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 evalHashLiteral(hl, env)
+					return Eval(node, env)
 				}
 
 				return newError(tok, "argument to `json` must be a valid JSON object, got '%s'", s.Value)
@@ -662,7 +693,7 @@ func getFns() map[string]*object.Builtin {
 					switch needle := args[1].(type) {
 					case *object.String:
 						for _, v := range arg.Elements {
-							if v.Inspect() == needle.Value {
+							if v.Inspect() == needle.Value && v.Type() == object.STRING_OBJ {
 								found = true
 								break // Let's get outta here!
 							}
diff --git a/lexer/lexer.go b/lexer/lexer.go
index ccb8e76..9e0aff5 100644
--- a/lexer/lexer.go
+++ b/lexer/lexer.go
@@ -381,33 +381,55 @@ func (l *Lexer) readLogicalOperator() string {
 // character itself ("\\").
 func (l *Lexer) readString(quote byte) string {
 	var chars []string
+	esc := byte('\\')
 	doubleEscape := false
 	for {
 		l.readChar()
 
-		if l.ch == '\\' && l.peekChar() == '\\' {
-			chars = append(chars, string('\\'))
+		if l.ch == esc && l.peekChar() == esc {
+			chars = append(chars, string(esc))
 			l.readChar()
-			doubleEscape = true
+			// be careful here, there may be double escaped LFs in the string
+			if l.peekChar() == quote {
+				doubleEscape = true
+			} else {
+				// this is not a double escaped quote
+				chars = append(chars, string(esc))
+			}
 			continue
 		}
-
-		// If we encounter a \, let's check whether
-		// we're trying to escape a ". If so, let's skip
-		// the / and add the " to the string.
-		if l.ch == '\\' && l.peekChar() == quote {
+		// If we encounter an escape, let's check whether
+		// we're trying to escape a quote. If so, let's skip
+		// the escape and add the quote to the string.
+		if l.ch == esc && l.peekChar() == quote {
 			chars = append(chars, string(quote))
 			l.readChar()
 			continue
 		}
-
-		// The string ends when we encounter a "
-		// and the character before that was not a \,
-		// or the \ was escaped as well ("string\\").
-		if (l.ch == quote && (l.prevChar(2) != '\\' || doubleEscape)) || l.ch == 0 {
+		// If this is a double quoted string we need to expand embedded
+		// LF, CR, and TAB to ASCII and add the ASCII code to the string
+		// NB. single quoted strings don't expand special characters to ASCII
+		if quote == '"' {
+			if l.ch == esc && l.peekChar() == 'n' {
+				chars = append(chars, "\n")
+				l.readChar()
+				continue
+			} else if l.ch == esc && l.peekChar() == 'r' {
+				chars = append(chars, "\r")
+				l.readChar()
+				continue
+			} else if l.ch == esc && l.peekChar() == 't' {
+				chars = append(chars, "\t")
+				l.readChar()
+				continue
+			}
+		}
+		// The string ends when we encounter a quote
+		// and the character before that was not an escape,
+		// or the escape was escaped as well ("string\\").
+		if (l.ch == quote && (l.prevChar(2) != esc || doubleEscape)) || l.ch == 0 {
 			break
 		}
-
 		chars = append(chars, string(l.ch))
 		doubleEscape = false
 	}
diff --git a/main.go b/main.go
index c283d10..b92e95f 100644
--- a/main.go
+++ b/main.go
@@ -10,7 +10,7 @@ import (
 	"github.com/abs-lang/abs/repl"
 )
 
-var VERSION = "preview-4"
+var VERSION = "1.0.0"
 
 // The ABS interpreter
 func main() {
diff --git a/parser/parser.go b/parser/parser.go
index c8ff658..0779c9a 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -84,14 +84,14 @@ func New(l *lexer.Lexer) *Parser {
 
 	p.prefixParseFns = make(map[token.TokenType]prefixParseFn)
 	p.registerPrefix(token.IDENT, p.parseIdentifier)
-	p.registerPrefix(token.NUMBER, p.parseNumberLiteral)
+	p.registerPrefix(token.NUMBER, p.ParseNumberLiteral)
 	p.registerPrefix(token.STRING, p.ParseStringLiteral)
 	p.registerPrefix(token.NULL, p.ParseNullLiteral)
 	p.registerPrefix(token.BANG, p.parsePrefixExpression)
 	p.registerPrefix(token.MINUS, p.parsePrefixExpression)
 	p.registerPrefix(token.TILDE, p.parsePrefixExpression)
-	p.registerPrefix(token.TRUE, p.parseBoolean)
-	p.registerPrefix(token.FALSE, p.parseBoolean)
+	p.registerPrefix(token.TRUE, p.ParseBoolean)
+	p.registerPrefix(token.FALSE, p.ParseBoolean)
 	p.registerPrefix(token.LPAREN, p.parseGroupedExpression)
 	p.registerPrefix(token.IF, p.parseIfExpression)
 	p.registerPrefix(token.WHILE, p.parseWhileExpression)
@@ -368,7 +368,7 @@ func (p *Parser) parseIdentifier() ast.Expression {
 }
 
 // 1 or 1.1
-func (p *Parser) parseNumberLiteral() ast.Expression {
+func (p *Parser) ParseNumberLiteral() ast.Expression {
 	lit := &ast.NumberLiteral{Token: p.curToken}
 
 	value, err := strconv.ParseFloat(p.curToken.Literal, 64)
@@ -482,7 +482,7 @@ func (p *Parser) parseMethodExpression(object ast.Expression) ast.Expression {
 }
 
 // true
-func (p *Parser) parseBoolean() ast.Expression {
+func (p *Parser) ParseBoolean() ast.Expression {
 	return &ast.Boolean{Token: p.curToken, Value: p.curTokenIs(token.TRUE)}
 }
 
@@ -500,29 +500,62 @@ func (p *Parser) parseGroupedExpression() ast.Expression {
 
 // if x {
 //   return x
-// } esle {
+// } else if y {
 //   return y
+// } else {
+//   return z
 // }
 func (p *Parser) parseIfExpression() ast.Expression {
 	expression := &ast.IfExpression{Token: p.curToken}
+	scenarios := []*ast.Scenario{}
 
 	p.nextToken()
-	expression.Condition = p.parseExpression(LOWEST)
+	scenario := &ast.Scenario{}
+	scenario.Condition = p.parseExpression(LOWEST)
 
 	if !p.expectPeek(token.LBRACE) {
 		return nil
 	}
 
-	expression.Consequence = p.parseBlockStatement()
+	scenario.Consequence = p.parseBlockStatement()
+	scenarios = append(scenarios, scenario)
 
-	if p.peekTokenIs(token.ELSE) {
+	// If we encounter ELSEs then let's add more
+	// scenarios to our expression.
+	for p.peekTokenIs(token.ELSE) {
 		p.nextToken()
-		if !p.expectPeek(token.LBRACE) {
-			return nil
+		p.nextToken()
+		scenario := &ast.Scenario{}
+
+		// ELSE IF
+		if p.curTokenIs(token.IF) {
+			p.nextToken()
+			scenario.Condition = p.parseExpression(LOWEST)
+
+			if !p.expectPeek(token.LBRACE) {
+				return nil
+			}
+		} else {
+			// This is a regular ELSE block.
+			//
+			// In order not to have a weird data structure
+			// representing an IF expression, we simply define
+			// it as a list of scenarios.
+			// In case a simple ELSE if encountered, we set the
+			// condition of this scenario to true, so that it always
+			// evaluates to true.
+			tok := &token.Token{Position: -99, Literal: "true", Type: token.LookupIdent(token.TRUE)}
+			scenario.Condition = &ast.Boolean{Token: *tok, Value: true}
 		}
 
-		expression.Alternative = p.parseBlockStatement()
+		scenario.Consequence = p.parseBlockStatement()
+		scenarios = append(scenarios, scenario)
+
+		if !p.peekTokenIs(token.ELSE) {
+			p.nextToken()
+		}
 	}
+	expression.Scenarios = scenarios
 	return expression
 }
 
diff --git a/scripts/release.abs b/scripts/release.abs
index cb4abb5..182afb6 100644
--- a/scripts/release.abs
+++ b/scripts/release.abs
@@ -16,6 +16,19 @@
 # "dragonfly/amd64"
 # "android/arm"
 # "solaris/amd64"
+
+# freebsd and netbsd can be easily
+# built, but we opt to keep them
+# out as the userbase doesn't seem
+# to be there.
+#
+# "freebsd/386",
+# "freebsd/amd64",
+# "freebsd/arm",
+# "netbsd/386",
+# "netbsd/amd64",
+# "netbsd/arm"
+
 platforms = [
     "linux/386",
     "linux/amd64",
@@ -28,13 +41,7 @@ platforms = [
     "windows/amd64",
     "windows/386",
     "darwin/amd64",
-    "darwin/386",
-    "freebsd/386",
-    "freebsd/amd64",
-    "freebsd/arm",
-    "netbsd/386",
-    "netbsd/amd64",
-    "netbsd/arm"
+    "darwin/386"
 ]
 
 echo("Deleting previous builds...")
diff --git a/util/util.go b/util/util.go
index 14e4a20..d3d870e 100644
--- a/util/util.go
+++ b/util/util.go
@@ -1,5 +1,7 @@
 package util
 
+import "strconv"
+
 // Checks whether the element e is in the
 // list of strings s
 func Contains(s []string, e string) bool {
@@ -10,3 +12,9 @@ func Contains(s []string, e string) bool {
 	}
 	return false
 }
+
+func IsNumber(s string) bool {
+	_, err := strconv.ParseFloat(s, 64)
+
+	return err == nil
+}


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index 6feecf9..a6f1aa5 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -1,6 +1,7 @@
 package evaluator
 
 import (
+	"flag"
 	"fmt"
 	"strings"
 	"testing"
@@ -14,8 +15,11 @@ func logErrorWithPosition(t *testing.T, msg string, expected interface{}) {
 	errorStr := msg
 	expected, _ = expected.(string)
 	expectedStr := fmt.Sprintf("%s", expected)
-	if strings.Contains(errorStr, expectedStr) {
-		t.Log("expected error:", errorStr)
+	if strings.HasPrefix(errorStr, expectedStr) {
+		// Only log when we're running the verbose tests
+		if flag.Lookup("test.v").Value.String() == "true" {
+			t.Log("expected error:", errorStr)
+		}
 	} else {
 		expectedStr = fmt.Sprintf("ERROR: wrong error message. expected='%s',", expectedStr)
 		t.Error(expectedStr, "\ngot=", errorStr)
@@ -95,6 +99,9 @@ func TestEvalStringExpression(t *testing.T) {
 	}{
 		{"9999999999.str()", "9999999999"},
 		{"12.1.str()", "12.1"},
+		{`"\n"`, "\n"},
+		{`"\r"`, "\r"},
+		{`"\t"`, "\t"},
 		{"12.123456789.str()", "12.123456789"},
 		{`"nice 'escaping"`, "nice 'escaping"},
 		{`'nice "escaping"`, `nice "escaping"`},
@@ -184,6 +191,10 @@ func TestIfElseExpressions(t *testing.T) {
 		{"if 1 > 2 { 10 }", nil},
 		{"if 1 > 2 { 10 } else { 20 }", 20},
 		{"if 1 < 2 { 10 } else { 20 }", 10},
+		{"if 3 > 2 { 1 } else if 1 > 0 {2} else if 5 > 0 {3} else {4}", 1},
+		{"if 1 > 2 { 1 } else if 1 > 0 {2} else if 5 > 0 {3} else {4}", 2},
+		{"if 1 > 2 { 1 } else if 1 > 1 {2} else if 5 > 0 {3} else {4}", 3},
+		{"if 1 > 2 { 1 } else if 1 > 1 {2} else if 5 > 10 {3} else {4}", 4},
 	}
 
 	for _, tt := range tests {
@@ -638,20 +649,11 @@ func TestBuiltinFunctions(t *testing.T) {
 		{`[].sum()`, 0},
 		{`[1, 2].sum()`, 3},
 		{`[1].map(f(x) { y = x + 1 }).str()`, "[null]"},
-		{`contains("hello", "lo")`, true},
-		{`contains("hello", "_")`, false},
-		{`contains("hello", 1)`, false},
-		{`contains(["hello"], "hello")`, true},
-		{`contains([1, 2], 2)`, true},
-		{`contains(["hello"], "_")`, false},
-		{`contains(["hello"], 1)`, false},
-		{`contains("1", 1)`, false},
-		{`contains(1..100000, 1)`, true},
 		{`find([1,2,3,3], f(x) {x == 3})`, 3},
 		{`find([1,2], f(x) {x == "some"})`, nil},
 		{`arg("o")`, "argument 0 to arg(...) is not supported (got: o, allowed: NUMBER)"},
 		{`arg(3)`, ""},
-		{`pwd().split("").reverse().slice(0, 33).reverse().join("").replace("\\", "/", -1)`, "/evaluator"}, // Little trick to get travis to run this test, as the base path is not /go/src/
+		{`pwd().split("").reverse().slice(0, 33).reverse().join("").replace("\\", "/", -1).suffix("/evaluator")`, true}, // Little trick to get travis to run this test, as the base path is not /go/src/
 		{`rand(1)`, 0},
 		{`int(10)`, 10},
 		{`int(10.5)`, 10},
@@ -684,8 +686,13 @@ func TestBuiltinFunctions(t *testing.T) {
 		{`"{\"a\": null}".json().a`, nil},
 		{`type(null)`, "NULL"},
 		{`"{\"k\": \"v\"}".json()["k"]`, "v"},
-		{`"hello".json()`, "argument to `json` must be a valid JSON object, got 'hello'"},
-		{`"\"hello".json()`, "argument to `json` must be a valid JSON object, got '\"hello'"},
+		{`"2".json()`, 2},
+		{`'"2"'.json()`, "2"},
+		{`'true'.json()`, true},
+		{`'null'.json()`, nil},
+		{`'"hello"'.json()`, "hello"},
+		{`'[1, 2, 3]'.json()`, []int{1, 2, 3}},
+		{`'"hello'.json()`, "argument to `json` must be a valid JSON object, got '\"hello'"},
 		{`split("a\"b\"c", "\"")`, []string{"a", "b", "c"}},
 		{`lines("a
 b
@@ -774,7 +781,7 @@ c")`, []string{"a", "b", "c"}},
 		case string:
 			s, ok := evaluated.(*object.String)
 			if ok {
-				if !strings.Contains(s.Value, tt.expected.(string)) {
+				if s.Value != tt.expected.(string) {
 					t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected)
 				}
 				continue
@@ -863,7 +870,7 @@ func TestLogicalOperators(t *testing.T) {
 		case string:
 			s, ok := evaluated.(*object.String)
 			if ok {
-				if !strings.Contains(s.Value, tt.expected.(string)) {
+				if s.Value != tt.expected.(string) {
 					t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected)
 				}
 
@@ -916,7 +923,7 @@ func TestRangesOperators(t *testing.T) {
 		case string:
 			s, ok := evaluated.(*object.String)
 			if ok {
-				if !strings.Contains(s.Value, tt.expected.(string)) {
+				if s.Value != tt.expected.(string) {
 					t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected)
 				}
 
@@ -948,6 +955,28 @@ func TestRangesOperators(t *testing.T) {
 	}
 }
 
+func TestInExpressions(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{`1 in [1]`, true},
+		{`1 in ["1"]`, false},
+		{`"1" in [1]`, false},
+		{`1 in [1, 2]`, true},
+		{`"hello" in [1, 2]`, false},
+	}
+
+	for _, tt := range tests {
+		evaluated := testEval(tt.input)
+
+		switch expected := tt.expected.(type) {
+		case bool:
+			testBooleanObject(t, evaluated, bool(expected))
+		}
+	}
+}
+
 func TestBuiltinProperties(t *testing.T) {
 	tests := []struct {
 		input    string
@@ -975,7 +1004,7 @@ func TestBuiltinProperties(t *testing.T) {
 		case string:
 			s, ok := evaluated.(*object.String)
 			if ok {
-				if !strings.Contains(s.Value, tt.expected.(string)) {
+				if s.Value != tt.expected.(string) {
 					t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected)
 				}
 
@@ -1048,7 +1077,7 @@ func TestCommand(t *testing.T) {
 				t.Errorf("object is not String. got=%T (%+v)", evaluated, evaluated)
 				continue
 			}
-			if !strings.Contains(stringObj.Value, expected) {
+			if stringObj.Value != expected {
 				t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, stringObj.Value, expected)
 			}
 		}
diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go
index 0b4890a..75fb0cc 100644
--- a/lexer/lexer_test.go
+++ b/lexer/lexer_test.go
@@ -19,6 +19,8 @@ result = add(five, ten);
 <=>
 if (5 < 10) {
 	return true;
+} else if x {
+	return 0;
 } else {
 	return false;
 }
@@ -144,6 +146,14 @@ $111
 		{token.SEMICOLON, ";"},
 		{token.RBRACE, "}"},
 		{token.ELSE, "else"},
+		{token.IF, "if"},
+		{token.IDENT, "x"},
+		{token.LBRACE, "{"},
+		{token.RETURN, "return"},
+		{token.NUMBER, "0"},
+		{token.SEMICOLON, ";"},
+		{token.RBRACE, "}"},
+		{token.ELSE, "else"},
 		{token.LBRACE, "{"},
 		{token.RETURN, "return"},
 		{token.FALSE, "false"},
@@ -237,12 +247,12 @@ $111
 		{token.IDENT, "tree"},
 		{token.STRING, "hel\"lo"},
 		{token.STRING, "hel\\lo"},
-		{token.STRING, "hel\\\\lo"},
+		{token.STRING, `hel\\\\lo`},
 		{token.STRING, "\"hello\""},
 		{token.STRING, "\"he\"\"llo\""},
 		{token.STRING, "hello\\"},
-		{token.STRING, "hello\\\\"},
-		{token.STRING, "\\\\hello"},
+		{token.STRING, `hello\\\`},
+		{token.STRING, `\\\\hello`},
 		{token.EXPONENT, "**"},
 		{token.NUMBER, "1"},
 		{token.RANGE, ".."},
@@ -310,7 +320,7 @@ func TestRewind(t *testing.T) {
 		}
 
 		if tok.Literal != tt.expectedLiteral {
-			t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q", i, tt.expectedLiteral, tok.Literal)
+			t.Errorf("tests[%d] - literal wrong. expected=%q, got=%q", i, tt.expectedLiteral, tok.Literal)
 		}
 	}
 
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 623cef4..16cdd81 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -428,8 +428,8 @@ func TestBooleanExpression(t *testing.T) {
 	}
 }
 
-func TestIfExpression(t *testing.T) {
-	input := `if x < y { x }`
+func TestIfIfElseExpression(t *testing.T) {
+	input := `if x < y { x } else if x > y { y } else if x == y { z }`
 
 	l := lexer.New(input)
 	p := New(l)
@@ -453,32 +453,80 @@ func TestIfExpression(t *testing.T) {
 			stmt.Expression)
 	}
 
-	if !testInfixExpression(t, exp.Condition, "x", "<", "y") {
+	if len(exp.Scenarios) != 3 {
+		t.Errorf("expression does not have 4 scenarios. got=%d\n",
+			len(exp.Scenarios))
+	}
+
+	// First scenario
+	scenario := exp.Scenarios[0]
+
+	if !testInfixExpression(t, scenario.Condition, "x", "<", "y") {
 		return
 	}
 
-	if len(exp.Consequence.Statements) != 1 {
+	if len(scenario.Consequence.Statements) != 1 {
 		t.Errorf("consequence is not 1 statements. got=%d\n",
-			len(exp.Consequence.Statements))
+			len(scenario.Consequence.Statements))
 	}
 
-	consequence, ok := exp.Consequence.Statements[0].(*ast.ExpressionStatement)
+	consequence, ok := scenario.Consequence.Statements[0].(*ast.ExpressionStatement)
 	if !ok {
 		t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T",
-			exp.Consequence.Statements[0])
+			scenario.Consequence.Statements[0])
 	}
 
 	if !testIdentifier(t, consequence.Expression, "x") {
 		return
 	}
 
-	if exp.Alternative != nil {
-		t.Errorf("exp.Alternative.Statements was not nil. got=%+v", exp.Alternative)
+	// Second scenario
+	scenario = exp.Scenarios[1]
+
+	if !testInfixExpression(t, scenario.Condition, "x", ">", "y") {
+		return
+	}
+
+	if len(scenario.Consequence.Statements) != 1 {
+		t.Errorf("consequence is not 1 statements. got=%d\n",
+			len(scenario.Consequence.Statements))
+	}
+
+	consequence, ok = scenario.Consequence.Statements[0].(*ast.ExpressionStatement)
+	if !ok {
+		t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T",
+			scenario.Consequence.Statements[0])
+	}
+
+	if !testIdentifier(t, consequence.Expression, "y") {
+		return
+	}
+
+	// Third scenario
+	scenario = exp.Scenarios[2]
+
+	if !testInfixExpression(t, scenario.Condition, "x", "==", "y") {
+		return
+	}
+
+	if len(scenario.Consequence.Statements) != 1 {
+		t.Errorf("consequence is not 1 statements. got=%d\n",
+			len(scenario.Consequence.Statements))
+	}
+
+	consequence, ok = scenario.Consequence.Statements[0].(*ast.ExpressionStatement)
+	if !ok {
+		t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T",
+			scenario.Consequence.Statements[0])
+	}
+
+	if !testIdentifier(t, consequence.Expression, "z") {
+		return
 	}
 }
 
-func TestIfElseExpression(t *testing.T) {
-	input := `if x < y { x } else { y }`
+func TestIfIfElseElseExpression(t *testing.T) {
+	input := `if x < y { x } else if x > y { y } else if x == y { z } else { a }`
 
 	l := lexer.New(input)
 	p := New(l)
@@ -498,38 +546,153 @@ func TestIfElseExpression(t *testing.T) {
 
 	exp, ok := stmt.Expression.(*ast.IfExpression)
 	if !ok {
-		t.Fatalf("stmt.Expression is not ast.IfExpression. got=%T", stmt.Expression)
+		t.Fatalf("stmt.Expression is not ast.IfExpression. got=%T",
+			stmt.Expression)
+	}
+
+	if len(exp.Scenarios) != 4 {
+		t.Errorf("expression does not have 4 scenarios. got=%d\n",
+			len(exp.Scenarios))
 	}
 
-	if !testInfixExpression(t, exp.Condition, "x", "<", "y") {
+	// First scenario
+	scenario := exp.Scenarios[0]
+
+	if !testInfixExpression(t, scenario.Condition, "x", "<", "y") {
 		return
 	}
 
-	if len(exp.Consequence.Statements) != 1 {
+	if len(scenario.Consequence.Statements) != 1 {
 		t.Errorf("consequence is not 1 statements. got=%d\n",
-			len(exp.Consequence.Statements))
+			len(scenario.Consequence.Statements))
 	}
 
-	consequence, ok := exp.Consequence.Statements[0].(*ast.ExpressionStatement)
+	consequence, ok := scenario.Consequence.Statements[0].(*ast.ExpressionStatement)
 	if !ok {
 		t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T",
-			exp.Consequence.Statements[0])
+			scenario.Consequence.Statements[0])
 	}
 
 	if !testIdentifier(t, consequence.Expression, "x") {
 		return
 	}
 
-	if len(exp.Alternative.Statements) != 1 {
-		t.Errorf("exp.Alternative.Statements does not contain 1 statements. got=%d\n", len(exp.Alternative.Statements))
+	// Second scenario
+	scenario = exp.Scenarios[1]
+
+	if !testInfixExpression(t, scenario.Condition, "x", ">", "y") {
+		return
+	}
+
+	if len(scenario.Consequence.Statements) != 1 {
+		t.Errorf("consequence is not 1 statements. got=%d\n",
+			len(scenario.Consequence.Statements))
 	}
 
-	alternative, ok := exp.Alternative.Statements[0].(*ast.ExpressionStatement)
+	consequence, ok = scenario.Consequence.Statements[0].(*ast.ExpressionStatement)
 	if !ok {
-		t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T", exp.Alternative.Statements[0])
+		t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T",
+			scenario.Consequence.Statements[0])
+	}
+
+	if !testIdentifier(t, consequence.Expression, "y") {
+		return
+	}
+
+	// Third scenario
+	scenario = exp.Scenarios[2]
+
+	if !testInfixExpression(t, scenario.Condition, "x", "==", "y") {
+		return
+	}
+
+	if len(scenario.Consequence.Statements) != 1 {
+		t.Errorf("consequence is not 1 statements. got=%d\n",
+			len(scenario.Consequence.Statements))
 	}
 
-	if !testIdentifier(t, alternative.Expression, "y") {
+	consequence, ok = scenario.Consequence.Statements[0].(*ast.ExpressionStatement)
+	if !ok {
+		t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T",
+			scenario.Consequence.Statements[0])
+	}
+
+	if !testIdentifier(t, consequence.Expression, "z") {
+		return
+	}
+
+	// Fourth scenario, the else
+	scenario = exp.Scenarios[3]
+
+	if !testBooleanLiteral(t, scenario.Condition, true) {
+		return
+	}
+
+	if len(scenario.Consequence.Statements) != 1 {
+		t.Errorf("consequence is not 1 statements. got=%d\n",
+			len(scenario.Consequence.Statements))
+	}
+
+	consequence, ok = scenario.Consequence.Statements[0].(*ast.ExpressionStatement)
+	if !ok {
+		t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T",
+			scenario.Consequence.Statements[0])
+	}
+
+	if !testIdentifier(t, consequence.Expression, "a") {
+		return
+	}
+}
+
+func TestIfExpression(t *testing.T) {
+	input := `if x < y { x }`
+
+	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])
+	}
+
+	exp, ok := stmt.Expression.(*ast.IfExpression)
+	if !ok {
+		t.Fatalf("stmt.Expression is not ast.IfExpression. got=%T",
+			stmt.Expression)
+	}
+
+	if len(exp.Scenarios) != 1 {
+		t.Errorf("expression does not have 4 scenarios. got=%d\n",
+			len(exp.Scenarios))
+	}
+
+	// First scenario
+	scenario := exp.Scenarios[0]
+
+	if !testInfixExpression(t, scenario.Condition, "x", "<", "y") {
+		return
+	}
+
+	if len(scenario.Consequence.Statements) != 1 {
+		t.Errorf("consequence is not 1 statements. got=%d\n",
+			len(scenario.Consequence.Statements))
+	}
+
+	consequence, ok := scenario.Consequence.Statements[0].(*ast.ExpressionStatement)
+	if !ok {
+		t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T",
+			scenario.Consequence.Statements[0])
+	}
+
+	if !testIdentifier(t, consequence.Expression, "x") {
 		return
 	}
 }
diff --git a/tests/test-strings.abs b/tests/test-strings.abs
new file mode 100644
index 0000000..afdff95
--- /dev/null
+++ b/tests/test-strings.abs
@@ -0,0 +1,67 @@
+# tests/tests-strings.abs
+echo("=====================")
+msg = 'string with expanded LFs'
+ta = 'echo("a\nb\nc")'
+echo(">>> Testing %s:", msg)
+echo("%s", ta)
+echo("a\nb\nc")
+
+echo("=====================")
+msg = 'string with expanded TABs'
+ta = 'echo("a\tb\tc")'
+echo(">>> Testing %s:", msg)
+echo("%s", ta)
+echo("a\tb\tc")
+
+echo("=====================")
+msg = 'string with expanded CRs'
+ta = 'echo("a\rb\rc")'
+echo(">>> Testing %s:", msg)
+echo("%s", ta)
+echo("a\rb\rc")
+
+echo("=====================")
+msg = 'string with mixed expanded 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 = 'string with multiple escapes'
+ta = 'echo("hel\\\\lo")'
+echo(">>> Testing %s:", msg)
+echo("%s", ta)
+echo("hel\\\\lo")
+
+echo("=====================")
+msg = 'split and join strings with 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 = 'split and join strings 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)