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

Support method missing #672

Merged
merged 5 commits into from
May 22, 2018
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
145 changes: 145 additions & 0 deletions vm/evaluation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,151 @@ func TestMethodCallWithoutParens(t *testing.T) {
}
}

func TestMethodMissing(t *testing.T) {
tests := []struct {
input string
expected interface{}
}{
{`
class Foo
def method_missing(name)
10
end
end

Foo.new.bar
`, 10},
{`
class Foo
def method_missing
10
end
end

f = Foo.new

def f.method_missing(name)
20
end

f.bar
`, 20},
{`
class Foo
def method_missing(name)
name
end
end

Foo.new.bar
`, "bar"},
{`
class Foo
def method_missing(name, *args)
args[0]
end
end

Foo.new.bar(1)
`, 1},
{`
class Foo
def method_missing(name, *args)
name + args[0]
end
end

Foo.new.foo("bar")
`, "foobar"},
{`
class Foo
def method_missing(name)
yield
end
end

Foo.new.bar do
10
end
`, 10},
{`
class Foo
def method_missing(name)
get_block.call
end
end

Foo.new.bar do
10
end
`, 10},
}

for i, tt := range tests {
v := initTestVM()
evaluated := v.testEval(t, tt.input, getFilename())

if isError(evaluated) {
t.Fatalf("got Error: %s", evaluated.(*Error).message)
}

VerifyExpected(t, i, evaluated, tt.expected)
v.checkCFP(t, i, 0)
v.checkSP(t, i, 1)
}
}

func TestMethodMissingFail(t *testing.T) {
tests := []struct {
input string
expected string
}{
{`
class Foo
def method_missing
10
end
end

Foo.new.bar
`, "ArgumentError: Expect at most 0 args for method 'method_missing'. got: 1"},
{`
class Bar
def method_missing
10
end
end

class Foo < Bar
end

Foo.new.bar
`, "UndefinedMethodError: Undefined Method 'bar' for <Instance of: Foo>"},
{`
module Bar
def method_missing
10
end
end

class Foo
include Bar
end

Foo.new.bar
`, "UndefinedMethodError: Undefined Method 'bar' for <Instance of: Foo>"},
}

for i, tt := range tests {
v := initTestVM()
evaluated := v.testEval(t, tt.input, getFilename())

checkErrorMsg(t, i, evaluated, tt.expected)
v.checkCFP(t, i, 1)
v.checkSP(t, i, 1)
}
}

func TestMinusPrefixMethodCall(t *testing.T) {
tests := []struct {
input string
Expand Down
67 changes: 44 additions & 23 deletions vm/instruction.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ var builtinActions = map[operationType]*action{
name: bytecode.NewArray,
operation: func(t *Thread, sourceLine int, cf *normalCallFrame, args ...interface{}) {
argCount := args[0].(int)
elems := []Object{}
var elems []Object

for i := 0; i < argCount; i++ {
v := t.Stack.Pop()
Expand All @@ -228,7 +228,7 @@ var builtinActions = map[operationType]*action{
t.pushErrorObject(errors.TypeError, sourceLine, "Expect stack top's value to be an Array when executing 'expandarray' instruction.")
}

elems := []Object{}
var elems []Object

for i := 0; i < arrLength; i++ {
var elem Object
Expand Down Expand Up @@ -279,10 +279,10 @@ var builtinActions = map[operationType]*action{
name: bytecode.BranchUnless,
operation: func(t *Thread, sourceLine int, cf *normalCallFrame, args ...interface{}) {
v := t.Stack.Pop()
bool, isBool := v.Target.(*BooleanObject)
bo, isBool := v.Target.(*BooleanObject)

if isBool {
if bool.value {
if bo.value {
return
}

Expand All @@ -304,9 +304,9 @@ var builtinActions = map[operationType]*action{
name: bytecode.BranchIf,
operation: func(t *Thread, sourceLine int, cf *normalCallFrame, args ...interface{}) {
v := t.Stack.Pop()
bool, isBool := v.Target.(*BooleanObject)
bo, isBool := v.Target.(*BooleanObject)

if isBool && !bool.value {
if isBool && !bo.value {
return
}

Expand Down Expand Up @@ -496,7 +496,28 @@ var builtinActions = map[operationType]*action{
method = receiver.findMethod(methodName)

if method == nil {
t.setErrorObject(receiverPr, argPr, errors.UndefinedMethodError, sourceLine, "Undefined Method '%+v' for %+v", methodName, receiver.toString())
mm := receiver.findMethodMissing()

if mm == nil {
t.setErrorObject(receiverPr, argPr, errors.UndefinedMethodError, sourceLine, "Undefined Method '%+v' for %+v", methodName, receiver.toString())
} else {
// Move up args for missed method's name
// before: | arg 1 | arg 2 |
// after: | method name | arg 1 | arg 2 |
// TODO: Improve this
t.Stack.Push(nil)

for i := argCount - 1; i >= 0; i-- {
position := argPr + i
arg := t.Stack.data[argPr+i]
t.Stack.Set(position+1, arg)
}

t.Stack.Set(argPr, &Pointer{Target: t.vm.initStringObject(methodName)})
argCount++

method = mm
}
}

// Find Block
Expand Down Expand Up @@ -604,39 +625,39 @@ var builtinActions = map[operationType]*action{
},
}

func (vm *VM) InitObjectFromGoType(value interface{}) Object {
switch v := value.(type) {
func (v *VM) InitObjectFromGoType(value interface{}) Object {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported method VM.InitObjectFromGoType should have comment or be unexported

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported method VM.InitObjectFromGoType should have comment or be unexported

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

switch val := value.(type) {
case nil:
return NULL
case int:
return vm.InitIntegerObject(v)
return v.InitIntegerObject(val)
case int64:
return vm.InitIntegerObject(int(v))
return v.InitIntegerObject(int(val))
case int32:
return vm.InitIntegerObject(int(v))
return v.InitIntegerObject(int(val))
case float64:
return vm.initFloatObject(v)
return v.initFloatObject(val)
case []uint8:
bytes := []byte{}
var bytes []byte

for _, i := range v {
for _, i := range val {
bytes = append(bytes, byte(i))
}

return vm.initStringObject(string(bytes))
return v.initStringObject(string(bytes))
case string:
return vm.initStringObject(v)
return v.initStringObject(val)
case bool:
return toBooleanObject(v)
return toBooleanObject(val)
case []interface{}:
var objs []Object
var objects []Object

for _, elem := range v {
objs = append(objs, vm.InitObjectFromGoType(elem))
for _, elem := range val {
objects = append(objects, v.InitObjectFromGoType(elem))
}

return vm.InitArrayObject(objs)
return v.InitArrayObject(objects)
default:
return vm.initGoObject(value)
return v.initGoObject(value)
}
}
13 changes: 13 additions & 0 deletions vm/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Object interface {
SingletonClass() *RClass
SetSingletonClass(*RClass)
findMethod(string) Object
findMethodMissing() Object
toString() string
toJSON(t *Thread) string
id() int
Expand Down Expand Up @@ -78,6 +79,18 @@ func (b *baseObj) findMethod(methodName string) (method Object) {
return
}

func (b *baseObj) findMethodMissing() (method Object) {
if b.SingletonClass() != nil {
method, _ = b.SingletonClass().Methods.get("method_missing")
}

if method == nil {
method, _ = b.Class().Methods.get("method_missing")
}

return
}

func (b *baseObj) id() int {
r, e := strconv.ParseInt(fmt.Sprintf("%p", b), 0, 64)
if e != nil {
Expand Down