Skip to content

Commit 6f451da

Browse files
authored
Feature/class traits (#130)
* add trait token * add trait AST * parse traits * evaluate traits * add use token * working traits ✨ * evaluate properties from traits
1 parent 7f7d98c commit 6f451da

File tree

19 files changed

+283
-22
lines changed

19 files changed

+283
-22
lines changed

ast/trait.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package ast
2+
3+
import "ghostlang.org/x/ghost/token"
4+
5+
type Trait struct {
6+
ExpressionNode
7+
Token token.Token
8+
Name *Identifier
9+
Body *Block
10+
}

ast/use.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package ast
2+
3+
import "ghostlang.org/x/ghost/token"
4+
5+
type Use struct {
6+
ExpressionNode
7+
Token token.Token
8+
Traits []*Identifier
9+
}

evaluator/class.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ func evaluateClass(node *ast.Class, scope *object.Scope) object.Object {
3434
classEnvironment := object.NewEnclosedEnvironment(scope.Environment)
3535
classScope := &object.Scope{Environment: classEnvironment, Self: class}
3636

37-
Evaluate(node.Body, classScope)
37+
result := Evaluate(node.Body, classScope)
38+
39+
if isError(result) {
40+
return result
41+
}
3842

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

evaluator/evaluator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,12 @@ func Evaluate(node ast.Node, scope *object.Scope) object.Object {
7676
return evaluateSwitch(node, scope)
7777
case *ast.Ternary:
7878
return evaluateTernary(node, scope)
79+
case *ast.Trait:
80+
return evaluateTrait(node, scope)
7981
case *ast.This:
8082
return evaluateThis(node, scope)
83+
case *ast.Use:
84+
return evaluateUse(node, scope)
8185
case *ast.While:
8286
return evaluateWhile(node, scope)
8387
}

evaluator/method.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,36 @@ func evaluateInstanceMethod(node *ast.Method, receiver *object.Instance, name st
6060
class := receiver.Class
6161
method, ok := receiver.Class.Environment.Get(name)
6262

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

6769
if !ok {
6870
class = class.Super
69-
70-
if class == nil {
71-
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)
72-
}
7371
} else {
7472
class = nil
7573
}
7674
}
7775
}
7876

77+
// if we dont have a method, check for a trait
78+
if method == nil {
79+
for _, trait := range receiver.Class.Traits {
80+
method, ok = trait.Environment.Get(name)
81+
82+
if !ok {
83+
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)
84+
}
85+
}
86+
}
87+
88+
// if we still dont have a method, return an error
89+
if method == nil {
90+
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)
91+
}
92+
7993
switch method := method.(type) {
8094
case *object.Function:
8195
env := createFunctionEnvironment(method, arguments)

evaluator/property.go

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,7 @@ func evaluateProperty(node *ast.Property, scope *object.Scope) object.Object {
1515

1616
switch left.(type) {
1717
case *object.Instance:
18-
property := node.Property.(*ast.Identifier)
19-
instance := left.(*object.Instance)
20-
21-
if !instance.Environment.Has(property.Value) {
22-
instance.Environment.Set(property.Value, value.NULL)
23-
}
24-
25-
val, _ := instance.Environment.Get(property.Value)
26-
27-
return val
18+
return evaluateInstanceProperty(left, node)
2819
case *object.LibraryModule:
2920
property := node.Property.(*ast.Identifier)
3021
module := left.(*object.LibraryModule)
@@ -49,3 +40,36 @@ func evaluateProperty(node *ast.Property, scope *object.Scope) object.Object {
4940

5041
return nil
5142
}
43+
44+
func evaluateInstanceProperty(left object.Object, node *ast.Property) object.Object {
45+
var val object.Object
46+
47+
instance := left.(*object.Instance)
48+
property := node.Property.(*ast.Identifier)
49+
50+
if instance.Environment.Has(property.Value) {
51+
val, _ = instance.Environment.Get(property.Value)
52+
53+
return val
54+
}
55+
56+
if instance.Class.Environment.Has(property.Value) {
57+
val, _ = instance.Class.Environment.Get(property.Value)
58+
59+
return val
60+
}
61+
62+
for _, trait := range instance.Class.Traits {
63+
if trait.Environment.Has(property.Value) {
64+
val, _ = trait.Environment.Get(property.Value)
65+
66+
return val
67+
}
68+
}
69+
70+
instance.Environment.Set(property.Value, value.NULL)
71+
72+
val, _ = instance.Environment.Get(property.Value)
73+
74+
return val
75+
}

evaluator/trait.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package evaluator
2+
3+
import (
4+
"ghostlang.org/x/ghost/ast"
5+
"ghostlang.org/x/ghost/object"
6+
)
7+
8+
func evaluateTrait(node *ast.Trait, scope *object.Scope) object.Object {
9+
trait := &object.Trait{
10+
Name: node.Name,
11+
Scope: scope,
12+
Environment: object.NewEnvironment(),
13+
}
14+
15+
// Create a new scope for this trait
16+
trait.Environment = object.NewEnclosedEnvironment(scope.Environment)
17+
traitScope := &object.Scope{Environment: trait.Environment, Self: trait}
18+
19+
result := Evaluate(node.Body, traitScope)
20+
21+
if isError(result) {
22+
return result
23+
}
24+
25+
scope.Environment.Set(node.Name.Value, trait)
26+
27+
return trait
28+
}

evaluator/use.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package evaluator
2+
3+
import (
4+
"ghostlang.org/x/ghost/ast"
5+
"ghostlang.org/x/ghost/object"
6+
)
7+
8+
func evaluateUse(node *ast.Use, scope *object.Scope) object.Object {
9+
// check that the scope is a class
10+
class, ok := scope.Self.(*object.Class)
11+
12+
if !ok {
13+
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)
14+
}
15+
16+
var traits []*object.Trait
17+
18+
for _, trait := range node.Traits {
19+
if !scope.Environment.Has(trait.Value) {
20+
return object.NewError("%d:%d:%s: runtime error: trait '%s' is not defined", trait.Token.Line, trait.Token.Column, trait.Token.File, trait.Value)
21+
}
22+
23+
identifier, _ := scope.Environment.Get(trait.Value)
24+
25+
t, ok := identifier.(*object.Trait)
26+
27+
if !ok {
28+
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)
29+
}
30+
31+
traits = append(traits, t)
32+
}
33+
34+
class.Traits = traits
35+
36+
return nil
37+
}

examples/foo.ghost

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
trait Foo {
2+
message = 'Hello World!!!!'
3+
4+
function bar() {
5+
console.log(this.message)
6+
}
7+
}

examples/scratch.ghost

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1-
x = 0
2-
list = [0, 1, 2, 3]
1+
import Foo from 'foo'
32

4-
for (i = 0; i < list.length(); i++) {
5-
print(i)
6-
}
3+
class Lorem {
4+
use Foo
5+
6+
function hello() {
7+
console.log('hello')
8+
}
9+
}
10+
11+
lorem = Lorem.new()
12+
13+
lorem.hello()
14+
lorem.bar()
15+
16+
console.log('done.')

object/class.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Class struct {
1212
Scope *Scope
1313
Environment *Environment
1414
Super *Class
15+
Traits []*Trait
1516
}
1617

1718
// String represents the class object's value as a string.

object/trait.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package object
2+
3+
import "ghostlang.org/x/ghost/ast"
4+
5+
const TRAIT = "TRAIT"
6+
7+
// Trait objects consist of a body and an environment.
8+
type Trait struct {
9+
Name *ast.Identifier
10+
Scope *Scope
11+
Environment *Environment
12+
}
13+
14+
// String represents the class object's value as a string.
15+
func (trait *Trait) String() string {
16+
return "trait"
17+
}
18+
19+
// Type returns the trait object type.
20+
func (trait *Trait) Type() Type {
21+
return TRAIT
22+
}
23+
24+
// Method defines the set of methods available on trait objects.
25+
func (trait *Trait) Method(method string, args []Object) (Object, bool) {
26+
return nil, false
27+
}

parser/parser.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ func New(scanner *scanner.Scanner) *Parser {
106106
parser.registerPrefix(token.WHILE, parser.whileExpression)
107107
parser.registerPrefix(token.FOR, parser.forExpression)
108108
parser.registerPrefix(token.CLASS, parser.classStatement)
109+
parser.registerPrefix(token.TRAIT, parser.traitStatement)
110+
parser.registerPrefix(token.USE, parser.useExpression)
109111
parser.registerPrefix(token.THIS, parser.thisExpression)
110112
parser.registerPrefix(token.IMPORT, parser.importStatement)
111113
parser.registerPrefix(token.SWITCH, parser.switchStatement)
@@ -202,7 +204,7 @@ func (parser *Parser) isAtEnd() bool {
202204

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

208210
parser.errors = append(parser.errors, message)

parser/parser_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,42 @@ func TestSwitchStatementsWithMultipleDefaults(t *testing.T) {
899899
}
900900
}
901901

902+
func TestTraitExpressions(t *testing.T) {
903+
input := `trait Foo {
904+
//
905+
}`
906+
907+
scanner := scanner.New(input, "test.ghost")
908+
parser := New(scanner)
909+
program := parser.Parse()
910+
911+
failIfParserHasErrors(t, parser)
912+
913+
if len(program.Statements) != 1 {
914+
t.Fatalf("program.Statements does not contain 1 statement. got=%d", len(program.Statements))
915+
}
916+
917+
statement, ok := program.Statements[0].(*ast.Expression)
918+
919+
if !ok {
920+
t.Fatalf("program.Statements[0] is not ast.Expression. got=%T", program.Statements[0])
921+
}
922+
923+
expression, ok := statement.Expression.(*ast.Trait)
924+
925+
if !ok {
926+
t.Fatalf("statement is not ast.Trait. got=%T", statement.Expression)
927+
}
928+
929+
if expression.Name.Value != "Foo" {
930+
t.Fatalf("expression.Name is not 'Foo'. got=%s", expression.Name.Value)
931+
}
932+
933+
if len(expression.Body.Statements) != 0 {
934+
t.Fatalf("expression.Body.Statements does not contain 0 statements. got=%d", len(expression.Body.Statements))
935+
}
936+
}
937+
902938
// =============================================================================
903939
// Helper methods
904940

parser/trait.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package parser
2+
3+
import (
4+
"ghostlang.org/x/ghost/ast"
5+
"ghostlang.org/x/ghost/token"
6+
)
7+
8+
func (parser *Parser) traitStatement() ast.ExpressionNode {
9+
trait := &ast.Trait{Token: parser.currentToken}
10+
11+
parser.readToken()
12+
13+
trait.Name = &ast.Identifier{Token: parser.currentToken, Value: parser.currentToken.Lexeme}
14+
15+
if !parser.expectNextTokenIs(token.LEFTBRACE) {
16+
return nil
17+
}
18+
19+
trait.Body = parser.blockStatement()
20+
21+
return trait
22+
}

parser/use.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package parser
2+
3+
import (
4+
"ghostlang.org/x/ghost/ast"
5+
"ghostlang.org/x/ghost/token"
6+
)
7+
8+
func (parser *Parser) useExpression() ast.ExpressionNode {
9+
use := &ast.Use{Token: parser.currentToken}
10+
11+
if !parser.expectNextTokenIs(token.IDENTIFIER) {
12+
return nil
13+
}
14+
15+
identifier := &ast.Identifier{Token: parser.currentToken, Value: parser.currentToken.Lexeme}
16+
17+
use.Traits = append(use.Traits, identifier)
18+
19+
return use
20+
}

scanner/scanner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ var keywords = map[string]token.Type{
4141
"super": token.SUPER,
4242
"switch": token.SWITCH,
4343
"this": token.THIS,
44+
"trait": token.TRAIT,
4445
"true": token.TRUE,
46+
"use": token.USE,
4547
"while": token.WHILE,
4648
}
4749

0 commit comments

Comments
 (0)