From 13c9062fed5f62056ecebcdfa105f9d78ebad5b7 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Sat, 20 Jan 2024 13:20:13 -0500 Subject: [PATCH] Strings --- ast/ast.go | 13 ++++++++++++ evaluator/evaluator.go | 16 ++++++++++++++ evaluator/evaluator_test.go | 32 ++++++++++++++++++++++++++++ lexer/lexer.go | 14 +++++++++++++ lexer/lexer_test.go | 42 ++++++++++++++++++++----------------- object/object.go | 17 +++++++++++++-- parser/parser.go | 5 +++++ parser/parser_test.go | 19 +++++++++++++++++ token/token.go | 5 +++-- 9 files changed, 140 insertions(+), 23 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 9ab4a76..eb667bc 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -264,3 +264,16 @@ func (ce *CallExpression) String() string { return out.String() } + +type StringLiteral struct { + Token token.Token + Value string +} + +func (sl StringLiteral) TokenLiteral() string { + return sl.Token.Literal +} +func (sl StringLiteral) String() string { + return sl.Token.Literal +} +func (sl StringLiteral) expressionNode() {} diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 679490d..6969853 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -94,6 +94,9 @@ func Eval(node ast.Node, env *object.Environment) object.Object { return applyFunction(function, args) + case *ast.StringLiteral: + return &object.String{Value: node.Value} + } return nil @@ -178,6 +181,8 @@ func evalInfixExpression(operator string, left, right object.Object) object.Obje switch { case left.Type() == object.INTEGER_OBJ && right.Type() == object.INTEGER_OBJ: return evalIntegerInfixExpression(operator, left, right) + case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ: + return evalStringInfixExpression(operator, left, right) case operator == "==": return nativeBoolToBooleanObject(left == right) case operator == "!=": @@ -189,6 +194,17 @@ func evalInfixExpression(operator string, left, right object.Object) object.Obje } } +func evalStringInfixExpression(operator string, left object.Object, right object.Object) object.Object { + if operator != "+" { + return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type()) + } + + leftVal := left.(*object.String).Value + rightVal := right.(*object.String).Value + + return &object.String{Value: leftVal + rightVal} +} + func evalIntegerInfixExpression(operator string, left, right object.Object) object.Object { leftVal := left.(*object.Integer).Value rightVal := right.(*object.Integer).Value diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 29baf42..dffabef 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -205,6 +205,10 @@ func TestErrorHandling(t *testing.T) { "foobar", "identifier not found: foobar", }, + { + `"Hello" - "World"`, + "unknown operator: STRING - STRING", + }, } for _, tt := range tests { @@ -294,6 +298,34 @@ func TestClosures(t *testing.T) { testIntegerObject(t, testEval(input), 4) } +func TestStringLiteral(t *testing.T) { + input := `"Hello World!"` + + evaluated := testEval(input) + str, ok := evaluated.(*object.String) + if !ok { + t.Fatalf("object is not String. got=%T (+%v)", evaluated, evaluated) + } + + if str.Value != "Hello World!" { + t.Errorf("String has wrong value. got=%q", str.Value) + } +} + +func TestStringConcatenation(t *testing.T) { + input := `"Hello" + " " + "World!"` + + evaluated := testEval(input) + str, ok := evaluated.(*object.String) + if !ok { + t.Fatalf("object is not String. got=%T (+%v)", evaluated, evaluated) + } + + if str.Value != "Hello World!" { + t.Errorf("String has wrong value. got=%q", str.Value) + } +} + func testEval(input string) object.Object { l := lexer.New(input) p := parser.New(l) diff --git a/lexer/lexer.go b/lexer/lexer.go index 30bccf8..33b9203 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -91,6 +91,9 @@ func (l *Lexer) NextToken() token.Token { case 0: tok.Literal = "" tok.Type = token.EOF + case '"': + tok.Type = token.STRING + tok.Literal = l.readString() default: if isLetter(l.ch) { tok.Literal = l.readIdentifier() @@ -145,3 +148,14 @@ func (l *Lexer) skipWhitespace() { l.readChar() } } + +func (l *Lexer) readString() string { + position := l.position + 1 + for { + l.readChar() + if l.ch == '"' || l.ch == 0 { + break + } + } + return l.input[position:l.position] +} diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index eb6e2cf..aa04c77 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -7,25 +7,27 @@ import ( func TestNextToken(t *testing.T) { input := `let five = 5; -let ten = 10; - -let add = fn(x, y) { - x + y; -}; - -let result = add(five, ten); -!-/*5; -5 < 10 > 5; - -if (5 < 10) { - return true; -} else { - return false; -} - -10 == 10; -10 != 9; -` + let ten = 10; + + let add = fn(x, y) { + x + y; + }; + + let result = add(five, ten); + !-/*5; + 5 < 10 > 5; + + if (5 < 10) { + return true; + } else { + return false; + } + + 10 == 10; + 10 != 9; + "foobar" + "foo bar" + ` tests := []struct { expectedType token.TokenType @@ -113,6 +115,8 @@ if (5 < 10) { {token.INT, "9"}, {token.SEMICOLON, ";"}, + {token.STRING, "foobar"}, + {token.STRING, "foo bar"}, {token.EOF, ""}, } diff --git a/object/object.go b/object/object.go index 32495d9..7d2db1d 100644 --- a/object/object.go +++ b/object/object.go @@ -16,6 +16,7 @@ const ( RETURN_VALUE_OBJ = "RETURN_VALUE" ERROR_OBJ = "ERROR" FUNCTION_OBJ = "FUNCTION" + STRING_OBJ = "STRING" ) type Object interface { @@ -85,11 +86,11 @@ type Function struct { Env *Environment } -func (f Function) Type() ObjectType { +func (f *Function) Type() ObjectType { return FUNCTION_OBJ } -func (f Function) Inspect() string { +func (f *Function) Inspect() string { var out bytes.Buffer params := []string{} @@ -106,3 +107,15 @@ func (f Function) Inspect() string { return out.String() } + +type String struct { + Value string +} + +func (s *String) Type() ObjectType { + return STRING_OBJ +} + +func (s *String) Inspect() string { + return s.Value +} diff --git a/parser/parser.go b/parser/parser.go index c632951..5f58356 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -64,6 +64,7 @@ func New(l *lexer.Lexer) *Parser { p.registerPrefix(token.LPAREN, p.parseGroupedExpression) p.registerPrefix(token.IF, p.parseIfExpression) p.registerPrefix(token.FUNCTION, p.parseFunctionLiteral) + p.registerPrefix(token.STRING, p.parseStringLiteral) p.infixParseFns = make(map[token.TokenType]infixParseFn) p.registerInfix(token.PLUS, p.parseInfixExpression) @@ -430,3 +431,7 @@ func (p *Parser) registerPrefix(tokenType token.TokenType, fn prefixParseFn) { func (p *Parser) registerInfix(tokenType token.TokenType, fn infixParseFn) { p.infixParseFns[tokenType] = fn } + +func (p *Parser) parseStringLiteral() ast.Expression { + return &ast.StringLiteral{Token: p.curToken, Value: p.curToken.Literal} +} diff --git a/parser/parser_test.go b/parser/parser_test.go index 52ba231..3b872f9 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -670,6 +670,25 @@ func TestCallExpressionParameterParsing(t *testing.T) { } } +func TestStringLiteralExpression(t *testing.T) { + input := `"hello world";` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + literal, ok := stmt.Expression.(*ast.StringLiteral) + if !ok { + t.Fatalf("exp not *ast.StringLiteral. got=%T", stmt.Expression) + } + + if literal.Value != "hello world" { + t.Errorf("literal.Value not %q. got=%q", "hello world", literal.Value) + } +} + func testLetStatement(t *testing.T, s ast.Statement, name string) bool { if s.TokenLiteral() != "let" { t.Errorf("s.TokenLiteral not 'let'. got=%q", s.TokenLiteral()) diff --git a/token/token.go b/token/token.go index 5cae921..fa5e6dc 100644 --- a/token/token.go +++ b/token/token.go @@ -12,8 +12,9 @@ const ( EOF = "EOF" // Identifiers + literals - IDENT = "IDENT" // add, foobar, x, y - INT = "INT" // 123456 + IDENT = "IDENT" // add, foobar, x, y + INT = "INT" // 123456 + STRING = "STRING" // Operators ASSIGN = "="