Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Added Methods #320

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist/
dist/
.idea
17 changes: 17 additions & 0 deletions builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ var builtinFuncs = []*BuiltinFunction{
Name: "is_function",
Value: builtinIsFunction,
},
{
Name: "is_method",
Value: builtinIsMethod,
},
{
Name: "is_callable",
Value: builtinIsCallable,
Expand Down Expand Up @@ -276,6 +280,19 @@ func builtinIsFunction(args ...Object) (Object, error) {
return FalseValue, nil
}

func builtinIsMethod(args ...Object) (Object, error) {
if len(args) != 1 {
return nil, ErrWrongNumArguments
}
switch args[0].(type) {
case *CompiledFunction:
if args[0].(*CompiledFunction).UsesReceiver {
return TrueValue, nil
}
}
return FalseValue, nil
}

func builtinIsCallable(args ...Object) (Object, error) {
if len(args) != 1 {
return nil, ErrWrongNumArguments
Expand Down
19 changes: 16 additions & 3 deletions compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,15 +368,19 @@ func (c *Compiler) Compile(node parser.Node) error {
if err := c.Compile(node.Sel); err != nil {
return err
}
c.emit(node, parser.OpIndex)
method := 0
if node.Reci { method = 1 }
c.emit(node, parser.OpIndex, method)
case *parser.IndexExpr:
if err := c.Compile(node.Expr); err != nil {
return err
}
if err := c.Compile(node.Index); err != nil {
return err
}
c.emit(node, parser.OpIndex)
method := 0
if node.Reci { method = 1 }
c.emit(node, parser.OpIndex, method)
case *parser.SliceExpr:
if err := c.Compile(node.Expr); err != nil {
return err
Expand All @@ -398,13 +402,20 @@ func (c *Compiler) Compile(node parser.Node) error {
c.emit(node, parser.OpSliceIndex)
case *parser.FuncLit:
c.enterScope()
reci := node.Type.Receiver
usesReceiver := reci != nil && len(reci.List) == 1

for _, p := range node.Type.Params.List {
s := c.symbolTable.Define(p.Name)

// function arguments is not assigned directly.
s.LocalAssigned = true
}
if usesReceiver {
c.symbolTable.defineFree(&Symbol{ Name: reci.List[0].Name })
} else {
c.symbolTable.defineFree(&Symbol{ Name: "" })
}

if err := c.Compile(node.Body); err != nil {
return err
Expand All @@ -413,7 +424,7 @@ func (c *Compiler) Compile(node parser.Node) error {
// code optimization
c.optimizeFunc(node)

freeSymbols := c.symbolTable.FreeSymbols()
freeSymbols := c.symbolTable.FreeSymbols()[1:]
numLocals := c.symbolTable.MaxSymbols()
instructions, sourceMap := c.leaveScope()

Expand Down Expand Up @@ -476,6 +487,8 @@ func (c *Compiler) Compile(node parser.Node) error {
NumParameters: len(node.Type.Params.List),
VarArgs: node.Type.Params.VarArgs,
SourceMap: sourceMap,
UsesReceiver: usesReceiver,
Free: []*ObjectPtr{{Value: &UndefinedValue}},
}
if len(freeSymbols) > 0 {
c.emit(node, parser.OpClosure,
Expand Down
31 changes: 23 additions & 8 deletions compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ func TestCompiler_Compile(t *testing.T) {
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpBinaryOp, 11),
tengo.MakeInstruction(parser.OpIndex),
tengo.MakeInstruction(parser.OpIndex, 0),
tengo.MakeInstruction(parser.OpPop),
tengo.MakeInstruction(parser.OpSuspend)),
objectsArray(
Expand All @@ -405,7 +405,7 @@ func TestCompiler_Compile(t *testing.T) {
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpConstant, 2),
tengo.MakeInstruction(parser.OpBinaryOp, 12),
tengo.MakeInstruction(parser.OpIndex),
tengo.MakeInstruction(parser.OpIndex, 0),
tengo.MakeInstruction(parser.OpPop),
tengo.MakeInstruction(parser.OpSuspend)),
objectsArray(
Expand Down Expand Up @@ -502,6 +502,21 @@ func TestCompiler_Compile(t *testing.T) {
intObject(1),
intObject(2))))

expectCompile(t, `f1 := func(r)() { return r }; f1();`,
bytecode(
concatInsts(
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpSetGlobal, 0),
tengo.MakeInstruction(parser.OpGetGlobal, 0),
tengo.MakeInstruction(parser.OpCall, 0, 0),
tengo.MakeInstruction(parser.OpPop),
tengo.MakeInstruction(parser.OpSuspend)),
objectsArray(
compiledFunction(1, 1,
tengo.MakeInstruction(parser.OpGetFree, 0),
tengo.MakeInstruction(parser.OpReturn, 1)),
)))

expectCompile(t, `func() { return 5 + 10 }`,
bytecode(
concatInsts(
Expand Down Expand Up @@ -827,7 +842,7 @@ func TestCompiler_Compile(t *testing.T) {
tengo.MakeInstruction(parser.OpSuspend)),
objectsArray(
compiledFunction(1, 1,
tengo.MakeInstruction(parser.OpGetFree, 0),
tengo.MakeInstruction(parser.OpGetFree, 1),
tengo.MakeInstruction(parser.OpGetLocal, 0),
tengo.MakeInstruction(parser.OpBinaryOp, 11),
tengo.MakeInstruction(parser.OpReturn, 1)),
Expand All @@ -852,14 +867,14 @@ func(a) {
tengo.MakeInstruction(parser.OpSuspend)),
objectsArray(
compiledFunction(1, 1,
tengo.MakeInstruction(parser.OpGetFree, 0),
tengo.MakeInstruction(parser.OpGetFree, 1),
tengo.MakeInstruction(parser.OpGetFree, 2),
tengo.MakeInstruction(parser.OpBinaryOp, 11),
tengo.MakeInstruction(parser.OpGetLocal, 0),
tengo.MakeInstruction(parser.OpBinaryOp, 11),
tengo.MakeInstruction(parser.OpReturn, 1)),
compiledFunction(1, 1,
tengo.MakeInstruction(parser.OpGetFreePtr, 0),
tengo.MakeInstruction(parser.OpGetFreePtr, 1),
tengo.MakeInstruction(parser.OpGetLocalPtr, 0),
tengo.MakeInstruction(parser.OpClosure, 0, 2),
tengo.MakeInstruction(parser.OpReturn, 1)),
Expand Down Expand Up @@ -900,17 +915,17 @@ func() {
tengo.MakeInstruction(parser.OpConstant, 3),
tengo.MakeInstruction(parser.OpDefineLocal, 0),
tengo.MakeInstruction(parser.OpGetGlobal, 0),
tengo.MakeInstruction(parser.OpGetFree, 0),
tengo.MakeInstruction(parser.OpBinaryOp, 11),
tengo.MakeInstruction(parser.OpGetFree, 1),
tengo.MakeInstruction(parser.OpBinaryOp, 11),
tengo.MakeInstruction(parser.OpGetFree, 2),
tengo.MakeInstruction(parser.OpBinaryOp, 11),
tengo.MakeInstruction(parser.OpGetLocal, 0),
tengo.MakeInstruction(parser.OpBinaryOp, 11),
tengo.MakeInstruction(parser.OpReturn, 1)),
compiledFunction(1, 0,
tengo.MakeInstruction(parser.OpConstant, 2),
tengo.MakeInstruction(parser.OpDefineLocal, 0),
tengo.MakeInstruction(parser.OpGetFreePtr, 0),
tengo.MakeInstruction(parser.OpGetFreePtr, 1),
tengo.MakeInstruction(parser.OpGetLocalPtr, 0),
tengo.MakeInstruction(parser.OpClosure, 4, 2),
tengo.MakeInstruction(parser.OpReturn, 1)),
Expand Down
9 changes: 7 additions & 2 deletions docs/builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,13 +290,18 @@ Returns `true` if the object's type is undefined. Or it returns `false`.

## is_function

Returns `true` if the object's type is function or closure. Or it returns
Returns `true` if the object's type is function, method, or closure. Or it returns
`false`. Note that `is_function` returns `false` for builtin functions and
user-provided callable objects.

## is_method

Returns `true` if the object's type is method. Or it returns
`false`.

## is_callable

Returns `true` if the object is callable (e.g. function, closure, builtin
Returns `true` if the object is callable (e.g. function, closure, method, builtin
function, or user-provided callable objects). Or it returns `false`.

## is_array
Expand Down
67 changes: 67 additions & 0 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Here's a list of all available value types in Tengo.
| immutable map | [immutable](#immutable-values) map | - |
| undefined | [undefined](#undefined-values) value | - |
| function | [function](#function-values) value | - |
| method | [method](#method-values) value | - |
| _user-defined_ | value of [user-defined types](https://github.com/d5/tengo/blob/master/docs/objects.md) | - |

### Error Values
Expand Down Expand Up @@ -218,6 +219,72 @@ f2(1, 2, 3) // valid; a = 1, b = [2, 3]
f2([1, 2, 3]...) // valid; a = 1, b = [2, 3]
```

## Method Values

Sometimes, when creating structures in Tengo, it becomes desirable to be able
to call back to the parent map or array. This is handled by a method, a
specialized subset of a function. When a method is defined, it creates a receiver in
the local scope that takes on the identity of the parent slice or array during a
call.

```golang
my_counter := {
counter: 21,
getCount: func(reci)() {
return reci.counter
}
}

my_counter.counter += 4

my_counter.getCount() //returns 25
```

Notably, and unlike Go, a method receiver is not dependent on being instanced inside
a map or array.

```golang
concat := func(reci)(str) {
reci.buffer += str
}

my_writer := {
buffer: "",
append: concat
}

my_writer.append("hello ") // my_obj.buffer = "hello "
my_writer.append("world!") // my_obj.buffer = "hello world!"
```

Furthermore, when called from outside a method-call, the receiver on a method call will
be ```undefined```

```golang
f := func(r)() {
return r
}

f() //returns undefined
```

Finally, a copy() of an array or slice containing a method will reference the new copy.

```golang
m1 := {
c: 21,
getC: func(r)() { return r.c }
}

m2 := copy(m1)

m2.c = 7
m1.getC() //21
m1.c = 2
m1.getC() //2
m2.getC() //7
```

## Variables and Scopes

A value can be assigned to a variable using assignment operator `:=` and `=`.
Expand Down
2 changes: 2 additions & 0 deletions objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ type CompiledFunction struct {
VarArgs bool
SourceMap map[int]parser.Pos
Free []*ObjectPtr
UsesReceiver bool
}

// TypeName returns the name of the type.
Expand All @@ -595,6 +596,7 @@ func (o *CompiledFunction) Copy() Object {
NumParameters: o.NumParameters,
VarArgs: o.VarArgs,
Free: append([]*ObjectPtr{}, o.Free...), // DO NOT Copy() of elements; these are variable pointers
UsesReceiver: o.UsesReceiver,
}
}

Expand Down
8 changes: 7 additions & 1 deletion parser/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,14 @@ func (e *FuncLit) End() Pos {
}

func (e *FuncLit) String() string {
return "func" + e.Type.Params.String() + " " + e.Body.String()
return e.Type.String() + " " + e.Body.String()
}

// FuncType represents a function type definition.
type FuncType struct {
FuncPos Pos
Params *IdentList
Receiver*IdentList
}

func (e *FuncType) exprNode() {}
Expand All @@ -278,6 +279,9 @@ func (e *FuncType) End() Pos {
}

func (e *FuncType) String() string {
if e.Receiver != nil {
return "func" + e.Receiver.String() + e.Params.String()
}
return "func" + e.Params.String()
}

Expand Down Expand Up @@ -360,6 +364,7 @@ type IndexExpr struct {
LBrack Pos
Index Expr
RBrack Pos
Reci bool
}

func (e *IndexExpr) exprNode() {}
Expand Down Expand Up @@ -483,6 +488,7 @@ func (e *ParenExpr) String() string {
type SelectorExpr struct {
Expr Expr
Sel Expr
Reci bool
}

func (e *SelectorExpr) exprNode() {}
Expand Down
2 changes: 1 addition & 1 deletion parser/opcodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ var OpcodeOperands = [...][]int{
OpMap: {2},
OpError: {},
OpImmutable: {},
OpIndex: {},
OpIndex: {1},
OpSliceIndex: {},
OpCall: {1, 1},
OpReturn: {1},
Expand Down
15 changes: 14 additions & 1 deletion parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,13 @@ func (p *Parser) parseIndexOrSlice(x Expr) Expr {
High: index[1],
}
}
receiver := p.token == token.LParen
return &IndexExpr{
Expr: x,
LBrack: lbrack,
RBrack: rbrack,
Index: index[0],
Reci: receiver,
}
}

Expand All @@ -359,7 +361,8 @@ func (p *Parser) parseSelector(x Expr) Expr {
}

sel := p.parseIdent()
return &SelectorExpr{Expr: x, Sel: &StringLit{
receiver := p.token == token.LParen
return &SelectorExpr{Expr: x, Reci: receiver, Sel: &StringLit{
Value: sel.Name,
ValuePos: sel.NamePos,
Literal: sel.Name,
Expand Down Expand Up @@ -578,10 +581,20 @@ func (p *Parser) parseFuncType() *FuncType {
}

pos := p.expect(token.Func)
var receiver *IdentList
params := p.parseIdentList()
if p.token == token.LParen {
if !params.VarArgs && len(params.List) <= 1 {
receiver = params
params = p.parseIdentList()
} else {
p.errorExpected(params.Pos(), "1 receiver")
}
}
return &FuncType{
FuncPos: pos,
Params: params,
Receiver:receiver,
}
}

Expand Down
Loading