From 37222c562650c4ac031d4054665bcc261b5c5fb6 Mon Sep 17 00:00:00 2001 From: Jun Nishimura Date: Wed, 2 Oct 2024 00:48:58 +0900 Subject: [PATCH 1/5] add tokens for loop expression (#12) --- token/token.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/token/token.go b/token/token.go index 5b6ee8e..9c0bc09 100644 --- a/token/token.go +++ b/token/token.go @@ -23,6 +23,11 @@ const ( SET = "SET" VAR = "VAR" VAL = "VAL" + LOOP = "LOOP" + FOR = "FOR" + FROM = "FROM" + TO = "TO" + DO = "DO" SYMBOL = "SYMBOL" @@ -63,6 +68,11 @@ var reservedWords = map[string]TokenType{ "set": SET, "var": VAR, "val": VAL, + "loop": LOOP, + "for": FOR, + "from": FROM, + "to": TO, + "do": DO, } func LookupStringTokenType(word string) TokenType { From 9e498975267d4fa6f87aa50e93d886416dccf789 Mon Sep 17 00:00:00 2001 From: Jun Nishimura Date: Wed, 2 Oct 2024 00:49:31 +0900 Subject: [PATCH 2/5] add unit tests for loop expression in lexer (#12) --- lexer/lexer_test.go | 79 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index 5a404e8..a958dc3 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -562,6 +562,85 @@ func TestSingleProgram(t *testing.T) { {Type: token.EOF, Literal: ""}, }, }, + { + name: "loop expression", + input: ` + { + "loop": { + "for": "$i", + "from": 0, + "to": 10, + "do": { + "command": { + "symbol": "==", + "args": ["$i", 5] + } + } + } + }`, + expected: []token.Token{ + {Type: token.LBRACE, Literal: "{"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.LOOP, Literal: "loop"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COLON, Literal: ":"}, + {Type: token.LBRACE, Literal: "{"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.FOR, Literal: "for"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COLON, Literal: ":"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.SYMBOL, Literal: "$i"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COMMA, Literal: ","}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.FROM, Literal: "from"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COLON, Literal: ":"}, + {Type: token.INT, Literal: "0"}, + {Type: token.COMMA, Literal: ","}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.TO, Literal: "to"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COLON, Literal: ":"}, + {Type: token.INT, Literal: "10"}, + {Type: token.COMMA, Literal: ","}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.DO, Literal: "do"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COLON, Literal: ":"}, + {Type: token.LBRACE, Literal: "{"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COMMAND, Literal: "command"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COLON, Literal: ":"}, + {Type: token.LBRACE, Literal: "{"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.SYMBOLKEY, Literal: "symbol"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COLON, Literal: ":"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.EQ, Literal: "=="}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COMMA, Literal: ","}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.ARGS, Literal: "args"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COLON, Literal: ":"}, + {Type: token.LBRACKET, Literal: "["}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.SYMBOL, Literal: "$i"}, + {Type: token.DOUBLE_QUOTE, Literal: "\""}, + {Type: token.COMMA, Literal: ","}, + {Type: token.INT, Literal: "5"}, + {Type: token.RBRACKET, Literal: "]"}, + {Type: token.RBRACE, Literal: "}"}, + {Type: token.RBRACE, Literal: "}"}, + {Type: token.RBRACE, Literal: "}"}, + {Type: token.RBRACE, Literal: "}"}, + {Type: token.EOF, Literal: ""}, + }, + }, } for _, tt := range tests { From c473b359a6b8e514707eb36b1efe0ce2a7fe5fd1 Mon Sep 17 00:00:00 2001 From: Jun Nishimura Date: Wed, 2 Oct 2024 00:49:57 +0900 Subject: [PATCH 3/5] add ast for loop expression (#12) --- ast/ast.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/ast/ast.go b/ast/ast.go index b7d8005..d258700 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -129,6 +129,31 @@ func (ie *IfExpression) String() string { return out.String() } +type LoopExpression struct { + Token token.Token + Index *Symbol + From *IntegerLiteral + To *IntegerLiteral + Body Expression +} + +func (le *LoopExpression) TokenLiteral() string { return le.Token.Literal } +func (le *LoopExpression) String() string { + var out bytes.Buffer + + out.WriteString("{\"loop\": {\"for\": ") + out.WriteString(le.Index.String()) + out.WriteString(", \"from\": ") + out.WriteString(le.From.String()) + out.WriteString(", \"to\": ") + out.WriteString(le.To.String()) + out.WriteString(", \"body\": ") + out.WriteString(le.Body.String()) + out.WriteString("}}") + + return out.String() +} + type SetExpression struct { Token token.Token Name *Symbol From 43dd9c9fa50555d91a0d0f342902bfbd7ba632f2 Mon Sep 17 00:00:00 2001 From: Jun Nishimura Date: Wed, 2 Oct 2024 00:50:16 +0900 Subject: [PATCH 4/5] parse loop expression (#12) --- parser/parser.go | 102 ++++++++++++++++++++++++++++++++++++++++++ parser/parser_test.go | 81 +++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) diff --git a/parser/parser.go b/parser/parser.go index cf968ac..5d27659 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -110,6 +110,8 @@ func (p *Parser) parseObject() (obj ast.Expression, err error) { obj, err = p.parseIfExpression() case token.SET: obj, err = p.parseSetExpression() + case token.LOOP: + obj, err = p.parseLoopExpression() default: err = fmt.Errorf("unexpected token type %s", p.curToken.Type) } @@ -324,6 +326,106 @@ func (p *Parser) parseSetExpression() (*ast.SetExpression, error) { }, nil } +func (p *Parser) parseLoopExpression() (*ast.LoopExpression, error) { + loopExpToken, err := p.expectQuotedToken(token.LOOP) + if err != nil { + return nil, err + } + + // skip to for + if err := p.expectTokens( + token.COLON, + token.LBRACE, + token.DOUBLE_QUOTE, + token.FOR, + token.DOUBLE_QUOTE, + token.COLON, + ); err != nil { + return nil, err + } + + // parse for + parsedIndex, err := p.parseExpression() + if err != nil { + return nil, err + } + index, ok := parsedIndex.(*ast.Symbol) + if !ok { + return nil, fmt.Errorf("expected symbol, got %T", parsedIndex) + } + + // skip to from + if err := p.expectTokens( + token.COMMA, + token.DOUBLE_QUOTE, + token.FROM, + token.DOUBLE_QUOTE, + token.COLON, + ); err != nil { + return nil, err + } + + // parse from + parsedFrom, err := p.parseExpression() + if err != nil { + return nil, err + } + from, ok := parsedFrom.(*ast.IntegerLiteral) + if !ok { + return nil, fmt.Errorf("expected integer, got %T", parsedFrom) + } + + // skip to to + if err := p.expectTokens( + token.COMMA, + token.DOUBLE_QUOTE, + token.TO, + token.DOUBLE_QUOTE, + token.COLON, + ); err != nil { + return nil, err + } + + // parse to + parsedTo, err := p.parseExpression() + if err != nil { + return nil, err + } + to, ok := parsedTo.(*ast.IntegerLiteral) + if !ok { + return nil, fmt.Errorf("expected integer, got %T", parsedTo) + } + + // skip to do + if err := p.expectTokens( + token.COMMA, + token.DOUBLE_QUOTE, + token.DO, + token.DOUBLE_QUOTE, + token.COLON, + ); err != nil { + return nil, err + } + + // parse do + body, err := p.parseExpression() + if err != nil { + return nil, err + } + + if err := p.expectTokens(token.RBRACE); err != nil { + return nil, err + } + + return &ast.LoopExpression{ + Token: loopExpToken, + Index: index, + From: from, + To: to, + Body: body, + }, nil +} + func (p *Parser) parseAtom() (ast.Expression, error) { switch p.curToken.Type { case token.MINUS: diff --git a/parser/parser_test.go b/parser/parser_test.go index 5a2a565..45d7339 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -672,6 +672,87 @@ func TestIfExpression(t *testing.T) { } } +func TestLoopExpression(t *testing.T) { + tests := []struct { + name string + input string + expected *ast.LoopExpression + }{ + { + name: "loop expression", + input: ` + { + "loop": { + "for": "$i", + "from": 0, + "to": 10, + "do": { + "command": { + "symbol": "+", + "args": ["$i", 1] + } + } + } + }`, + expected: &ast.LoopExpression{ + Token: token.Token{Type: token.LOOP, Literal: "loop"}, + Index: &ast.Symbol{ + Token: token.Token{Type: token.SYMBOL, Literal: "$i"}, + Value: "$i", + }, + From: &ast.IntegerLiteral{ + Token: token.Token{Type: token.INT, Literal: "0"}, + Value: 0, + }, + To: &ast.IntegerLiteral{ + Token: token.Token{Type: token.INT, Literal: "10"}, + Value: 10, + }, + Body: &ast.CommandObject{ + Token: token.Token{Type: token.COMMAND, Literal: "command"}, + Symbol: &ast.Symbol{ + Token: token.Token{Type: token.PLUS, Literal: "+"}, + Value: "+", + }, + Args: &ast.Array{ + Token: token.Token{Type: token.LBRACKET, Literal: "["}, + Elements: []ast.Expression{ + &ast.Symbol{ + Token: token.Token{Type: token.SYMBOL, Literal: "$i"}, + Value: "$i", + }, + &ast.IntegerLiteral{ + Token: token.Token{Type: token.INT, Literal: "1"}, + Value: 1, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := lexer.New(tt.input) + p := New(l) + + program, err := p.ParseProgram() + if err != nil { + t.Fatalf("ParseProgram() error: %v", err) + } + + loopExp, ok := program.(*ast.LoopExpression) + if !ok { + t.Fatalf("exp not *ast.LoopExpression. got=%T", program) + } + if loopExp.String() != tt.expected.String() { + t.Fatalf("loopExp.String() not %q. got=%q", tt.expected.String(), loopExp.String()) + } + }) + } +} + func TestSetExpression(t *testing.T) { tests := []struct { name string From 9f6cb41014665e50d129db1e838cc016ebe2805d Mon Sep 17 00:00:00 2001 From: Jun Nishimura Date: Wed, 2 Oct 2024 00:50:33 +0900 Subject: [PATCH 5/5] evaluate loop expression (#12) --- evaluator/evaluator.go | 36 ++++++++++++++++++++++++++++++++++++ evaluator/evaluator_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 3bb5c1a..4c3c924 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -39,6 +39,8 @@ func Eval(exp ast.Expression, env *object.Environment) object.Object { return value } return env.Set(expt.Name.Value, value) + case *ast.LoopExpression: + return evalLoopExpression(expt, env) default: return newError("unknown expression type: %T", exp) } @@ -162,3 +164,37 @@ func isTruthy(obj object.Object) bool { return true } } + +func evalLoopExpression(le *ast.LoopExpression, env *object.Environment) object.Object { + from := Eval(le.From, env) + if isError(from) { + return from + } + fromValue, ok := from.(*object.Integer) + if !ok { + return newError("from value must be INTEGER, got %s", from.Type()) + } + + to := Eval(le.To, env) + if isError(to) { + return to + } + toValue, ok := to.(*object.Integer) + if !ok { + return newError("to value must be INTEGER, got %s", to.Type()) + } + + var result object.Object + + for i := fromValue.Value; i < toValue.Value; i++ { + env.Set(le.Index.Value, &object.Integer{Value: i}) + evaluated := Eval(le.Body, env) + if isError(evaluated) { + return evaluated + } + + result = evaluated + } + + return result +} diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index cb180ea..bc1487e 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -369,6 +369,40 @@ func TestIfElseExpression(t *testing.T) { } } +func TestLoopExpression(t *testing.T) { + tests := []struct { + name string + input string + expected int64 + }{ + { + name: "loop expression", + input: ` + { + "loop": { + "for": "$i", + "from": 1, + "to": 3, + "do": { + "command": { + "symbol": "+", + "args": [1, "$i"] + } + } + } + }`, + expected: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + evaluated := testEval(t, tt.input) + testIntegerObject(t, evaluated, tt.expected) + }) + } +} + func TestSetExpression(t *testing.T) { tests := []struct { name string