From 605fb346251655b67b97fa726af27be2edd4875c Mon Sep 17 00:00:00 2001 From: st0012 Date: Mon, 21 May 2018 18:58:32 +0200 Subject: [PATCH 1/5] Support most basic method_missing. --- vm/evaluation_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++ vm/instruction.go | 9 ++++++++- vm/object.go | 13 +++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/vm/evaluation_test.go b/vm/evaluation_test.go index ce0ff0c24..7d68a1425 100644 --- a/vm/evaluation_test.go +++ b/vm/evaluation_test.go @@ -1966,6 +1966,51 @@ func TestMethodCallWithoutParens(t *testing.T) { } } +func TestMethodMissing(t *testing.T) { + tests := []struct { + input string + expected int + }{ + {` + class Foo + def method_missing + 10 + end + end + + Foo.new.bar +`, 10}, + {` + class Foo + def method_missing + 10 + end + end + + f = Foo.new + + def f.method_missing + 20 + end + + f.bar +`, 20}, + } + + 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 TestMinusPrefixMethodCall(t *testing.T) { tests := []struct { input string diff --git a/vm/instruction.go b/vm/instruction.go index e705fbee5..f6377f651 100644 --- a/vm/instruction.go +++ b/vm/instruction.go @@ -496,7 +496,14 @@ 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 { + t.Stack.Push(&Pointer{Target: t.vm.initStringObject(methodName)}) + method = mm + } } // Find Block diff --git a/vm/object.go b/vm/object.go index 15d18bf67..74e12552a 100644 --- a/vm/object.go +++ b/vm/object.go @@ -14,6 +14,7 @@ type Object interface { SingletonClass() *RClass SetSingletonClass(*RClass) findMethod(string) Object + findMethodMissing() (Object) toString() string toJSON(t *Thread) string id() int @@ -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 { From 49af1485fab57bfda73b03df581d18420372303d Mon Sep 17 00:00:00 2001 From: st0012 Date: Mon, 21 May 2018 19:48:32 +0200 Subject: [PATCH 2/5] Make method missing capible of receiving args correctly. --- vm/evaluation_test.go | 33 ++++++++++++++++++++++++++++++--- vm/instruction.go | 16 +++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/vm/evaluation_test.go b/vm/evaluation_test.go index 7d68a1425..30f45b06d 100644 --- a/vm/evaluation_test.go +++ b/vm/evaluation_test.go @@ -1969,11 +1969,11 @@ func TestMethodCallWithoutParens(t *testing.T) { func TestMethodMissing(t *testing.T) { tests := []struct { input string - expected int + expected interface{} }{ {` class Foo - def method_missing + def method_missing(name) 10 end end @@ -1989,12 +1989,39 @@ func TestMethodMissing(t *testing.T) { f = Foo.new - def f.method_missing + 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"}, } for i, tt := range tests { diff --git a/vm/instruction.go b/vm/instruction.go index f6377f651..86c4a35e1 100644 --- a/vm/instruction.go +++ b/vm/instruction.go @@ -501,7 +501,21 @@ var builtinActions = map[operationType]*action{ if mm == nil { t.setErrorObject(receiverPr, argPr, errors.UndefinedMethodError, sourceLine, "Undefined Method '%+v' for %+v", methodName, receiver.toString()) } else { - t.Stack.Push(&Pointer{Target: t.vm.initStringObject(methodName)}) + // 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(&Pointer{Target: 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 } } From b264ce7f73624cec7a3807479e503ce487cd083b Mon Sep 17 00:00:00 2001 From: st0012 Date: Mon, 21 May 2018 19:50:35 +0200 Subject: [PATCH 3/5] Format code. --- vm/instruction.go | 4 ++-- vm/object.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vm/instruction.go b/vm/instruction.go index 86c4a35e1..7e8d7c5be 100644 --- a/vm/instruction.go +++ b/vm/instruction.go @@ -507,8 +507,8 @@ var builtinActions = map[operationType]*action{ // TODO: Improve this t.Stack.Push(&Pointer{Target: nil}) - for i := argCount-1; i >= 0; i-- { - position := argPr+i + for i := argCount - 1; i >= 0; i-- { + position := argPr + i arg := t.Stack.data[argPr+i] t.Stack.Set(position+1, arg) } diff --git a/vm/object.go b/vm/object.go index 74e12552a..0799d281a 100644 --- a/vm/object.go +++ b/vm/object.go @@ -14,7 +14,7 @@ type Object interface { SingletonClass() *RClass SetSingletonClass(*RClass) findMethod(string) Object - findMethodMissing() (Object) + findMethodMissing() Object toString() string toJSON(t *Thread) string id() int From d5e7158e28f312af825985b48b870e4cb2d9643c Mon Sep 17 00:00:00 2001 From: st0012 Date: Tue, 22 May 2018 00:38:19 +0400 Subject: [PATCH 4/5] Add more test cases. --- vm/evaluation_test.go | 73 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/vm/evaluation_test.go b/vm/evaluation_test.go index 30f45b06d..b804cfcd5 100644 --- a/vm/evaluation_test.go +++ b/vm/evaluation_test.go @@ -2022,6 +2022,28 @@ func TestMethodMissing(t *testing.T) { 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 { @@ -2038,6 +2060,57 @@ func TestMethodMissing(t *testing.T) { } } +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 "}, + {` + module Bar + def method_missing + 10 + end + end + + class Foo + include Bar + end + + Foo.new.bar +`, "UndefinedMethodError: Undefined Method 'bar' for "}, + } + + 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 From bfd36bea5aa0268f9f74ded8deb9e03e9266916b Mon Sep 17 00:00:00 2001 From: st0012 Date: Tue, 22 May 2018 00:46:15 +0400 Subject: [PATCH 5/5] Refactor instructions implementations. --- vm/instruction.go | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/vm/instruction.go b/vm/instruction.go index 7e8d7c5be..776546829 100644 --- a/vm/instruction.go +++ b/vm/instruction.go @@ -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() @@ -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 @@ -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 } @@ -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 } @@ -505,7 +505,7 @@ var builtinActions = map[operationType]*action{ // before: | arg 1 | arg 2 | // after: | method name | arg 1 | arg 2 | // TODO: Improve this - t.Stack.Push(&Pointer{Target: nil}) + t.Stack.Push(nil) for i := argCount - 1; i >= 0; i-- { position := argPr + i @@ -625,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 { + 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) } }