Skip to content

Commit ab68701

Browse files
authored
Fix trait method lookup for multi-trait classes (#133)
* fix trait method lookup * allow multiple traits on class
1 parent 14cde84 commit ab68701

File tree

4 files changed

+134
-4
lines changed

4 files changed

+134
-4
lines changed

CLAUDE.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build and Development Commands
6+
7+
```bash
8+
make run # Build and run Ghost REPL
9+
make build # Build for all platforms (mac/linux/windows)
10+
make test # Run all tests with colored output
11+
go test -v ./evaluator/... # Run tests for a specific package
12+
```
13+
14+
## Architecture Overview
15+
16+
Ghost is a tree-walking interpreter written in Go. The execution pipeline follows this flow:
17+
18+
**Source Code → Scanner → Parser → AST → Evaluator → Object**
19+
20+
### Core Packages
21+
22+
- **scanner/** - Lexical analysis. Transforms source into tokens. Keywords defined in `scanner/scanner.go:21-48`.
23+
- **token/** - Token type definitions and the Token struct containing lexeme, literal, position info.
24+
- **parser/** - Recursive descent parser using Pratt parsing. Each AST node type has its own parsing function (e.g., `parser/function.go`, `parser/if.go`).
25+
- **ast/** - Abstract syntax tree node definitions. Base interfaces in `ast/ast.go`: `Node`, `StatementNode`, `ExpressionNode`.
26+
- **evaluator/** - Tree-walking evaluation. Main entry point is `Evaluate()` in `evaluator/evaluator.go:15`. Each AST node type has a corresponding `evaluate*` function.
27+
- **object/** - Runtime value types (Number, String, Boolean, List, Map, Function, Class, etc.). The `Object` interface (`object/object.go:15-19`) requires `Type()`, `String()`, and `Method()`.
28+
- **ghost/** - Main Ghost struct that orchestrates the pipeline (`ghost/ghost.go`). Entry point for embedding Ghost in Go applications.
29+
30+
### Key Design Patterns
31+
32+
- **Scope**: Wraps Environment and tracks `Self` for method calls (`object/scope.go`).
33+
- **Environment**: Variable storage with parent chain for lexical scoping (`object/environment.go`).
34+
- **Library system**: Native functions and modules registered via `library.RegisterFunction()` and `library.RegisterModule()`. Built-in modules in `library/modules/`.
35+
36+
### Object Method System
37+
38+
All object types implement the `Method(method string, args []Object) (Object, bool)` interface. Methods are defined directly on object types (e.g., string methods in `object/string.go`).
39+
40+
## Language Features
41+
42+
Ghost supports: classes with inheritance (`extends`), traits (`trait`/`use`), first-class functions, closures, lists, maps, for/for-in/while loops, switch statements, imports, and compound operators (`+=`, `++`, etc.).
43+
44+
## Version
45+
46+
Update `version/version.go` when releasing. GoReleaser handles binary distribution.

evaluator/evaluator_test.go

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ func TestClassProperties(t *testing.T) {
207207
function constructor(area) {
208208
this.area = area
209209
}
210-
210+
211211
function area() {
212212
return math.pi * this.area * this.area
213213
}
@@ -223,6 +223,90 @@ func TestClassProperties(t *testing.T) {
223223
isNumberObject(t, result, 314)
224224
}
225225

226+
func TestTraitMethodLookup(t *testing.T) {
227+
tests := []struct {
228+
name string
229+
input string
230+
expected int64
231+
}{
232+
{
233+
name: "method from single trait",
234+
input: `
235+
trait Greet {
236+
function greet() {
237+
return 1
238+
}
239+
}
240+
241+
class Person {
242+
use Greet
243+
}
244+
245+
p = Person.new()
246+
p.greet()
247+
`,
248+
expected: 1,
249+
},
250+
{
251+
name: "method from second trait",
252+
input: `
253+
trait First {
254+
function first() {
255+
return 1
256+
}
257+
}
258+
259+
trait Second {
260+
function second() {
261+
return 2
262+
}
263+
}
264+
265+
class Thing {
266+
use First
267+
use Second
268+
}
269+
270+
t = Thing.new()
271+
t.second()
272+
`,
273+
expected: 2,
274+
},
275+
{
276+
name: "methods from both traits",
277+
input: `
278+
trait Add {
279+
function add() {
280+
return 10
281+
}
282+
}
283+
284+
trait Multiply {
285+
function multiply() {
286+
return 20
287+
}
288+
}
289+
290+
class Calculator {
291+
use Add
292+
use Multiply
293+
}
294+
295+
c = Calculator.new()
296+
c.add() + c.multiply()
297+
`,
298+
expected: 30,
299+
},
300+
}
301+
302+
for _, tt := range tests {
303+
t.Run(tt.name, func(t *testing.T) {
304+
result := evaluate(tt.input)
305+
isNumberObject(t, result, tt.expected)
306+
})
307+
}
308+
}
309+
226310
// =============================================================================
227311
// Helper functions
228312

evaluator/method.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ func evaluateInstanceMethod(node *ast.Method, receiver *object.Instance, name st
7979
for _, trait := range receiver.Class.Traits {
8080
method, ok = trait.Environment.Get(name)
8181

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)
82+
if ok {
83+
break
8484
}
8585
}
8686
}

evaluator/use.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func evaluateUse(node *ast.Use, scope *object.Scope) object.Object {
3131
traits = append(traits, t)
3232
}
3333

34-
class.Traits = traits
34+
class.Traits = append(class.Traits, traits...)
3535

3636
return nil
3737
}

0 commit comments

Comments
 (0)