Skip to content

Commit

Permalink
Class support (#11)
Browse files Browse the repository at this point in the history
* fix method calls

* Add support for class properties
  • Loading branch information
sevenreup authored Sep 9, 2024
1 parent 17a7ff1 commit 60c5aa6
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 24 deletions.
17 changes: 17 additions & 0 deletions src/ast/property.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ast

import "github.com/sevenreup/duwa/src/token"

type PropertyExpression struct {
Token token.Token
Left Expression
Property Expression
}

func (pe *PropertyExpression) expressionNode() {}

func (pe *PropertyExpression) TokenLiteral() string { return pe.Token.Literal }

func (pe *PropertyExpression) String() string {
return pe.Left.String() + "." + pe.Property.String()
}
2 changes: 2 additions & 0 deletions src/evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
return function
case *ast.CallExpression:
return evaluateFunctionCall(node, env)
case *ast.PropertyExpression:
return evaluateProperty(node, env)
case *ast.ArrayLiteral:
elements := evalExpressions(node.Elements, env)
if len(elements) == 1 && isError(elements[0]) {
Expand Down
16 changes: 15 additions & 1 deletion src/evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,21 @@ func TestClasses(t *testing.T) {
Munthu maliko = Munthu();
maliko.zaka();
`,
5,
10,
},
{
`
kalasi Munthu {
numbala zaka = 2;
ndondomeko yikaZaka() {
zaka = 10;
}
}
Munthu maliko = Munthu();
maliko.yikaZaka();
maliko.zaka;
`,
10,
},
}

Expand Down
5 changes: 5 additions & 0 deletions src/evaluator/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func applyFunction(tok token.Token, fn object.Object, args []object.Object, env
extendedEnv := extendFunctionEnv(fn, args)
evaluated := Eval(fn.Body, extendedEnv)
return unwrapReturnValue(evaluated)
case *object.Class:
if tok.Literal != fn.Name.TokenLiteral() {
return newError("class name mismatch: expected %s, got %s", fn.Name.TokenLiteral(), tok.Literal)
}
return fn.CreateInstance(tok.Literal, args)
default:
return newError("not a function: %s", fn.Type())
}
Expand Down
41 changes: 38 additions & 3 deletions src/evaluator/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package evaluator
import (
"github.com/sevenreup/duwa/src/ast"
"github.com/sevenreup/duwa/src/object"
"github.com/sevenreup/duwa/src/values"
)

func evaluateMethod(node *ast.MethodExpression, env *object.Environment) object.Object {
Expand All @@ -24,15 +25,49 @@ func evaluateMethod(node *ast.MethodExpression, env *object.Environment) object.
return result
}

switch left.(type) {
switch receiver := left.(type) {
case *object.LibraryModule:
method := node.Method.(*ast.Identifier)
module := left.(*object.LibraryModule)

if function, ok := module.Methods[method.Value]; ok {
if function, ok := receiver.Methods[method.Value]; ok {
return applyFunction(node.Token, function, arguments, env)
}
case *object.Instance:
method := node.Method.(*ast.Identifier)
evaluated := evaluateInstanceMethod(node, receiver, method.Value, arguments)

if isError(evaluated) {
return evaluated
}

return unwrapReturn(evaluated)
}

return result
}

func evaluateInstanceMethod(node *ast.MethodExpression, receiverInstance *object.Instance, name string, arguments []object.Object) object.Object {
method, ok := receiverInstance.Class.Env.Get(name)

if !ok {
return newError("%d:%d:%s: runtime error: undefined method %s for class %s", node.Token.Pos.Line, node.Token.Pos.Column, node.Token.File, name, receiverInstance.Class.Name.Value)
}

if method, ok := method.(*object.Function); ok {
extendedEnv := extendFunctionEnv(method, arguments)
return Eval(method.Body, extendedEnv)
} else {
return newError("not a method: %s", name)
}
}

func unwrapReturn(obj object.Object) object.Object {
switch value := obj.(type) {
case *object.Error:
return obj
case *object.ReturnValue:
return value.Value
}

return values.NULL
}
40 changes: 40 additions & 0 deletions src/evaluator/property.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package evaluator

import (
"github.com/sevenreup/duwa/src/ast"
"github.com/sevenreup/duwa/src/object"
"github.com/sevenreup/duwa/src/values"
)

func evaluateProperty(node *ast.PropertyExpression, env *object.Environment) object.Object {
left := Eval(node.Left, env)

if isError(left) {
return left
}

switch receiver := left.(type) {
case *object.Instance:
return evaluateInstanceProperty(node, receiver)
}

return nil
}

func evaluateInstanceProperty(node *ast.PropertyExpression, instance *object.Instance) object.Object {
property := node.Property.(*ast.Identifier)

if instance.Env.Has(property.Value) {
val, _ := instance.Env.Get(property.Value)
return val
}

if instance.Class.Env.Has(property.Value) {
val, _ := instance.Class.Env.Get(property.Value)
return val
}

// If the property is not found in the instance or the class, return NULL
instance.Env.Set(property.Value, values.NULL)
return values.NULL
}
22 changes: 11 additions & 11 deletions src/object/class.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ func (c *Class) Inspect() string {
return "class " + c.Name.String()
}

func (i *Class) Method(method string, args []Object) (Object, bool) {
switch method {
case "new":
instance := &Instance{Class: i, Env: NewEnclosedEnvironment(i.Env)}
func (c *Class) CreateInstance(method string, args []Object) Object {
instance := &Instance{Class: c, Env: c.Env}

if ok := i.Env.Has("constructor"); ok {
result := instance.Call("constructor", args)
if ok := c.Env.Has("constructor"); ok {
result := instance.Call("constructor", args)

if result != nil && result.Type() == ERROR_OBJ {
return result, false
}
if result != nil && result.Type() == ERROR_OBJ {
return result
}

return instance, true
}

return instance
}

func (i *Class) Method(method string, args []Object) (Object, bool) {
return nil, false
}
10 changes: 5 additions & 5 deletions src/object/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ func (e *Environment) Get(name string) (Object, bool) {
}
func (e *Environment) Set(name string, val Object) Object {
// TODO: Make sure we dont accidentally mutate data that is not in the current scope
// _, ok := e.store[name]
// if !ok && e.outer != nil {
// e.outer.Set(name, val)
// return val
// }
_, ok := e.store[name]
if !ok && e.outer != nil {
e.outer.Set(name, val)
return val
}
e.store[name] = val
return val
}
Expand Down
2 changes: 1 addition & 1 deletion src/parser/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
)

func (p *Parser) parseCallExpression(function ast.Expression) ast.Expression {
exp := &ast.CallExpression{Token: p.curToken, Function: function}
exp := &ast.CallExpression{Token: p.previousToken, Function: function}
exp.Arguments = p.parseCallArguments()
return exp
}
Expand Down
5 changes: 2 additions & 3 deletions src/parser/dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ func (parser *Parser) dotExpression(left ast.Expression) ast.Expression {
return expression
}

// TODO: Add logic for handling properties "class.me"

return nil
// Property
return &ast.PropertyExpression{Token: parser.curToken, Left: left, Property: parser.parseExpression(currentPrecedence)}
}
30 changes: 30 additions & 0 deletions src/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1428,3 +1428,33 @@ func TestInstanceCreation(t *testing.T) {
}
}
}

func TestClassPropertyAccess(t *testing.T) {
input := `Munthu maria = Munthu(); maria.zaka;`
l := lexer.New([]byte(input))
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 2 {
t.Fatalf("program.Statements does not contain 2 statements. got=%d", len(program.Statements))
}
stmt := program.Statements[1]

expression, ok := stmt.(*ast.ExpressionStatement)
if !ok {
t.Fatalf("stmt is not ast.ExpressionStatement. got=%T", stmt)
}
propertyExp, ok := expression.Expression.(*ast.PropertyExpression)

if !ok {
t.Fatalf("stmt.Expression is not ast.PropertyExpression. got=%T", expression.Expression)
}

if !testIdentifier(t, propertyExp.Left, "maria") {
return
}

if !testIdentifier(t, propertyExp.Property, "zaka") {
return
}
}

0 comments on commit 60c5aa6

Please sign in to comment.