Demystifying Data Structures

The last piece to unpack before we begin fusing programs is the set of data structures we've been using in our program definitions. Just like the add_1 function, these data structures are also part of the overall program definition and contain essential information that Fern uses during code generation and execution.

Let's look at the array object that mk_array produces:

    auto input = mk_array("input");
class ArrayCPU : public gern::AbstractDataType {
public:
    // Array must have a name!
    ArrayCPU(const std::string &name)
        : name(name) {
    }
    // Get the name to use in codegen. 
    std::string getName() const override {
        return name;
    }

    // Get the type to use in codegen. 
    std::string getType() const override {
        return "library::impl::ArrayCPU";
    }

    // The fields of the array are what we will eventually 
    // use to tile our program! In this represenatation
    // x denotes about the start of a subarray, while len
    // denotes the length of the subarray. Gern attaches
    // no semantic meaning to these fields, it just promises
    // to wire then up correctly! The ordering here does play a role!
    std::vector<Variable> getFields() const override {
        return {x, len};
    }
    // How can gern allocate an array?
    FunctionSignature getAllocateFunction() const override {
        return FunctionSignature{
            .name = "library::impl::ArrayCPU::allocate",
            .args = {x, len},
        };
    }
    // How can gern free an array?
    FunctionSignature getFreeFunction() const override {
        return FunctionSignature{
            .name = "destroy",
            .args = {},
        };
    }
    // If gern has a subarray, how can it insert into a parent array?
    FunctionSignature getInsertFunction() const override {
        return FunctionSignature{
            .name = "insert",
            .args = {x, len},
        };
    }

    // If gern wants to extract a subarray, how can it query it?
    FunctionSignature getQueryFunction() const override {
        return FunctionSignature{
            .name = "query",
            .args = {x, len},
        };
    }

protected:
    std::string name;
    Variable x{"x"};
    Variable len{"len"};
};

Similar to our array example, we could have instead used a matrix data structure. Let's take a look at the fieds of the matrix:

  std::vector<Variable> getFields() const override {
        return {x, y, l_x, l_y};
  }

Here, x and y point at the start of a submatrix, while l_x and l_y denote its height and width. Similar to the array, we can write a program that uses the matrix!

#include "helpers.h"
#include "library/matrix/annot/cpu-matrix.h"
#include "library/matrix/impl/cpu-matrix.h"

    int
    main()
{
    // ***** PROGRAM DEFINITION *****
    auto input = mk_matrix("input");
    auto output = mk_matrix("output");
    auto temp = mk_matrix("temp");

    annot::MatrixAddCPU add_1;

    Composable program({
        add_1(input, temp),
        add_1(temp, output),
    });

    // ***** PROGRAM EVALUATION *****
    library::impl::MatrixCPU a(10, 10, 10);
    a.ascending();
    library::impl::MatrixCPU b(10, 10, 10);

    auto runner = compile_program(program);
    runner.evaluate({
        {"input", &a},
        {"output", &b},
    });

    // ***** SANITY CHECK *****
    for (int i = 0; i < a.col; i++)
    {
        for (int j = 0; j < a.row; j++)
        {
            assert(a(i, j) + 2 == b(i, j));
        }
    }
}