From 306a3afe55732ba1dd22ba0d0750e93689a47a61 Mon Sep 17 00:00:00 2001 From: Christopher Seven Phiri Date: Fri, 23 Aug 2024 21:55:41 +0200 Subject: [PATCH] Init class support (#8) * Update parser_test.go * can parse classes * Add instances * init tests * update --- examples/classes.ny | 8 ++++ src/ast/class.go | 31 ++++++++++++ src/chewa/chewa.go | 46 ++++++++++++++++++ src/evaluator/class.go | 25 ++++++++++ src/evaluator/evaluator.go | 4 ++ src/evaluator/evaluator_test.go | 38 ++++++++++++++- src/main.go | 22 ++------- src/object/class.go | 35 ++++++++++++++ src/object/environment.go | 16 +++++-- src/object/error.go | 6 +++ src/object/instance.go | 44 +++++++++++++++++ src/object/object.go | 11 ++++- src/parser/class.go | 24 ++++++++++ src/parser/parser.go | 1 + src/parser/parser_test.go | 85 +++++++++++++++++++++++++++++++++ src/parser/statement.go | 2 +- src/repl/repl.go | 1 + src/token/token.go | 2 + 18 files changed, 374 insertions(+), 27 deletions(-) create mode 100644 examples/classes.ny create mode 100644 src/ast/class.go create mode 100644 src/chewa/chewa.go create mode 100644 src/evaluator/class.go create mode 100644 src/object/class.go create mode 100644 src/object/instance.go create mode 100644 src/parser/class.go diff --git a/examples/classes.ny b/examples/classes.ny new file mode 100644 index 0000000..6a17339 --- /dev/null +++ b/examples/classes.ny @@ -0,0 +1,8 @@ +kalasi Munthu { + ndondomeko ndiNdani() { + console.lemba("Mariya Malizeni"); + } +} + +Munthu munthu = Munthu(); +munthu.ndiNdani(); \ No newline at end of file diff --git a/src/ast/class.go b/src/ast/class.go new file mode 100644 index 0000000..b9d159d --- /dev/null +++ b/src/ast/class.go @@ -0,0 +1,31 @@ +package ast + +import ( + "bytes" + + "github.com/sevenreup/chewa/src/token" +) + +type ClassStatement struct { + Expression + Token token.Token + Name *Identifier + Super *Identifier + Body *BlockStatement +} + +func (class *ClassStatement) expressionNode() {} +func (class *ClassStatement) TokenLiteral() string { return class.Token.Literal } +func (class *ClassStatement) String() string { + var out bytes.Buffer + out.WriteString("ndondomeko ") + out.WriteString(class.Name.String()) + if class.Super != nil { + out.WriteString(" ndi ") + out.WriteString(class.Super.String()) + } + out.WriteString(" {\n") + out.WriteString(class.Body.String()) + out.WriteString("\n}") + return out.String() +} diff --git a/src/chewa/chewa.go b/src/chewa/chewa.go new file mode 100644 index 0000000..5e03044 --- /dev/null +++ b/src/chewa/chewa.go @@ -0,0 +1,46 @@ +package chewa + +import ( + "log" + "os" + + "github.com/sevenreup/chewa/src/evaluator" + "github.com/sevenreup/chewa/src/object" + "github.com/sevenreup/chewa/src/utils" + + "github.com/sevenreup/chewa/src/lexer" + "github.com/sevenreup/chewa/src/parser" +) + +type Chewa struct { + file string + Environment *object.Environment +} + +func New(file string) *Chewa { + chewa := &Chewa{ + file: file, + Environment: object.NewEnvironment(), + } + chewa.registerEvaluator() + return chewa +} + +func (c *Chewa) Run() { + file, err := os.ReadFile(c.file) + if err != nil { + log.Fatal(err) + } + l := lexer.New(file) + p := parser.New(l) + env := object.NewEnvironment() + program := p.ParseProgram() + if len(p.Errors()) != 0 { + utils.PrintParserErrors(os.Stdout, p.Errors()) + } + evaluator.Eval(program, env) +} + +func (c *Chewa) registerEvaluator() { + object.RegisterEvaluator(evaluator.Eval) +} diff --git a/src/evaluator/class.go b/src/evaluator/class.go new file mode 100644 index 0000000..02c9f28 --- /dev/null +++ b/src/evaluator/class.go @@ -0,0 +1,25 @@ +package evaluator + +import ( + "github.com/sevenreup/chewa/src/ast" + "github.com/sevenreup/chewa/src/object" +) + +func evaluateClass(node *ast.ClassStatement, env *object.Environment) object.Object { + classEnv := object.NewEnclosedEnvironment(env) + + class := &object.Class{ + Name: node.Name, + Env: classEnv, + } + + result := Eval(node.Body, classEnv) + + if isError(result) { + return result + } + + env.Set(class.Name.Value, class) + + return class +} diff --git a/src/evaluator/evaluator.go b/src/evaluator/evaluator.go index e099683..f3a8fb6 100644 --- a/src/evaluator/evaluator.go +++ b/src/evaluator/evaluator.go @@ -8,6 +8,8 @@ import ( "github.com/sevenreup/chewa/src/values" ) +type Evaluator func(node ast.Node, env *object.Environment) object.Object + func Eval(node ast.Node, env *object.Environment) object.Object { switch node := node.(type) { case *ast.Program: @@ -73,6 +75,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object { return &object.String{Value: node.Value} case *ast.PostfixExpression: return evaluatePostfix(node, env) + case *ast.ClassStatement: + return evaluateClass(node, env) } return nil } diff --git a/src/evaluator/evaluator_test.go b/src/evaluator/evaluator_test.go index cb4d90a..9d4c009 100644 --- a/src/evaluator/evaluator_test.go +++ b/src/evaluator/evaluator_test.go @@ -14,6 +14,10 @@ func testEval(input string) object.Object { p := parser.New(l) program := p.ParseProgram() env := object.NewEnvironment() + + evaluatorInstance := Eval + object.RegisterEvaluator(evaluatorInstance) + return Eval(program, env) } @@ -619,8 +623,8 @@ func TestHashLiterals(t *testing.T) { (&object.String{Value: "two"}).MapKey(): 2, (&object.String{Value: "three"}).MapKey(): 3, (&object.Integer{Value: decimal.NewFromInt(4)}).MapKey(): 4, - values.TRUE.MapKey(): 5, - values.FALSE.MapKey(): 6, + values.TRUE.MapKey(): 5, + values.FALSE.MapKey(): 6, } if len(result.Pairs) != len(expected) { t.Fatalf("Hash has wrong num of pairs. got=%d", len(result.Pairs)) @@ -678,3 +682,33 @@ func TestHashIndexExpressions(t *testing.T) { } } } + +func TestClasses(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + { + ` + kalasi Munthu { + ndondomeko zaka() { + bweza 10; + } + } + Munthu maliko = Munthu(); + maliko.zaka(); + `, + 5, + }, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + integer, ok := tt.expected.(int) + if ok { + testIntegerObject(t, evaluated, decimal.NewFromInt(int64(integer))) + } else { + testNullObject(t, evaluated) + } + } +} diff --git a/src/main.go b/src/main.go index ec48c4d..e4ccc44 100644 --- a/src/main.go +++ b/src/main.go @@ -3,14 +3,8 @@ package main import ( "flag" "log" - "os" - "github.com/sevenreup/chewa/src/evaluator" - "github.com/sevenreup/chewa/src/object" - "github.com/sevenreup/chewa/src/utils" - - "github.com/sevenreup/chewa/src/lexer" - "github.com/sevenreup/chewa/src/parser" + "github.com/sevenreup/chewa/src/chewa" ) var ( @@ -28,16 +22,6 @@ func main() { log.Fatal("Please provide a file to run") } - file, err := os.ReadFile(file) - if err != nil { - log.Fatal(err) - } - l := lexer.New(file) - p := parser.New(l) - env := object.NewEnvironment() - program := p.ParseProgram() - if len(p.Errors()) != 0 { - utils.PrintParserErrors(os.Stdout, p.Errors()) - } - evaluator.Eval(program, env) + chewa := chewa.New(file) + chewa.Run() } diff --git a/src/object/class.go b/src/object/class.go new file mode 100644 index 0000000..4543b1f --- /dev/null +++ b/src/object/class.go @@ -0,0 +1,35 @@ +package object + +import "github.com/sevenreup/chewa/src/ast" + +const CLASS_OBJ = "CLASS" + +type Class struct { + Object + Name *ast.Identifier + Env *Environment +} + +func (c *Class) Type() ObjectType { return CLASS_OBJ } + +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)} + + if ok := i.Env.Has("constructor"); ok { + result := instance.Call("constructor", args) + + if result != nil && result.Type() == ERROR_OBJ { + return result, false + } + } + + return instance, true + } + return nil, false +} diff --git a/src/object/environment.go b/src/object/environment.go index d30be88..3c76da3 100644 --- a/src/object/environment.go +++ b/src/object/environment.go @@ -24,13 +24,21 @@ 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 + // } + e.store[name] = val + return val +} + +func (e *Environment) Has(name string) bool { _, ok := e.store[name] if !ok && e.outer != nil { - e.outer.Set(name, val) - return val + return e.outer.Has(name) } - e.store[name] = val - return val + return ok } func (e *Environment) Delete(name string) { diff --git a/src/object/error.go b/src/object/error.go index 2e3f1ef..735d367 100644 --- a/src/object/error.go +++ b/src/object/error.go @@ -1,5 +1,7 @@ package object +import "fmt" + const ERROR_OBJ = "ERROR" type Error struct { @@ -15,3 +17,7 @@ func (i *Error) Method(method string, args []Object) (Object, bool) { //TODO implement me panic("implement me") } + +func NewError(format string, a ...interface{}) *Error { + return &Error{Message: fmt.Sprintf(format, a...)} +} diff --git a/src/object/instance.go b/src/object/instance.go new file mode 100644 index 0000000..e1a142b --- /dev/null +++ b/src/object/instance.go @@ -0,0 +1,44 @@ +package object + +const INSTANCE_OBJ = "INSTANCE" + +type Instance struct { + Class *Class + Env *Environment +} + +func (i *Instance) Type() ObjectType { return INSTANCE_OBJ } + +func (i *Instance) Inspect() string { + return i.Class.Name.String() +} + +func (i *Instance) Method(method string, args []Object) (Object, bool) { + return nil, true +} + +func (i *Instance) Call(method string, args []Object) Object { + function, ok := i.Env.Get(method) + if !ok { + return NewError("undefined method %s for %s", method, i.Class.Name.String()) + } + methodFunction, ok := function.(*Function) + if !ok { + return NewError("undefined method %s for %s", method, i.Class.Name.String()) + } + + methodEnv := createNewMethodInstanceEnvironment(methodFunction, args) + return evaluator(methodFunction.Body, methodEnv) +} + +func createNewMethodInstanceEnvironment(method *Function, args []Object) *Environment { + env := NewEnclosedEnvironment(method.Env) + + for i, param := range method.Parameters { + if len(args) > i { + env.Set(param.Value, args[i]) + } + } + + return env +} diff --git a/src/object/object.go b/src/object/object.go index 360d561..c4a6223 100644 --- a/src/object/object.go +++ b/src/object/object.go @@ -1,6 +1,11 @@ package object -import "github.com/sevenreup/chewa/src/token" +import ( + "github.com/sevenreup/chewa/src/ast" + "github.com/sevenreup/chewa/src/token" +) + +var evaluator func(node ast.Node, env *Environment) Object type ObjectType string @@ -25,3 +30,7 @@ type HasMethods interface { type GoFunction func(env *Environment, tok token.Token, args ...Object) Object type GoProperty func(env *Environment, tok token.Token) Object + +func RegisterEvaluator(e func(node ast.Node, env *Environment) Object) { + evaluator = e +} diff --git a/src/parser/class.go b/src/parser/class.go new file mode 100644 index 0000000..0cd09d9 --- /dev/null +++ b/src/parser/class.go @@ -0,0 +1,24 @@ +package parser + +import ( + "github.com/sevenreup/chewa/src/ast" + "github.com/sevenreup/chewa/src/token" +) + +func (p *Parser) classStatement() ast.Expression { + class := &ast.ClassStatement{Token: p.curToken} + + p.nextToken() + + class.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} + + // TODO: Implement inheritance + + if !p.expectPeek(token.OPENING_BRACE) { + return nil + } + + class.Body = p.parseBlockStatement() + + return class +} diff --git a/src/parser/parser.go b/src/parser/parser.go index 1b5142a..113491f 100644 --- a/src/parser/parser.go +++ b/src/parser/parser.go @@ -85,6 +85,7 @@ func New(l *lexer.Lexer) *Parser { p.registerPrefix(token.FOR, p.parseForExpression) p.registerPrefix(token.WHILE, p.parseWhileExpression) p.registerPrefix(token.FUNCTION, p.parseFunctionLiteral) + p.registerPrefix(token.CLASS, p.classStatement) p.registerPrefix(token.OPENING_BRACE, p.mapLiteral) p.registerPrefix(token.OPENING_PAREN, p.parseGroupedExpression) diff --git a/src/parser/parser_test.go b/src/parser/parser_test.go index 80e443c..c92c23b 100644 --- a/src/parser/parser_test.go +++ b/src/parser/parser_test.go @@ -1343,3 +1343,88 @@ func TestParsingHashLiteralsWithExpressions(t *testing.T) { testFunc(value) } } + +func TestParsingClassExpressions(t *testing.T) { + input := ` + kalasi Munthu { + nambala age = 5; + }` + l := lexer.New([]byte(input)) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + stmt := program.Statements[0].(*ast.ExpressionStatement) + classStatement := stmt.Expression.(*ast.ClassStatement) + if classStatement.Name.Value != "Munthu" { + t.Fatalf("stmt.Name.Value not 'Munthu'. got=%q", classStatement.Name.Value) + } + if len(classStatement.Body.Statements) != 1 { + t.Fatalf("stmt.Body.Statements does not contain 1 statements. got=%d\n", + len(classStatement.Body.Statements)) + } + bodyStmt, ok := classStatement.Body.Statements[0].(*ast.VariableDeclarationStatement) + if !ok { + t.Fatalf("stmt.Body.Statements[0] is not ast.VariableDeclarationStatement. got=%T", + classStatement.Body.Statements[0]) + } + if bodyStmt.Identifier.Value != "age" { + t.Fatalf("bodyStmt.Identifier.Value not 'age'. got=%s", bodyStmt.Identifier.Value) + } +} + +func TestInstanceCreation(t *testing.T) { + tests := []struct { + input string + expectedIdentifier string + expectedType string + }{ + {"Munthu maria = Munthu();", "maria", "Munthu"}, + {"maria = Munthu();", "maria", "Munthu"}, + } + + for _, tt := range tests { + l := lexer.New([]byte(tt.input)) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain 1 statements. got=%d", + len(program.Statements)) + } + stmt := program.Statements[0] + switch statement := stmt.(type) { + case *ast.AssigmentStatement: + { + if identifier, ok := statement.Identifier.(*ast.Identifier); ok { + if identifier.Value != tt.expectedIdentifier { + t.Errorf("AssigmentStatement.Name.Value not '%s'. got=%s", tt.expectedIdentifier, identifier.Value) + continue + } + } else { + t.Errorf("AssigmentStatement.Name not *ast.Identifier. got=%T", statement.Identifier) + continue + } + if callExpression, ok := statement.Value.(*ast.CallExpression); ok { + if callExpression.Function.String() != tt.expectedType { + t.Errorf("CallExpression.Function not '%s'. got=%s", tt.expectedType, callExpression.Function.String()) + continue + } + } else { + t.Errorf("AssigmentStatement.Value not *ast.CallExpression. got=%T", statement.Value) + continue + } + } + case *ast.VariableDeclarationStatement: + { + if statement.Type.Literal != tt.expectedType { + t.Errorf("s.TokenLiteral not '%s'. got=%q", tt.expectedType, statement.Type.Literal) + continue + } + if statement.Identifier.Value != tt.expectedIdentifier { + t.Errorf("VariableDeclarationStatement.Name.Value not '%s'. got=%s", tt.expectedIdentifier, statement.Identifier.Value) + continue + } + } + } + } +} diff --git a/src/parser/statement.go b/src/parser/statement.go index ae6da19..d1df44f 100644 --- a/src/parser/statement.go +++ b/src/parser/statement.go @@ -10,7 +10,7 @@ func (p *Parser) parseStatement() ast.Statement { return p.parseReturnStatement() } - if token.LookupVariableType(p.curToken.Type) != "" { + if token.LookupVariableType(p.curToken.Type) != "" || (p.curToken.Type == token.IDENT && p.peekTokenIs(token.IDENT)) { return p.parseVariableDeclarationStatement() } diff --git a/src/repl/repl.go b/src/repl/repl.go index 8b78328..7afaa65 100644 --- a/src/repl/repl.go +++ b/src/repl/repl.go @@ -15,6 +15,7 @@ import ( const PROMPT = ">> " func Start(in io.Reader, out io.Writer) { + object.RegisterEvaluator(evaluator.Eval) scanner := bufio.NewScanner(in) env := object.NewEnvironment() for { diff --git a/src/token/token.go b/src/token/token.go index 3daaac0..0272162 100644 --- a/src/token/token.go +++ b/src/token/token.go @@ -63,6 +63,7 @@ const ( FOR = "FOR" WHILE = "WHILE" MAP = "MAP" + CLASS = "CLASS" ) type Position struct { @@ -89,6 +90,7 @@ var keywords = map[string]TokenType{ "za": FOR, "pamene": WHILE, "mgwirizano": MAP, + "kalasi": CLASS, } var variableTypes = map[TokenType]TokenType{