Skip to content

Commit

Permalink
Feature/class traits (#130)
Browse files Browse the repository at this point in the history
* add trait token

* add trait AST

* parse traits

* evaluate traits

* add use token

* working traits ✨

* evaluate properties from traits
  • Loading branch information
kaidesu committed Nov 5, 2023
1 parent 7f7d98c commit 6f451da
Show file tree
Hide file tree
Showing 19 changed files with 283 additions and 22 deletions.
10 changes: 10 additions & 0 deletions ast/trait.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ast

import "ghostlang.org/x/ghost/token"

type Trait struct {
ExpressionNode
Token token.Token
Name *Identifier
Body *Block
}
9 changes: 9 additions & 0 deletions ast/use.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ast

import "ghostlang.org/x/ghost/token"

type Use struct {
ExpressionNode
Token token.Token
Traits []*Identifier
}
6 changes: 5 additions & 1 deletion evaluator/class.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ func evaluateClass(node *ast.Class, scope *object.Scope) object.Object {
classEnvironment := object.NewEnclosedEnvironment(scope.Environment)
classScope := &object.Scope{Environment: classEnvironment, Self: class}

Evaluate(node.Body, classScope)
result := Evaluate(node.Body, classScope)

if isError(result) {
return result
}

scope.Environment.Set(node.Name.Value, class)

Expand Down
4 changes: 4 additions & 0 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ func Evaluate(node ast.Node, scope *object.Scope) object.Object {
return evaluateSwitch(node, scope)
case *ast.Ternary:
return evaluateTernary(node, scope)
case *ast.Trait:
return evaluateTrait(node, scope)
case *ast.This:
return evaluateThis(node, scope)
case *ast.Use:
return evaluateUse(node, scope)
case *ast.While:
return evaluateWhile(node, scope)
}
Expand Down
22 changes: 18 additions & 4 deletions evaluator/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,36 @@ func evaluateInstanceMethod(node *ast.Method, receiver *object.Instance, name st
class := receiver.Class
method, ok := receiver.Class.Environment.Get(name)

// If we dont have a method, loop through the super classes and check them.
// Then check the traits.
if !ok {
for class != nil {
method, ok = class.Environment.Get(name)

if !ok {
class = class.Super

if class == nil {
return object.NewError("%d:%d:%s: runtime error: undefined method %s for class %s", node.Token.Line, node.Token.Column, node.Token.File, name, receiver.Class.Name.Value)
}
} else {
class = nil
}
}
}

// if we dont have a method, check for a trait
if method == nil {
for _, trait := range receiver.Class.Traits {
method, ok = trait.Environment.Get(name)

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

// if we still dont have a method, return an error
if method == nil {
return object.NewError("%d:%d:%s: runtime error: undefined method %s for class %s", node.Token.Line, node.Token.Column, node.Token.File, name, receiver.Class.Name.Value)
}

switch method := method.(type) {
case *object.Function:
env := createFunctionEnvironment(method, arguments)
Expand Down
44 changes: 34 additions & 10 deletions evaluator/property.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,7 @@ func evaluateProperty(node *ast.Property, scope *object.Scope) object.Object {

switch left.(type) {
case *object.Instance:
property := node.Property.(*ast.Identifier)
instance := left.(*object.Instance)

if !instance.Environment.Has(property.Value) {
instance.Environment.Set(property.Value, value.NULL)
}

val, _ := instance.Environment.Get(property.Value)

return val
return evaluateInstanceProperty(left, node)
case *object.LibraryModule:
property := node.Property.(*ast.Identifier)
module := left.(*object.LibraryModule)
Expand All @@ -49,3 +40,36 @@ func evaluateProperty(node *ast.Property, scope *object.Scope) object.Object {

return nil
}

func evaluateInstanceProperty(left object.Object, node *ast.Property) object.Object {
var val object.Object

instance := left.(*object.Instance)
property := node.Property.(*ast.Identifier)

if instance.Environment.Has(property.Value) {
val, _ = instance.Environment.Get(property.Value)

return val
}

if instance.Class.Environment.Has(property.Value) {
val, _ = instance.Class.Environment.Get(property.Value)

return val
}

for _, trait := range instance.Class.Traits {
if trait.Environment.Has(property.Value) {
val, _ = trait.Environment.Get(property.Value)

return val
}
}

instance.Environment.Set(property.Value, value.NULL)

val, _ = instance.Environment.Get(property.Value)

return val
}
28 changes: 28 additions & 0 deletions evaluator/trait.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package evaluator

import (
"ghostlang.org/x/ghost/ast"
"ghostlang.org/x/ghost/object"
)

func evaluateTrait(node *ast.Trait, scope *object.Scope) object.Object {
trait := &object.Trait{
Name: node.Name,
Scope: scope,
Environment: object.NewEnvironment(),
}

// Create a new scope for this trait
trait.Environment = object.NewEnclosedEnvironment(scope.Environment)
traitScope := &object.Scope{Environment: trait.Environment, Self: trait}

result := Evaluate(node.Body, traitScope)

if isError(result) {
return result
}

scope.Environment.Set(node.Name.Value, trait)

return trait
}
37 changes: 37 additions & 0 deletions evaluator/use.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package evaluator

import (
"ghostlang.org/x/ghost/ast"
"ghostlang.org/x/ghost/object"
)

func evaluateUse(node *ast.Use, scope *object.Scope) object.Object {
// check that the scope is a class
class, ok := scope.Self.(*object.Class)

if !ok {
return object.NewError("%d:%d:%s: runtime error: use statement can only be used in a class", node.Token.Line, node.Token.Column, node.Token.File)
}

var traits []*object.Trait

for _, trait := range node.Traits {
if !scope.Environment.Has(trait.Value) {
return object.NewError("%d:%d:%s: runtime error: trait '%s' is not defined", trait.Token.Line, trait.Token.Column, trait.Token.File, trait.Value)
}

identifier, _ := scope.Environment.Get(trait.Value)

t, ok := identifier.(*object.Trait)

if !ok {
return object.NewError("%d:%d:%s: runtime error: referenced identifier in use not a trait, got=%T", trait.Token.Line, trait.Token.Column, trait.Token.File, trait)
}

traits = append(traits, t)
}

class.Traits = traits

return nil
}
7 changes: 7 additions & 0 deletions examples/foo.ghost
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
trait Foo {
message = 'Hello World!!!!'

function bar() {
console.log(this.message)
}
}
20 changes: 15 additions & 5 deletions examples/scratch.ghost
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
x = 0
list = [0, 1, 2, 3]
import Foo from 'foo'

for (i = 0; i < list.length(); i++) {
print(i)
}
class Lorem {
use Foo

function hello() {
console.log('hello')
}
}

lorem = Lorem.new()

lorem.hello()
lorem.bar()

console.log('done.')
1 change: 1 addition & 0 deletions object/class.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Class struct {
Scope *Scope
Environment *Environment
Super *Class
Traits []*Trait
}

// String represents the class object's value as a string.
Expand Down
27 changes: 27 additions & 0 deletions object/trait.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package object

import "ghostlang.org/x/ghost/ast"

const TRAIT = "TRAIT"

// Trait objects consist of a body and an environment.
type Trait struct {
Name *ast.Identifier
Scope *Scope
Environment *Environment
}

// String represents the class object's value as a string.
func (trait *Trait) String() string {
return "trait"
}

// Type returns the trait object type.
func (trait *Trait) Type() Type {
return TRAIT
}

// Method defines the set of methods available on trait objects.
func (trait *Trait) Method(method string, args []Object) (Object, bool) {
return nil, false
}
4 changes: 3 additions & 1 deletion parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func New(scanner *scanner.Scanner) *Parser {
parser.registerPrefix(token.WHILE, parser.whileExpression)
parser.registerPrefix(token.FOR, parser.forExpression)
parser.registerPrefix(token.CLASS, parser.classStatement)
parser.registerPrefix(token.TRAIT, parser.traitStatement)
parser.registerPrefix(token.USE, parser.useExpression)
parser.registerPrefix(token.THIS, parser.thisExpression)
parser.registerPrefix(token.IMPORT, parser.importStatement)
parser.registerPrefix(token.SWITCH, parser.switchStatement)
Expand Down Expand Up @@ -202,7 +204,7 @@ func (parser *Parser) isAtEnd() bool {

func (parser *Parser) nextError(tt token.Type) {
message := fmt.Sprintf(
"%d:%d: syntax error: expected next token to be %s, got: %s instead", parser.nextToken.Line, parser.nextToken.Column, tt, parser.nextToken.Type,
"%d:%d: syntax error: expected next token to be `%s`, got: `%s` instead", parser.nextToken.Line, parser.nextToken.Column, tt, parser.nextToken.Type,
)

parser.errors = append(parser.errors, message)
Expand Down
36 changes: 36 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,42 @@ func TestSwitchStatementsWithMultipleDefaults(t *testing.T) {
}
}

func TestTraitExpressions(t *testing.T) {
input := `trait Foo {
//
}`

scanner := scanner.New(input, "test.ghost")
parser := New(scanner)
program := parser.Parse()

failIfParserHasErrors(t, parser)

if len(program.Statements) != 1 {
t.Fatalf("program.Statements does not contain 1 statement. got=%d", len(program.Statements))
}

statement, ok := program.Statements[0].(*ast.Expression)

if !ok {
t.Fatalf("program.Statements[0] is not ast.Expression. got=%T", program.Statements[0])
}

expression, ok := statement.Expression.(*ast.Trait)

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

if expression.Name.Value != "Foo" {
t.Fatalf("expression.Name is not 'Foo'. got=%s", expression.Name.Value)
}

if len(expression.Body.Statements) != 0 {
t.Fatalf("expression.Body.Statements does not contain 0 statements. got=%d", len(expression.Body.Statements))
}
}

// =============================================================================
// Helper methods

Expand Down
22 changes: 22 additions & 0 deletions parser/trait.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package parser

import (
"ghostlang.org/x/ghost/ast"
"ghostlang.org/x/ghost/token"
)

func (parser *Parser) traitStatement() ast.ExpressionNode {
trait := &ast.Trait{Token: parser.currentToken}

parser.readToken()

trait.Name = &ast.Identifier{Token: parser.currentToken, Value: parser.currentToken.Lexeme}

if !parser.expectNextTokenIs(token.LEFTBRACE) {
return nil
}

trait.Body = parser.blockStatement()

return trait
}
20 changes: 20 additions & 0 deletions parser/use.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package parser

import (
"ghostlang.org/x/ghost/ast"
"ghostlang.org/x/ghost/token"
)

func (parser *Parser) useExpression() ast.ExpressionNode {
use := &ast.Use{Token: parser.currentToken}

if !parser.expectNextTokenIs(token.IDENTIFIER) {
return nil
}

identifier := &ast.Identifier{Token: parser.currentToken, Value: parser.currentToken.Lexeme}

use.Traits = append(use.Traits, identifier)

return use
}
2 changes: 2 additions & 0 deletions scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ var keywords = map[string]token.Type{
"super": token.SUPER,
"switch": token.SWITCH,
"this": token.THIS,
"trait": token.TRAIT,
"true": token.TRUE,
"use": token.USE,
"while": token.WHILE,
}

Expand Down

0 comments on commit 6f451da

Please sign in to comment.