Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add loop expression #28

Merged
merged 5 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
34 changes: 34 additions & 0 deletions evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
102 changes: 102 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading