Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ast/compound.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ type Compound struct {
ExpressionNode
Token token.Token
Left ExpressionNode
Operator string
Operator token.Type
Right ExpressionNode
}
25 changes: 24 additions & 1 deletion ast/identifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,32 @@ package ast

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

// Library binding states for an identifier.
//
// Resolving an identifier means consulting the library module and function
// registries before the scope chain, which costs two map lookups keyed by
// string. Ordinary variables miss both every time they are read, and in a loop
// that hashing dominated identifier evaluation.
//
// The optimizer classifies each identifier once, before evaluation begins, so
// the evaluator can skip the registries for names that cannot be library
// globals. The zero value is deliberately "unknown": an AST that was never
// optimized keeps the original behavior of always consulting the registries.
//
// This is written during optimization and only read during evaluation, which
// matters because Ghost code can be evaluated concurrently - http.handle()
// callbacks run per request on their own goroutines.
const (
LibraryBindingUnknown uint8 = iota // not analyzed; consult the registries
LibraryBindingLocal // cannot be a library global
LibraryBindingGlobal // names a library module or function
)

type Identifier struct {
ExpressionNode
AssignmentNode
Token token.Token
Value string

// LibraryBinding is set by the optimizer; see the constants above.
LibraryBinding uint8
}
2 changes: 1 addition & 1 deletion ast/infix.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ type Infix struct {
ExpressionNode
Token token.Token
Left ExpressionNode
Operator string
Operator token.Type
Right ExpressionNode
}
2 changes: 1 addition & 1 deletion ast/postfix.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import "ghostlang.org/x/ghost/token"
type Postfix struct {
ExpressionNode
Token token.Token
Operator string
Operator token.Type
}
2 changes: 1 addition & 1 deletion ast/prefix.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import "ghostlang.org/x/ghost/token"
type Prefix struct {
ExpressionNode
Token token.Token
Operator string
Operator token.Type
Right ExpressionNode
}
158 changes: 158 additions & 0 deletions evaluator/benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package evaluator

import (
"testing"

"ghostlang.org/x/ghost/library/modules"
"ghostlang.org/x/ghost/object"
"ghostlang.org/x/ghost/optimizer"
"ghostlang.org/x/ghost/parser"
"ghostlang.org/x/ghost/scanner"
)

// Benchmark programs exercise the hot paths of the tree-walking evaluator:
// function call overhead, variable lookup through the scope chain, arithmetic
// allocation, and method dispatch.
var benchmarks = []struct {
name string
source string
}{
{
"Fib",
`function fib(n) {
if (n <= 1) { return n }
return fib(n - 1) + fib(n - 2)
}
fib(18)`,
},
{
"LoopArithmetic",
`total = 0
for (i = 0; i < 50000; i = i + 1) {
total = total + i * 2 - 1
}
total`,
},
{
"WhileLoop",
`i = 0
total = 0
while (i < 50000) {
total = total + i
i = i + 1
}
total`,
},
{
"FunctionCalls",
`function add(a, b) { return a + b }
total = 0
for (i = 0; i < 20000; i = i + 1) {
total = add(total, i)
}
total`,
},
{
"NestedScopeLookup",
`outer = 1
function level1() {
function level2() {
function level3() {
total = 0
for (i = 0; i < 10000; i = i + 1) {
total = total + outer
}
return total
}
return level3()
}
return level2()
}
level1()`,
},
{
"ConstantExpressions",
`total = 0
for (i = 0; i < 20000; i = i + 1) {
total = total + (2 * 3 + 4 * 5 - 6)
}
total`,
},
{
"ListOperations",
`items = []
for (i = 0; i < 10000; i = i + 1) {
items.push(i)
}
total = 0
for (i = 0; i < 10000; i = i + 1) {
total = total + items[i]
}
total`,
},
{
"StringConcat",
`s = ""
for (i = 0; i < 5000; i = i + 1) {
s = s + "x"
}
s.length()`,
},
{
"ClassMethods",
`class Counter {
count = 0
function increment() {
this.count = this.count + 1
return this.count
}
}
c = Counter.new()
for (i = 0; i < 10000; i = i + 1) {
c.increment()
}
c.count`,
},
{
"MapOperations",
`m = {"a": 1, "b": 2, "c": 3}
total = 0
for (i = 0; i < 10000; i = i + 1) {
total = total + m["a"] + m["b"] + m["c"]
}
total`,
},
}

func BenchmarkEvaluate(b *testing.B) {
object.RegisterEvaluator(Evaluate)
modules.RegisterEvaluator(Evaluate)

for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
scope := &object.Scope{Environment: object.NewEnvironment()}
s := scanner.New(bm.source, "bench.ghost")
p := parser.New(s)
program := p.Parse()

if len(p.Errors()) != 0 {
b.Fatalf("parse errors: %v", p.Errors())
}

// Mirror the real pipeline in ghost.Execute, which optimizes
// the program before evaluating it.
program = optimizer.Optimize(program)

result := Evaluate(program, scope)

if object.IsError(result) {
b.Fatalf("runtime error: %s", result.(*object.Error).Message)
}
}
})
}
}
9 changes: 5 additions & 4 deletions evaluator/boolean.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package evaluator
import (
"ghostlang.org/x/ghost/ast"
"ghostlang.org/x/ghost/object"
"ghostlang.org/x/ghost/token"
)

func evaluateBoolean(node *ast.Boolean, scope *object.Scope) object.Object {
Expand All @@ -14,13 +15,13 @@ func evaluateBooleanInfix(node *ast.Infix, left object.Object, right object.Obje
rightValue := right.(*object.Boolean).Value

switch node.Operator {
case "and":
case token.AND:
return toBooleanValue(leftValue && rightValue)
case "or":
case token.OR:
return toBooleanValue(leftValue || rightValue)
case "==":
case token.EQUALEQUAL:
return toBooleanValue(leftValue == rightValue)
case "!=":
case token.BANGEQUAL:
return toBooleanValue(leftValue != rightValue)
}

Expand Down
18 changes: 17 additions & 1 deletion evaluator/compound.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,29 @@ package evaluator
import (
"ghostlang.org/x/ghost/ast"
"ghostlang.org/x/ghost/object"
"ghostlang.org/x/ghost/token"
)

// compoundOperators maps each compound assignment operator to the binary
// operator it applies, e.g. `+=` performs `+`.
var compoundOperators = map[token.Type]token.Type{
token.PLUSEQUAL: token.PLUS,
token.MINUSEQUAL: token.MINUS,
token.STAREQUAL: token.STAR,
token.SLASHEQUAL: token.SLASH,
}

func evaluateCompound(node *ast.Compound, scope *object.Scope) object.Object {
operator, ok := compoundOperators[node.Operator]

if !ok {
return newError("%d:%d:%s: runtime error: unknown operator: %s", node.Token.Line, node.Token.Column, node.Token.File, node.Operator)
}

infix := &ast.Infix{
Token: node.Token,
Left: node.Left,
Operator: node.Operator[:len(node.Operator)-1],
Operator: operator,
Right: node.Right,
}

Expand Down
Loading
Loading