Skip to content

Commit

Permalink
feat: add unquote macro
Browse files Browse the repository at this point in the history
  • Loading branch information
grantwforsythe committed Sep 19, 2024
1 parent 68ebf64 commit a4ae9fd
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 3 deletions.
61 changes: 61 additions & 0 deletions pkg/ast/modify.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ type ModifierFunc func(Node) Node

// Apply a modifer to an AST node.
func Modify(node Node, modifier ModifierFunc) Node {
// TODO: Replace underscores with proper error handling

switch node := node.(type) {
case *Program:
for i, statement := range node.Statements {
Expand All @@ -12,6 +14,65 @@ func Modify(node Node, modifier ModifierFunc) Node {

case *ExpressionStatement:
node.Expression, _ = Modify(node.Expression, modifier).(Expression)

case *CallExpression:
for i, argument := range node.Arguments {
node.Arguments[i], _ = Modify(argument, modifier).(Expression)

Check warning on line 20 in pkg/ast/modify.go

View check run for this annotation

Codecov / codecov/patch

pkg/ast/modify.go#L18-L20

Added lines #L18 - L20 were not covered by tests
}

case *InfixExpression:
node.Left, _ = Modify(node.Left, modifier).(Expression)
node.Right, _ = Modify(node.Right, modifier).(Expression)

case *PrefixExpression:
node.Right, _ = Modify(node.Right, modifier).(Expression)

case *IndexEpression:
node.Left, _ = Modify(node.Left, modifier).(Expression)
node.Index, _ = Modify(node.Index, modifier).(Expression)

case *IfExpression:
node.Condition, _ = Modify(node.Condition, modifier).(Expression)
node.Consequence, _ = Modify(node.Consequence, modifier).(*BlockStatement)
if node.Alternative != nil {
node.Alternative, _ = Modify(node.Alternative, modifier).(*BlockStatement)
}

case *BlockStatement:
for i, statement := range node.Statements {
node.Statements[i], _ = Modify(statement, modifier).(Statement)
}

case *ReturnStatement:
node.ReturnValue, _ = Modify(node.ReturnValue, modifier).(Expression)

case *LetStatement:
node.Value, _ = Modify(node.Value, modifier).(Expression)

case *FunctionLiteral:
if node.Parameters != nil {
for i, parameter := range node.Parameters {
node.Parameters[i], _ = Modify(parameter, modifier).(*Identifier)

Check warning on line 55 in pkg/ast/modify.go

View check run for this annotation

Codecov / codecov/patch

pkg/ast/modify.go#L54-L55

Added lines #L54 - L55 were not covered by tests
}
}
node.Body, _ = Modify(node.Body, modifier).(*BlockStatement)

case *ArrayLiteral:
for i, element := range node.Elements {
node.Elements[i], _ = Modify(element, modifier).(Expression)
}

case *HashLiteral:
modifiedPairs := make(map[Expression]Expression)

for key, value := range node.Pairs {
modifiedKey, _ := Modify(key, modifier).(Expression)
modifiedValue, _ := Modify(value, modifier).(Expression)

modifiedPairs[modifiedKey] = modifiedValue
}

node.Pairs = modifiedPairs
}

return modifier(node)
Expand Down
86 changes: 86 additions & 0 deletions pkg/ast/modify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,72 @@ func TestModify(t *testing.T) {
},
},
},
{
&InfixExpression{Left: one(), Operator: "+", Right: two()},
&InfixExpression{Left: two(), Operator: "+", Right: two()},
},
{
&InfixExpression{Left: two(), Operator: "+", Right: one()},
&InfixExpression{Left: two(), Operator: "+", Right: two()},
},
{
&PrefixExpression{Operator: "-", Right: one()},
&PrefixExpression{Operator: "-", Right: two()},
},
{
&IndexEpression{Left: one(), Index: one()},
&IndexEpression{Left: two(), Index: two()},
},
{
&IfExpression{
Condition: one(),
Consequence: &BlockStatement{
Statements: []Statement{
&ExpressionStatement{Expression: one()},
},
},
Alternative: &BlockStatement{
Statements: []Statement{
&ExpressionStatement{Expression: one()},
},
},
},
&IfExpression{
Condition: two(),
Consequence: &BlockStatement{
Statements: []Statement{
&ExpressionStatement{Expression: two()},
},
},
Alternative: &BlockStatement{
Statements: []Statement{
&ExpressionStatement{Expression: two()},
},
},
},
},
{&ReturnStatement{ReturnValue: one()}, &ReturnStatement{ReturnValue: two()}},
{&LetStatement{Value: one()}, &LetStatement{Value: two()}},
{
&FunctionLiteral{
Body: &BlockStatement{
Statements: []Statement{
&ExpressionStatement{Expression: one()},
},
},
},
&FunctionLiteral{
Body: &BlockStatement{
Statements: []Statement{
&ExpressionStatement{Expression: two()},
},
},
},
},
{
&ArrayLiteral{Elements: []Expression{one(), two()}},
&ArrayLiteral{Elements: []Expression{two(), two()}},
},
}

for _, test := range tests {
Expand All @@ -49,4 +115,24 @@ func TestModify(t *testing.T) {
t.Errorf("not equal. got=%#v, expected=%#v", modified, test.expected)
}
}

hashLiteral := &HashLiteral{
Pairs: map[Expression]Expression{
one(): one(),
one(): one(),
},
}

Modify(hashLiteral, turnOneIntoTwo)

for key, val := range hashLiteral.Pairs {
key, _ := key.(*IntegerLiteral)
if key.Value != 2 {
t.Errorf("value is not %d, got=%d", 2, key.Value)
}
val, _ := val.(*IntegerLiteral)
if val.Value != 2 {
t.Errorf("value is not %d, got=%d", 2, val.Value)
}
}
}
2 changes: 1 addition & 1 deletion pkg/evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
// Skip evaluation of argument when calling `quote`
// Quote only accepts one argument
if node.Function.TokenLiteral() == "quote" {
return &object.Quote{Node: node.Arguments[0]}
return quote(node.Arguments[0], env)
}

fn := Eval(node.Function, env)
Expand Down
44 changes: 44 additions & 0 deletions pkg/evaluator/quote_unqote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package evaluator

import (
"github.com/grantwforsythe/monkeylang/pkg/ast"
"github.com/grantwforsythe/monkeylang/pkg/object"
)

func quote(node ast.Node, env *object.Environment) object.Object {
return &object.Quote{Node: evalUnquoteCalls(node, env)}
}

func evalUnquoteCalls(quote ast.Node, env *object.Environment) ast.Node {
return ast.Modify(quote, func(node ast.Node) ast.Node {
if !isUnquoteCall(node) {
return node
}

// We make this same assertion in isUnquoteCall so no need to do it again
call, _ := node.(*ast.CallExpression)

// Can only handle one expression
if len(call.Arguments) != 1 {
return node

Check warning on line 23 in pkg/evaluator/quote_unqote.go

View check run for this annotation

Codecov / codecov/patch

pkg/evaluator/quote_unqote.go#L23

Added line #L23 was not covered by tests
}

eval := Eval(call.Arguments[0], env)

convertible, ok := eval.(object.Convertible)
if !ok {
return node

Check warning on line 30 in pkg/evaluator/quote_unqote.go

View check run for this annotation

Codecov / codecov/patch

pkg/evaluator/quote_unqote.go#L30

Added line #L30 was not covered by tests
}

return convertible.ToNode()
})
}

// Check if a node is a call expression for unquote.
func isUnquoteCall(node ast.Node) bool {
fn, ok := node.(*ast.CallExpression)
if !ok {
return false
}
return fn.Function.TokenLiteral() == "unquote"
}
9 changes: 7 additions & 2 deletions pkg/evaluator/quote_unquote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ func TestQuoteUnqote(t *testing.T) {
}{
{"quote(unquote(4))", "4"},
{"quote(unquote(4 + 4))", "8"},
{"quote(8 + unquote(4 + 4))", "8 + 8"},
{"quote(8 + unquote(4 + 4))", "(8 + 8)"},
{`let value = 8; quote(8 + unquote(value))`, "(8 + 8)"},
}

for _, test := range tests {
Expand All @@ -62,7 +63,11 @@ func TestQuoteUnqote(t *testing.T) {
}

if quote.Node.String() != test.expected {
t.Fatalf("quote.Node.String() is not equal to 5. got=%s", quote.Node.String())
t.Fatalf(
"quote.Node.String() is not equal to %s. got=%s",
test.expected,
quote.Node.String(),
)
}
}
}
14 changes: 14 additions & 0 deletions pkg/object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"bytes"
"fmt"
"hash/fnv"
"strconv"
"strings"

"github.com/grantwforsythe/monkeylang/pkg/ast"
"github.com/grantwforsythe/monkeylang/pkg/token"
)

type ObjectType string
Expand All @@ -32,6 +34,12 @@ type Object interface {
Inspect() string
}

// Represents an Object that is convertible
type Convertible interface {
// Convert object to resulting AST Node
ToNode() ast.Node
}

// Represents a hash key for an object
type HashKey struct {
Type ObjectType
Expand All @@ -54,6 +62,12 @@ func (i *Integer) Inspect() string { return fmt.Sprintf("%d", i.Value) }
func (i *Integer) HashKey() HashKey {
return HashKey{Type: i.Type(), Value: uint64(i.Value)}
}
func (i *Integer) ToNode() ast.Node {
return &ast.IntegerLiteral{
Token: token.Token{Type: token.INT, Literal: strconv.FormatInt(i.Value, 10)},
Value: i.Value,

Check warning on line 68 in pkg/object/object.go

View check run for this annotation

Codecov / codecov/patch

pkg/object/object.go#L65-L68

Added lines #L65 - L68 were not covered by tests
}
}

type Boolean struct {
Value bool
Expand Down

0 comments on commit a4ae9fd

Please sign in to comment.