Repository: goby-lang/goby
Pull request #791 (2019-10-13 09:30:55)

Implement dup
The default `dup` method returns the receiver object. Mutable data structures like `Object`, `Array`, `String` and `Hash` have their own implementation to override the default behavior for real duplication.


Patch:
diff --git a/vm/array.go b/vm/array.go
index 488f609..e4aef4d 100644
--- a/vm/array.go
+++ b/vm/array.go
@@ -617,6 +617,18 @@ var builtinArrayInstanceMethods = []*BuiltinMethodObject{
 
 		},
 	},
+	{
+		Name: "dup",
+		Fn: func(receiver Object, sourceLine int, t *Thread, args []Object, blockFrame *normalCallFrame) Object {
+			arr, _ := receiver.(*ArrayObject)
+			newArr := make([]Object, len(arr.Elements))
+			copy(newArr, arr.Elements)
+			newObj := t.vm.InitArrayObject(newArr)
+			newObj.setInstanceVariables(arr.instanceVariables().copy())
+
+			return newObj
+		},
+	},
 	{
 		// Loops through each element in the array, with the given block.
 		// Returns self.
@@ -1490,7 +1502,7 @@ var builtinArrayInstanceMethods = []*BuiltinMethodObject{
 // InitArrayObject returns a new object with the given elemnts
 func (vm *VM) InitArrayObject(elements []Object) *ArrayObject {
 	return &ArrayObject{
-		BaseObj:  &BaseObj{class: vm.TopLevelClass(classes.ArrayClass)},
+		BaseObj:  &BaseObj{class: vm.TopLevelClass(classes.ArrayClass), InstanceVariables: newEnvironment()},
 		Elements: elements,
 	}
 }
diff --git a/vm/class.go b/vm/class.go
index 6e0067c..7123950 100644
--- a/vm/class.go
+++ b/vm/class.go
@@ -874,6 +874,20 @@ var builtinClassCommonInstanceMethods = []*BuiltinMethodObject{
 
 		},
 	},
+	{
+		Name: "dup",
+		Fn: func(receiver Object, sourceLine int, t *Thread, args []Object, blockFrame *normalCallFrame) Object {
+			switch receiver.(type) {
+			case *RObject:
+				newObj := receiver.Class().initializeInstance()
+				newObj.setInstanceVariables(receiver.instanceVariables().copy())
+
+				return newObj
+			default:
+				return receiver
+			}
+		},
+	},
 	// Exits from the interpreter, returning the specified exit code (if any).
 	//
 	// The method itself formally returns nil, although it's not usable.
diff --git a/vm/environment.go b/vm/environment.go
index a940ade..6787a55 100644
--- a/vm/environment.go
+++ b/vm/environment.go
@@ -29,3 +29,11 @@ func (e *environment) names() []string {
 	sort.Strings(keys)
 	return keys
 }
+
+func (e *environment) copy() *environment {
+	newEnv := make(map[string]Object)
+	for key, value := range e.store {
+		newEnv[key] = value
+	}
+	return &environment{store: newEnv}
+}
diff --git a/vm/error.go b/vm/error.go
index 88f2b01..f598523 100644
--- a/vm/error.go
+++ b/vm/error.go
@@ -59,7 +59,7 @@ func (vm *VM) InitErrorObject(errorType string, sourceLine int, format string, a
 }
 
 func (vm *VM) initErrorClasses() {
-	errTypes := []string{errors.InternalError, errors.IOError, errors.ArgumentError, errors.NameError, errors.StopIteration, errors.TypeError, errors.NoMethodError, errors.ConstantAlreadyInitializedError, errors.HTTPError, errors.ZeroDivisionError, errors.ChannelCloseError}
+	errTypes := []string{errors.InternalError, errors.IOError, errors.ArgumentError, errors.NameError, errors.StopIteration, errors.TypeError, errors.NoMethodError, errors.ConstantAlreadyInitializedError, errors.HTTPError, errors.ZeroDivisionError, errors.ChannelCloseError, errors.NotImplementedError}
 
 	for _, errType := range errTypes {
 		c := vm.initializeClass(errType)
diff --git a/vm/errors/error.go b/vm/errors/error.go
index fd37219..45c2b70 100644
--- a/vm/errors/error.go
+++ b/vm/errors/error.go
@@ -23,30 +23,33 @@ const (
 	ZeroDivisionError = "ZeroDivisionError"
 	// ChannelCloseError is for accessing to the closed channel
 	ChannelCloseError = "ChannelCloseError"
+
+	NotImplementedError = "NotImplementedError"
 )
 
 /*
 	Here defines different error message formats for different types of errors
 */
 const (
-	WrongNumberOfArgument       = "Expect %d argument(s). got: %d"
-	WrongNumberOfArgumentMore   = "Expect %d or more argument(s). got: %d"
-	WrongNumberOfArgumentLess   = "Expect %d or less argument(s). got: %d"
-	WrongNumberOfArgumentRange  = "Expect %d to %d argument(s). got: %d"
-	WrongArgumentTypeFormat     = "Expect argument to be %s. got: %s"
-	WrongArgumentTypeFormatNum  = "Expect argument #%d to be %s. got: %s"
-	InvalidChmodNumber          = "Invalid chmod number. got: %d"
-	InvalidNumericString        = "Invalid numeric string. got: %s"
-	CantLoadFile                = "Can't load \"%s\""
-	CantRequireNonString        = "Can't require \"%s\": Pass a string instead"
-	CantYieldWithoutBlockFormat = "Can't yield without a block"
-	NotDiggable                 = "Expect target to be Diggable, got %s"
-	DividedByZero               = "Divided by 0"
-	ChannelIsClosed             = "The channel is already closed."
-	TooSmallIndexValue          = "Index value %d too small for array. minimum: %d"
-	IndexOutOfRange             = "Index value out of range. got: %v"
-	RegexpFailure               = "Replacement failure with the Regexp. got: %s"
-	NegativeValue               = "Expect argument to be positive value. got: %d"
-	NegativeSecondValue         = "Expect second argument to be positive value. got: %d"
-	UndefinedMethod             = "Undefined Method '%+v' for %+v"
+	WrongNumberOfArgument           = "Expect %d argument(s). got: %d"
+	WrongNumberOfArgumentMore       = "Expect %d or more argument(s). got: %d"
+	WrongNumberOfArgumentLess       = "Expect %d or less argument(s). got: %d"
+	WrongNumberOfArgumentRange      = "Expect %d to %d argument(s). got: %d"
+	WrongArgumentTypeFormat         = "Expect argument to be %s. got: %s"
+	WrongArgumentTypeFormatNum      = "Expect argument #%d to be %s. got: %s"
+	InvalidChmodNumber              = "Invalid chmod number. got: %d"
+	InvalidNumericString            = "Invalid numeric string. got: %s"
+	CantLoadFile                    = "Can't load \"%s\""
+	CantRequireNonString            = "Can't require \"%s\": Pass a string instead"
+	CantYieldWithoutBlockFormat     = "Can't yield without a block"
+	NotDiggable                     = "Expect target to be Diggable, got %s"
+	DividedByZero                   = "Divided by 0"
+	ChannelIsClosed                 = "The channel is already closed."
+	TooSmallIndexValue              = "Index value %d too small for array. minimum: %d"
+	IndexOutOfRange                 = "Index value out of range. got: %v"
+	RegexpFailure                   = "Replacement failure with the Regexp. got: %s"
+	NegativeValue                   = "Expect argument to be positive value. got: %d"
+	NegativeSecondValue             = "Expect second argument to be positive value. got: %d"
+	NativeNotImplementedErrorFormat = "'%s' should be implemented on %s but haven't be done yet. Looking forward to see your PR for it ;-)"
+	UndefinedMethod                 = "Undefined Method '%+v' for %+v"
 )
diff --git a/vm/hash.go b/vm/hash.go
index c12ab40..316df4d 100644
--- a/vm/hash.go
+++ b/vm/hash.go
@@ -426,6 +426,12 @@ var builtinHashInstanceMethods = []*BuiltinMethodObject{
 
 		},
 	},
+	{
+		Name: "dup",
+		Fn: func(receiver Object, sourceLine int, t *Thread, args []Object, blockFrame *normalCallFrame) Object {
+			return receiver.(*HashObject).copy()
+		},
+	},
 	{
 		// Calls block once for each key in the hash (in sorted key order), passing the
 		// key-value pair as parameters.
@@ -1292,7 +1298,7 @@ func (h *HashObject) copy() Object {
 	}
 
 	newHash := &HashObject{
-		BaseObj: &BaseObj{class: h.class},
+		BaseObj: &BaseObj{class: h.class, InstanceVariables: newEnvironment()},
 		Pairs:   elems,
 	}
 
diff --git a/vm/object.go b/vm/object.go
index 0c8cfa9..35c58dc 100644
--- a/vm/object.go
+++ b/vm/object.go
@@ -21,6 +21,8 @@ type Object interface {
 	id() int
 	InstanceVariableGet(string) (Object, bool)
 	InstanceVariableSet(string, Object) Object
+	instanceVariables() *environment
+	setInstanceVariables(*environment)
 	isTruthy() bool
 }
 
@@ -75,6 +77,14 @@ func (b *BaseObj) InstanceVariableSet(name string, value Object) Object {
 	return value
 }
 
+func (b *BaseObj) instanceVariables() *environment {
+	return b.InstanceVariables
+}
+
+func (b *BaseObj) setInstanceVariables(e *environment) {
+	b.InstanceVariables = e
+}
+
 func (b *BaseObj) findMethod(methodName string) (method Object) {
 	if b.SingletonClass() != nil {
 		method = b.SingletonClass().lookupMethod(methodName)
diff --git a/vm/string.go b/vm/string.go
index 2082686..782c7ce 100644
--- a/vm/string.go
+++ b/vm/string.go
@@ -595,6 +595,16 @@ var builtinStringInstanceMethods = []*BuiltinMethodObject{
 
 		},
 	},
+	{
+		Name: "dup",
+		Fn: func(receiver Object, sourceLine int, t *Thread, args []Object, blockFrame *normalCallFrame) Object {
+			str, _ := receiver.(*StringObject)
+			newObj := t.vm.InitStringObject(str.value)
+			newObj.setInstanceVariables(str.instanceVariables().copy())
+
+			return newObj
+		},
+	},
 	{
 		// Split and loop through the string byte.
 		//
@@ -1651,7 +1661,7 @@ var builtinStringInstanceMethods = []*BuiltinMethodObject{
 
 func (vm *VM) InitStringObject(value string) *StringObject {
 	return &StringObject{
-		BaseObj: &BaseObj{class: vm.TopLevelClass(classes.StringClass)},
+		BaseObj: &BaseObj{class: vm.TopLevelClass(classes.StringClass), InstanceVariables: newEnvironment()},
 		value:   value,
 	}
 }


Test patch:
diff --git a/vm/array_test.go b/vm/array_test.go
index e30d814..b505f20 100644
--- a/vm/array_test.go
+++ b/vm/array_test.go
@@ -2244,4 +2244,29 @@ func TestArrayValuesAtMethodFail(t *testing.T) {
 		v.checkCFP(t, i, tt.expectedCFP)
 		v.checkSP(t, i, 1)
 	}
-}
\ No newline at end of file
+}
+
+func TestArrayDupMethod(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{`[1,2,3].dup`, []interface{}{1, 2, 3}},
+
+		{`
+a = [1,2,3]
+b = a.dup
+a[0] = 10
+b
+`, []interface{}{1, 2, 3}},
+	}
+
+
+	for i, tt := range tests {
+		v := initTestVM()
+		evaluated := v.testEval(t, tt.input, getFilename())
+		VerifyExpected(t, i, evaluated, tt.expected)
+		v.checkCFP(t, i, 0)
+		v.checkSP(t, i, 1)
+	}
+}
diff --git a/vm/boolean_test.go b/vm/boolean_test.go
index 780bcad..54689bb 100644
--- a/vm/boolean_test.go
+++ b/vm/boolean_test.go
@@ -214,3 +214,22 @@ func TestInitializeBoolean(t *testing.T) {
 		t.Errorf("expected 'false'. got=%t", FALSE.value)
 	}
 }
+
+func TestBooleanDupMethod(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{`true.dup`, true},
+		{`false.dup`, false},
+	}
+
+
+	for i, tt := range tests {
+		v := initTestVM()
+		evaluated := v.testEval(t, tt.input, getFilename())
+		VerifyExpected(t, i, evaluated, tt.expected)
+		v.checkCFP(t, i, 0)
+		v.checkSP(t, i, 1)
+	}
+}
diff --git a/vm/decimal_test.go b/vm/decimal_test.go
index 4eaeb99..25806dc 100644
--- a/vm/decimal_test.go
+++ b/vm/decimal_test.go
@@ -481,3 +481,20 @@ func TestDecimalMinusZero(t *testing.T) {
 		v.checkSP(t, i, 1)
 	}
 }
+
+func TestDecimalDupMethod(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{`"1.1".to_d.dup == "1.1".to_d`, true},
+	}
+
+	for i, tt := range tests {
+		v := initTestVM()
+		evaluated := v.testEval(t, tt.input, getFilename())
+		VerifyExpected(t, i, evaluated, tt.expected)
+		v.checkCFP(t, i, 0)
+		v.checkSP(t, i, 1)
+	}
+}
diff --git a/vm/float_test.go b/vm/float_test.go
index b7ba292..b82b2e3 100644
--- a/vm/float_test.go
+++ b/vm/float_test.go
@@ -368,3 +368,20 @@ func TestFloatMinusZero(t *testing.T) {
 		v.checkSP(t, i, 1)
 	}
 }
+
+func TestFloatDupMethod(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{`1.1.dup == 1.1`, true},
+	}
+
+	for i, tt := range tests {
+		v := initTestVM()
+		evaluated := v.testEval(t, tt.input, getFilename())
+		VerifyExpected(t, i, evaluated, tt.expected)
+		v.checkCFP(t, i, 0)
+		v.checkSP(t, i, 1)
+	}
+}
diff --git a/vm/hash_test.go b/vm/hash_test.go
index 9d3a5fa..f1f5b41 100644
--- a/vm/hash_test.go
+++ b/vm/hash_test.go
@@ -1949,3 +1949,28 @@ func TestHashInspectCallsChildElementToString(t *testing.T) {
 	vm.checkCFP(t, i, 0)
 	vm.checkSP(t, i, 1)
 }
+
+func TestHashDupMethod(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected map[string]interface{}
+	}{
+		{`{foo: "bar"}.dup`, map[string]interface{}{"foo": "bar"}},
+
+		{`
+a = {foo: "bar"}
+b = a.dup
+a["foo"] = 10
+b
+`, map[string]interface{}{"foo": "bar"}},
+	}
+
+
+	for i, tt := range tests {
+		v := initTestVM()
+		evaluated := v.testEval(t, tt.input, getFilename())
+		verifyHashObject(t, i, evaluated, tt.expected)
+		v.checkCFP(t, i, 0)
+		v.checkSP(t, i, 1)
+	}
+}
diff --git a/vm/integer_test.go b/vm/integer_test.go
index c0595c3..e9559e8 100644
--- a/vm/integer_test.go
+++ b/vm/integer_test.go
@@ -361,3 +361,22 @@ func TestIntegerZeroDivisionFail(t *testing.T) {
 		v.checkSP(t, i, 1)
 	}
 }
+
+
+func TestIntegerDupMethod(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{`1.dup`, 1},
+	}
+
+
+	for i, tt := range tests {
+		v := initTestVM()
+		evaluated := v.testEval(t, tt.input, getFilename())
+		VerifyExpected(t, i, evaluated, tt.expected)
+		v.checkCFP(t, i, 0)
+		v.checkSP(t, i, 1)
+	}
+}
diff --git a/vm/object_test.go b/vm/object_test.go
index fb18205..c71fa34 100644
--- a/vm/object_test.go
+++ b/vm/object_test.go
@@ -66,3 +66,62 @@ func TestObjectTapMethodFail(t *testing.T) {
 	}
 }
 
+func TestObjectDupMethod(t *testing.T) {
+	setup := `
+class Student
+	attr_accessor :name
+	def initialize(name)
+		@name = name
+	end
+end
+
+class School
+	attr_accessor :name, :students
+
+	def initialize(name, students)
+		@name = name
+		@students = students
+	end
+
+	def inspect
+		String.fmt("Name: %s. Students: %s", name, students.map do |s| s.name end.join(", "))
+	end
+end
+`
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{
+			`
+stan = Student.new("Stan")
+stan.dup.name
+`, "Stan"},
+		{
+			`
+stan = Student.new("Stan")
+dup = stan.dup
+dup.name = "Jane"
+
+[stan.name, dup.name]
+`, []interface{}{"Stan", "Jane"}},
+{
+			`
+stan = Student.new("Stan")
+jane = Student.new("Stan")
+
+s1 = School.new("S1", [stan])
+s2 = s1.dup
+s2.name = "S2"
+s2.inspect
+`, "Name: S2. Students: Stan"},
+	}
+
+	for i, tt := range tests {
+		v := initTestVM()
+		evaluated := v.testEval(t, setup + tt.input, getFilename())
+		VerifyExpected(t, i, evaluated, tt.expected)
+		v.checkCFP(t, i, 0)
+		v.checkSP(t, i, 1)
+	}
+}
diff --git a/vm/range_test.go b/vm/range_test.go
index a57f18d..674d3b4 100644
--- a/vm/range_test.go
+++ b/vm/range_test.go
@@ -655,3 +655,20 @@ func TestRangeToEnumMethod(t *testing.T) {
 	v.checkCFP(t, i, 0)
 	v.checkSP(t, i, 1)
 }
+
+func TestRangeDupMethod(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{`(1..2).dup == (1..2)`, true},
+	}
+
+	for i, tt := range tests {
+		v := initTestVM()
+		evaluated := v.testEval(t, tt.input, getFilename())
+		VerifyExpected(t, i, evaluated, tt.expected)
+		v.checkCFP(t, i, 0)
+		v.checkSP(t, i, 1)
+	}
+}
diff --git a/vm/regexp_test.go b/vm/regexp_test.go
index e1218f0..9fb80aa 100644
--- a/vm/regexp_test.go
+++ b/vm/regexp_test.go
@@ -197,3 +197,21 @@ func TestRegexpMatchMethodFail(t *testing.T) {
 		v.checkSP(t, i, 1)
 	}
 }
+
+
+func TestRegexpDupMethod(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{`Regexp.new("abc").dup == Regexp.new("abc")`, true},
+	}
+
+	for i, tt := range tests {
+		v := initTestVM()
+		evaluated := v.testEval(t, tt.input, getFilename())
+		VerifyExpected(t, i, evaluated, tt.expected)
+		v.checkCFP(t, i, 0)
+		v.checkSP(t, i, 1)
+	}
+}
diff --git a/vm/string_test.go b/vm/string_test.go
index 85c2619..9347202 100644
--- a/vm/string_test.go
+++ b/vm/string_test.go
@@ -1490,3 +1490,28 @@ func TestStringInspectEvaluatesToOriginalString(t *testing.T) {
 		v.checkSP(t, i, 3)
 	}
 }
+
+func TestStringDupMethod(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{
+			`"Stan".dup`, "Stan"},
+		{`
+s = "Stan"
+ds = "Stan".dup
+ds += " Lo"
+[s, ds]
+`, []interface{}{"Stan", "Stan Lo"}},
+	}
+
+
+	for i, tt := range tests {
+		v := initTestVM()
+		evaluated := v.testEval(t, tt.input, getFilename())
+		VerifyExpected(t, i, evaluated, tt.expected)
+		v.checkCFP(t, i, 0)
+		v.checkSP(t, i, 1)
+	}
+}