A Simple REPL

In the Embedding Hobbes chapter we built a simple Hobbes interpreter with two-way function bindings.

This allowed us to call C++ functions from within the Hobbes environment, and also, via the function pointer, to execute Hobbes code in the containing C++ code.

Here’s the full code listing. Afterwards we’ll dig into all the parts one by one:

#include <iostream>
#include <stdexcept>
#include <hobbes/hobbes.H>

int addTwo(int i){ return 2 + i; }

DEFINE_STRUCT(Writer,
  (size_t, age),
  (const hobbes::array<char>*, name)
);

typedef std::pair<int, const hobbes::array<char>*> Writer;

Writer* getWriter(){
  return hobbes::make<Writer>(34, hobbes::makeString("Sam"));
}

hobbes::array<Writer>* getWriters(){
  auto writers = hobbes::makeArray<Writer>(4);

  writers->data[0].age = 21;
  writers->data[0].name = hobbes::makeString("John");

  writers->data[1].age = 22;
  writers->data[1].name = hobbes::makeString("Paul");

  writers->data[2].age = 22;
  writers->data[2].name = hobbes::makeString("George");

  writers->data[3].age = 42;
  writers->data[3].name = hobbes::makeString("Sam");

  return writers;
}

typedef hobbes::variant<int, const hobbes::array<char>*> CountOrMessage;

CountOrMessage* classify(int i){
  if(i < 22){
    return hobbes::make<CountOrMessage>(i);
  }else{
    return hobbes::make<CountOrMessage>(hobbes::makeString("haha!"));
  }
}

int binaryIntFn(int (*pf)(int, int), int x){
  return pf(x, x);
}

int main() {
  hobbes::cc c;

  c.bind("addTwo", &addTwo);
  c.bind("getWriters", &getWriters);
  c.bind("getWriter", &getWriter);
  c.bind("classify", &classify);
  c.bind("binaryIntFn", &binaryIntFn);

  while (std::cin) {
    std::cout << "> " << std::flush;

    std::string line;
    std::getline(std::cin, line);
    if (line == ":q") break;

    try {
      c.compileFn<void()>("print(" + line + ")")();
    } catch (std::exception& ex) {
      std::cout << "*** " << ex.what();
    }

    std::cout << std::endl;
    hobbes::resetMemoryPool();
  }

  return 0;
}

Sections

Prelude

#include <iostream>
#include <stdexcept>
#include <hobbes/hobbes.H>

First comes the include statements for the c++ pre-processor. The Hobbes header file is available in the GitHub repo under the include directory.

A simple function

int addTwo(int i){ return 2 + i; }

This simple c++ funciton takes an int and adds 2 to it, returning the result. It’s used later, as an example of binding a c++ funciton in to the hobbes environment.

Binding custom datatypes

DEFINE_STRUCT(Writer,
  (size_t, age),
  (const hobbes::array<char>*, name)
);

typedef std::pair<int, const hobbes::array<char>*> Writer;

Writer* getWriter(){
  return hobbes::make<Writer>(34, hobbes::makeString("Sam"));
}

Here, we use the DEFINE_STRUCT macro to make Hobbes aware of a custom datatype, and use the make* functions for our custom type and for std::string. This makes Hobbes responsible for allocating space for this data, and allows it to deallocate the memory when it’s finished.

Arrays

hobbes::array<Writer>* getWriters(){
  auto writers = hobbes::makeArray<Writer>(4);

  writers->data[0].age = 21;
  writers->data[0].name = hobbes::makeString("John");

  writers->data[1].age = 22;
  writers->data[1].name = hobbes::makeString("Paul");

  writers->data[2].age = 22;
  writers->data[2].name = hobbes::makeString("George");

  writers->data[3].age = 42;
  writers->data[3].name = hobbes::makeString("Sam");

  return writers;
}

We’re extending the example to include makeArray, showing how Hobbes can interact with collections.

Variants

typedef hobbes::variant<int, const hobbes::array<char>*> CountOrMessage;

Firstly, we use a c++ typedef* to give a nice name to a Hobbes variant

CountOrMessage* classify(int i){
  if(i < 22){
    return hobbes::make<CountOrMessage>(i);
  }else{
    return hobbes::make<CountOrMessage>(hobbes::makeString("haha!"));
  }
}

Hobbes creates factory methods for each subclass in the variant, and so, depending on some external factor, we’re able to initialise either a count or a message.

Function pointers

int binaryIntFn(int (*pf)(int, int), int x){
  return pf(x, x);
}

The basic mechanism by which work is abstracted, and how we can externalise behaviour from Hobbes - allowing us to interact with Hobbes functionality from outside the environment. In this case we expect binaryIntFn to be called with two items - firstly, a function which takes two ints and returns an int, and secondly an int.

The result of the application of the function with the second argument twice is then returned to Hobbes as an int.

main()

Finally, the main method brings it all together:

int main() {
  hobbes::cc c;

  c.bind("addTwo", &addTwo);
  c.bind("getWriters", &getWriters);
  c.bind("getWriter", &getWriter);
  c.bind("classify", &classify);
  c.bind("binaryIntFn", &binaryIntFn);

  while (std::cin) {
    std::cout << "> " << std::flush;

    std::string line;
    std::getline(std::cin, line);
    if (line == ":q") break;

    try {
      c.compileFn<void()>("print(" + line + ")")();
    } catch (std::exception& ex) {
      std::cout << "*** " << ex.what();
    }

    std::cout << std::endl;
    hobbes::resetMemoryPool();
  }

  return 0;
}
  1. We initialise an instance of hobbes::cc on the stack;
  2. We bind five functions to it by their address;
  3. Then, in a loop, we print a prompt, accept a string from STDIN, and attempt to compile and execute the input string;
  4. Any failures are caught and reported;
  5. Finally, any unreferenced data left after the REPL loop is collected to avoid memory leaks.