Skip to content

Contibuting a Method

Shozo Hatta edited this page Sep 9, 2017 · 11 revisions

In Goby we have different classes and methods. If you are interested in adding one, here's what to do:

1. Find the Source

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

2. Keep the files organized and add comments

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)

  1. type declaration -- Add comments to describe the class for API doc
  2. variable/constant/interface declaration (optional)
  3. Goby class methods -- Add comments to each method for API doc
  4. Goby instance methods -- Add comments to each method for API doc

keep the instance methods in alphabetical order

  1. internal functions
  • a: initialization functions
  • b: polymorphic helper functions
  • c: other (non-polymorphic) helper functions
  1. addition (optional)
  • declarations (such as type)
  • helper functions

For adding comments as API doc, see Documenting-Goby-Code.

organization of functions 1 organization of functions 2

3. Add a Method

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 takes 20 as its argument.
  • blockFrame is a block brought into this method.
  • Object is always the returned value because in Goby everything is an object!

4. Add a Method: An Example

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)
        }
    },
}

5. Add Test Cases

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 in integer_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.

6. Add Test Cases: An Example

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)
    }
}

7. Finally

Run make test in project root directory to see if all tests passed. If so, congratulations! Now push and create a pull request!