-
Notifications
You must be signed in to change notification settings - Fork 171
Contibuting a Method
In Goby we have different classes and methods. If you are interested in adding one, here's what to do:
All built-in classes are all in /vm
directory, each file represents a class. Its instance methods are stored in an array variable, with its name prefixed with "builtin" and suffixed with "Methods". The following table offers several naming examples:
class | file | methods stored in variable |
---|---|---|
Integer | integer.go | builtinIntegerMethods |
String | string.go | builtinStringMethods |
Array | array.go | builtinArrayMethods |
In vm/
dir, the functions in each builtin (native) class file are organized as follows:
(except vm-related files such as vm.go, instruction.go or thread.go)
- type declaration -- Add comments to describe the class for API doc
- variable/constant/interface declaration (optional)
- Goby class methods -- Add comments to each method for API doc
- Goby instance methods -- Add comments to each method for API doc
keep the instance methods in alphabetical order
- internal functions
- a: initialization functions
- b: polymorphic helper functions
- c: other (non-polymorphic) helper functions
- addition (optional)
- declarations (such as type)
- helper functions
For adding comments as API doc, see Documenting-Goby-Code.
Adding a new method is adding an element in the array. The element includes two parts, Name
and Fn
. A Name
is what the method is called, and Fn
usually looks like:
func(receiver Object) builtinMethodBody {
return func(vm *VM, args []Object, blockFrame *callFrame) Object {
// ...
}
}
Usually you don't have to modify this structure, neither variable names, but you do need to know several things:
-
receiver
is the object that performs this action. For instance, if a code runs like"10".to_i
, the"10"
is the receiver. -
arg
is the arguments of this method. For example,10 + 20
takes20
as its argument. -
blockFrame
is a block brought into this method. -
Object
is always the returned value because in Goby everything is an object!
Let's try adding a index
method to the Array
class that allows us to find a String
or Integer
in an array!
Supposedly we're building a method that allows us to do this:
a = ["a", "b", "c", "d", 2]
a.index("a") #=> 0
c = a.index do |x|
x == "c"
end
c #=> 2
Find the builtinArrayMethods
variable in vm/array.go
. Follow the comments and see how we add it:
{
// First we give it a name.
Name: "index",
// Remember, receiver is the Array instance.
Fn: func(receiver Object) builtinMethodBody {
// `vm` is a pointer to VM.
// `args` is an array of arguments.
//
// Let's say our method will run like `array.index("c")` and return "c", so the returned value is:
//
// []Object{
// 0: StringObject{
// Class: *RString
// Value: "c"
// }
// }
//
// `blockFrame` is our block argument, this method returns `nil` if there's no block.
return func(vm *VM, args []Object, blockFrame *callFrame) Object {
arr := receiver.(*ArrayObject) // Retrieve array ["a", "b", "c", "d", 2]
arg = args[0] // Get the value we are looking for in the array
// Check the type of object
elInt, isInt := arg.(*IntegerObject)
elStr, isStr := arg.(*StringObject)
// `index` searches given value in the array and returns its index if found
// i: index of element
// o: value to compare with arg[0]
for i, o := range arr.Elements {
switch o.(type) {
case *IntegerObject:
el := o.(*IntegerObject)
// If both objects are integers, returns an IntegerObject i
if isInt && el.equal(elInt) {
return initilaizeInteger(i)
}
case *StringObject:
el := o.(*StringObject)
if isStr && el.equal(elStr) {
return initilaizeInteger(i)
}
}
}
return initilaizeInteger(nil)
}
},
}
We can't have a method without a test case. Let's add a test case.
The best way to test it is to write Goby code directly and see if evaluating the code returns the desired result. We're using the built-in testing package in Go, so please follow Go's convention in writing tests, especially keeping the failing message understandable.
- Find the test file for your class. If you added a method in
integer.go
then add a test ininteger_test.go
. - Create a function name starting with "Test", so Go will run this function in testing.
- In this function, use
testEval()
function to evaluate a piece of Goby code. - Validate if the returned object is as you expect. If not, write a failing message. The message should include what we expect, and what is the actual output.
Let's add a test case for the #index
method we just added. In array_test.go
, we add the following code block.
func TestIndexMethod(t *testing.T) {
tests := []struct {
input string
expected *IntegerObject
}{
{
// The following is a piece of Goby code
`
a = [1, 2, "a", 3, "5", "r"]
a.index("r")
`,
// We expect it to generate an Integer object
initilaizeInteger(5)},
}
for _, tt := range tests {
// Evaluate the code
evaluated := testEval(t, tt.input)
// Use our function to evaluate if it is as expected
testIntegerObject(t, evaluated, tt.expected.Value)
}
}
Run make test
in project root directory to see if all tests passed. If so, congratulations! Now push and create a pull request!