From ce7e5cc980218feb8d13b714b71d3deac468698f Mon Sep 17 00:00:00 2001
From: Daniel Kang <me@daniel.gs>
Date: Wed, 9 Jan 2019 17:18:37 -0800
Subject: [PATCH] type conversion builtin functions: string(), int(), bool(),
 float(), char()

---
 ast/undefined_lit.go       |  21 ++++++++
 compiler/compiler.go       |   4 ++
 objects/array.go           |   2 +-
 objects/builtin_convert.go | 104 +++++++++++++++++++++++++++++++++++++
 objects/builtins.go        |  20 +++++++
 objects/char.go            |   6 +--
 objects/float.go           |   3 +-
 objects/map.go             |   2 +-
 objects/undefined.go       |   2 +
 parser/parser.go           |  15 +++---
 token/tokens.go            |   2 +
 vm/vm_builtin_test.go      |  56 ++++++++++++++++++++
 12 files changed, 224 insertions(+), 13 deletions(-)
 create mode 100644 ast/undefined_lit.go
 create mode 100644 objects/builtin_convert.go

diff --git a/ast/undefined_lit.go b/ast/undefined_lit.go
new file mode 100644
index 0000000..8d0c7d2
--- /dev/null
+++ b/ast/undefined_lit.go
@@ -0,0 +1,21 @@
+package ast
+
+import "github.com/d5/tengo/scanner"
+
+type UndefinedLit struct {
+	TokenPos scanner.Pos
+}
+
+func (e *UndefinedLit) exprNode() {}
+
+func (e *UndefinedLit) Pos() scanner.Pos {
+	return e.TokenPos
+}
+
+func (e *UndefinedLit) End() scanner.Pos {
+	return e.TokenPos + 9 // len(undefined) == 9
+}
+
+func (e *UndefinedLit) String() string {
+	return "undefined"
+}
diff --git a/compiler/compiler.go b/compiler/compiler.go
index 73ae8de..55dc00b 100644
--- a/compiler/compiler.go
+++ b/compiler/compiler.go
@@ -162,6 +162,10 @@ func (c *Compiler) Compile(node ast.Node) error {
 		}
 	case *ast.StringLit:
 		c.emit(OpConstant, c.addConstant(&objects.String{Value: node.Value}))
+	case *ast.CharLit:
+		c.emit(OpConstant, c.addConstant(&objects.Char{Value: node.Value}))
+	case *ast.UndefinedLit:
+		c.emit(OpNull)
 	case *ast.UnaryExpr:
 		if err := c.Compile(node.Expr); err != nil {
 			return err
diff --git a/objects/array.go b/objects/array.go
index 5cc3fdf..4b6f55d 100644
--- a/objects/array.go
+++ b/objects/array.go
@@ -46,7 +46,7 @@ func (o *Array) Copy() Object {
 }
 
 func (o *Array) IsFalsy() bool {
-	return false
+	return len(o.Value) == 0
 }
 
 func (o *Array) Equals(x Object) bool {
diff --git a/objects/builtin_convert.go b/objects/builtin_convert.go
new file mode 100644
index 0000000..c1d6cca
--- /dev/null
+++ b/objects/builtin_convert.go
@@ -0,0 +1,104 @@
+package objects
+
+import (
+	"errors"
+	"strconv"
+)
+
+func builtinString(args ...Object) (Object, error) {
+	if len(args) != 1 {
+		return nil, errors.New("wrong number of arguments")
+	}
+
+	switch arg := args[0].(type) {
+	case *String:
+		return arg, nil
+	case *Undefined:
+		return undefined, nil
+	default:
+		return &String{Value: arg.String()}, nil
+	}
+}
+
+func builtinInt(args ...Object) (Object, error) {
+	if len(args) != 1 {
+		return nil, errors.New("wrong number of arguments")
+	}
+
+	switch arg := args[0].(type) {
+	case *Int:
+		return arg, nil
+	case *Float:
+		return &Int{Value: int64(arg.Value)}, nil
+	case *Char:
+		return &Int{Value: int64(arg.Value)}, nil
+	case *Bool:
+		if arg.Value {
+			return &Int{Value: 1}, nil
+		}
+		return &Int{Value: 0}, nil
+	case *String:
+		n, err := strconv.ParseInt(arg.Value, 10, 64)
+		if err == nil {
+			return &Int{Value: n}, nil
+		}
+	}
+
+	return undefined, nil
+}
+
+func builtinFloat(args ...Object) (Object, error) {
+	if len(args) != 1 {
+		return nil, errors.New("wrong number of arguments")
+	}
+
+	switch arg := args[0].(type) {
+	case *Float:
+		return arg, nil
+	case *Int:
+		return &Float{Value: float64(arg.Value)}, nil
+	case *String:
+		f, err := strconv.ParseFloat(arg.Value, 64)
+		if err == nil {
+			return &Float{Value: f}, nil
+		}
+	}
+
+	return undefined, nil
+}
+
+func builtinBool(args ...Object) (Object, error) {
+	if len(args) != 1 {
+		return nil, errors.New("wrong number of arguments")
+	}
+
+	switch arg := args[0].(type) {
+	case *Bool:
+		return arg, nil
+	default:
+		return &Bool{Value: !arg.IsFalsy()}, nil
+	}
+}
+
+func builtinChar(args ...Object) (Object, error) {
+	if len(args) != 1 {
+		return nil, errors.New("wrong number of arguments")
+	}
+
+	switch arg := args[0].(type) {
+	case *Char:
+		return arg, nil
+	case *Int:
+		return &Char{Value: rune(arg.Value)}, nil
+	case *String:
+		rs := []rune(arg.Value)
+		switch len(rs) {
+		case 0:
+			return &Char{}, nil
+		case 1:
+			return &Char{Value: rs[0]}, nil
+		}
+	}
+
+	return undefined, nil
+}
diff --git a/objects/builtins.go b/objects/builtins.go
index 4f83b58..6dad33b 100644
--- a/objects/builtins.go
+++ b/objects/builtins.go
@@ -22,4 +22,24 @@ var Builtins = []struct {
 		Name: "append",
 		Func: builtinAppend,
 	},
+	{
+		Name: "string",
+		Func: builtinString,
+	},
+	{
+		Name: "int",
+		Func: builtinInt,
+	},
+	{
+		Name: "bool",
+		Func: builtinBool,
+	},
+	{
+		Name: "float",
+		Func: builtinFloat,
+	},
+	{
+		Name: "char",
+		Func: builtinChar,
+	},
 }
diff --git a/objects/char.go b/objects/char.go
index a674894..340e3c5 100644
--- a/objects/char.go
+++ b/objects/char.go
@@ -1,8 +1,6 @@
 package objects
 
 import (
-	"fmt"
-
 	"github.com/d5/tengo/token"
 )
 
@@ -11,7 +9,7 @@ type Char struct {
 }
 
 func (o *Char) String() string {
-	return fmt.Sprintf("%q", string(o.Value))
+	return string(o.Value)
 }
 
 func (o *Char) TypeName() string {
@@ -27,7 +25,7 @@ func (o *Char) Copy() Object {
 }
 
 func (o *Char) IsFalsy() bool {
-	return false
+	return o.Value == 0
 }
 
 func (o *Char) Equals(x Object) bool {
diff --git a/objects/float.go b/objects/float.go
index 0714477..ecbe8a4 100644
--- a/objects/float.go
+++ b/objects/float.go
@@ -1,6 +1,7 @@
 package objects
 
 import (
+	"math"
 	"strconv"
 
 	"github.com/d5/tengo/token"
@@ -68,7 +69,7 @@ func (o *Float) Copy() Object {
 }
 
 func (o *Float) IsFalsy() bool {
-	return false
+	return math.IsNaN(o.Value)
 }
 
 func (o *Float) Equals(x Object) bool {
diff --git a/objects/map.go b/objects/map.go
index 260661e..a0eab33 100644
--- a/objects/map.go
+++ b/objects/map.go
@@ -38,7 +38,7 @@ func (o *Map) Copy() Object {
 }
 
 func (o *Map) IsFalsy() bool {
-	return false
+	return len(o.Value) == 0
 }
 
 func (o *Map) Get(key string) (Object, bool) {
diff --git a/objects/undefined.go b/objects/undefined.go
index 98b27b8..61a035f 100644
--- a/objects/undefined.go
+++ b/objects/undefined.go
@@ -2,6 +2,8 @@ package objects
 
 import "github.com/d5/tengo/token"
 
+var undefined = &Undefined{}
+
 type Undefined struct{}
 
 func (o Undefined) TypeName() string {
diff --git a/parser/parser.go b/parser/parser.go
index eb4a50a..37439df 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"io"
 	"strconv"
-	"unicode/utf8"
 
 	"github.com/d5/tengo/ast"
 	"github.com/d5/tengo/scanner"
@@ -286,9 +285,8 @@ func (p *Parser) parseOperand() ast.Expr {
 		return x
 
 	case token.Char:
-		v, _ := utf8.DecodeRuneInString(p.tokenLit)
 		x := &ast.CharLit{
-			Value:    v,
+			Value:    rune(p.tokenLit[1]),
 			ValuePos: p.pos,
 			Literal:  p.tokenLit,
 		}
@@ -323,6 +321,11 @@ func (p *Parser) parseOperand() ast.Expr {
 		p.next()
 		return x
 
+	case token.Undefined:
+		x := &ast.UndefinedLit{TokenPos: p.pos}
+		p.next()
+		return x
+
 	case token.LParen:
 		lparen := p.pos
 		p.next()
@@ -485,10 +488,10 @@ func (p *Parser) parseStmt() (stmt ast.Stmt) {
 	}
 
 	switch p.token {
-	case                                                                                                                  // simple statements
+	case // simple statements
 		token.Func, token.Ident, token.Int, token.Float, token.Char, token.String, token.True, token.False, token.LParen, // operands
-		token.LBrace, token.LBrack,                                                                                       // composite types
-		token.Add, token.Sub, token.Mul, token.And, token.Xor, token.Not:                                                 // unary operators
+		token.LBrace, token.LBrack, // composite types
+		token.Add, token.Sub, token.Mul, token.And, token.Xor, token.Not: // unary operators
 		s := p.parseSimpleStmt(false)
 		p.expectSemi()
 		return s
diff --git a/token/tokens.go b/token/tokens.go
index 2fd549f..19e2f19 100644
--- a/token/tokens.go
+++ b/token/tokens.go
@@ -78,6 +78,7 @@ const (
 	True
 	False
 	In
+	Undefined
 	_keywordEnd
 )
 
@@ -150,6 +151,7 @@ var tokens = [...]string{
 	True:         "true",
 	False:        "false",
 	In:           "in",
+	Undefined:    "undefined",
 }
 
 func (tok Token) String() string {
diff --git a/vm/vm_builtin_test.go b/vm/vm_builtin_test.go
index 098639c..aa2f028 100644
--- a/vm/vm_builtin_test.go
+++ b/vm/vm_builtin_test.go
@@ -17,4 +17,60 @@ func TestBuiltinFunction(t *testing.T) {
 	expect(t, `out = append([1, 2, 3], 4)`, ARR{1, 2, 3, 4})
 	expect(t, `out = append([1, 2, 3], 4, 5, 6)`, ARR{1, 2, 3, 4, 5, 6})
 	expect(t, `out = append([1, 2, 3], "foo", false)`, ARR{1, 2, 3, "foo", false})
+
+	expect(t, `out = int(1)`, 1)
+	expect(t, `out = int(1.8)`, 1)
+	expect(t, `out = int("-522")`, -522)
+	expect(t, `out = int(true)`, 1)
+	expect(t, `out = int(false)`, 0)
+	expect(t, `out = int('8')`, 56)
+	expect(t, `out = int([1])`, undefined())
+	expect(t, `out = int({a: 1})`, undefined())
+	expect(t, `out = int(undefined)`, undefined())
+
+	expect(t, `out = string(1)`, "1")
+	expect(t, `out = string(1.8)`, "1.8")
+	expect(t, `out = string("-522")`, "-522")
+	expect(t, `out = string(true)`, "true")
+	expect(t, `out = string(false)`, "false")
+	expect(t, `out = string('8')`, "8")
+	expect(t, `out = string([1,8.1,true,3])`, "[1, 8.1, true, 3]")
+	expect(t, `out = string({b: "foo"})`, `{b: "foo"}`)
+	expect(t, `out = string(undefined)`, undefined()) // not "undefined"
+
+	expect(t, `out = float(1)`, 1.0)
+	expect(t, `out = float(1.8)`, 1.8)
+	expect(t, `out = float("-52.2")`, -52.2)
+	expect(t, `out = float(true)`, undefined())
+	expect(t, `out = float(false)`, undefined())
+	expect(t, `out = float('8')`, undefined())
+	expect(t, `out = float([1,8.1,true,3])`, undefined())
+	expect(t, `out = float({a: 1, b: "foo"})`, undefined())
+	expect(t, `out = float(undefined)`, undefined())
+
+	expect(t, `out = char(56)`, '8')
+	expect(t, `out = char(1.8)`, undefined())
+	expect(t, `out = char("-52.2")`, undefined())
+	expect(t, `out = char(true)`, undefined())
+	expect(t, `out = char(false)`, undefined())
+	expect(t, `out = char('8')`, '8')
+	expect(t, `out = char([1,8.1,true,3])`, undefined())
+	expect(t, `out = char({a: 1, b: "foo"})`, undefined())
+	expect(t, `out = char(undefined)`, undefined())
+
+	expect(t, `out = bool(1)`, true)          // non-zero integer: true
+	expect(t, `out = bool(0)`, false)         // zero: true
+	expect(t, `out = bool(1.8)`, true)        // all floats (except for NaN): true
+	expect(t, `out = bool(0.0)`, true)        // all floats (except for NaN): true
+	expect(t, `out = bool("false")`, true)    // non-empty string: true
+	expect(t, `out = bool("")`, false)        // empty string: false
+	expect(t, `out = bool(true)`, true)       // true: true
+	expect(t, `out = bool(false)`, false)     // false: false
+	expect(t, `out = bool('8')`, true)        // non-zero chars: true
+	expect(t, `out = bool(char(0))`, false)   // zero char: false
+	expect(t, `out = bool([1])`, true)        // non-empty arrays: true
+	expect(t, `out = bool([])`, false)        // empty array: false
+	expect(t, `out = bool({a: 1})`, true)     // non-empty maps: true
+	expect(t, `out = bool({})`, false)        // empty maps: false
+	expect(t, `out = bool(undefined)`, false) // undefined: false
 }