A language and an embedded JIT compiler
a language, embedded compiler, and runtime for efficient dynamic expression evaluation, data storage and analysis
| section | description |
|---|---|
| Building | how to build and install hobbes |
| Embedding | use hobbes in C++ programs |
| Evaluation | evaluate basic hobbes expressions |
| Storage | record data for out-of-band analysis |
| Networking | interact with remote hobbes processes |
| Comprehensions | transform/sort/join/filter/group sequences for data analysis |
| Pattern Matching | efficiently classify and destructure data |
| Parsing | parse text with LALR(1) grammars |
| Type Classes | overloading and compile-time calculation |
| Unqualifier Modules | user-defined "compiler plugins" for custom constraint handling |
Hobbes is built for high performance integration with C/C++ applications. While Hobbes is a strongly typed language that offers compile-time checks, it doesn't have a sandboxed runtime environment or runtime safety features. By design, Hobbes gives direct access to memory and does not have array bounds checks. Additionally, Hobbes supports compilation and execution of native code remotely over a network (RPC). This feature is meant for use within your trusted internal network only. If you choose to utilize such functionality, you need to be aware of these design choices and understand the security implications.
To build hobbes, you will need LLVM 3.3 or later, cmake 3.19 or later, GNU gcc 4.8 or later, and a version 2.5 or later Linux kernel.
With LLVM, cmake, and g++ installed, after downloading this code you should be able to build and install hobbes just by running:
$ cmake .
$ make
$ make installDepending on where you've installed LLVM, this might not work until you give cmake the path to LLVM's cmake modules. In particular, you want the path to LLVM's LLVMConfig.cmake file. If you set the environment variable LLVM_DIR to point to that directory, then the previous steps should complete successfully.
The build process will produce a static library, libhobbes.a, which can be linked into a C++ executable (if you want to use hobbes in a .so instead, the build produces a different static library to use, libhobbes-pic.a).
In addition, the build process will produce two utility programs, hi and hog. The hi program is a basic interactive interpreter for hobbes expressions and the hog program will efficiently record structured data produced by applications into data files (these files can be loaded and queried at the same time by hi). The source code for these programs can be informative as well, because they demonstrate many different aspects of the hobbes API.
Let's consider how to embed hobbes in a simple C++ program. This code implements a very basic shell, similar to hi:
#include
#include
#include
int main() {
hobbes::cc c;
while (std::cin) {
std::cout " ("print(" + line + ")")();
} catch (std::exception& ex) {
std::cout 'c'
'c'
> 42
42
> 3.14159
3.14159In total, these are the set of supported primitive types/constants:
| name | example | description |
|---|---|---|
| unit | () |
like 'void' in C, a trivial type with just one value |
| bool | false |
either true or false |
| char | 'c' |
a single character of text |
| byte | 0Xff |
a single byte (0-255) |
| short | 42S |
a 2-byte number |
| int | 42 |
a 4-byte number |
| long | 42L |
an 8-byte number |
| float | 42.0f |
a 4-byte floating point value |
| double | 42.0 |
an 8-byte floating point value |
These primitives can be combined with arrays:
> [1, 2, 3]
[1, 2, 3]
> "foobar"
"foobar"
> 0xdeadbeef
0xdeadbeefThey can also be combined with records/tuples:
> {name="Jimmy", age=45, job="programmer"}
{name="Jimmy", age=45, job="programmer"}
> ("Jimmy", 45, "programmer")
("Jimmy", 45, "programmer")Or with arrays of records/tuples, where they will print conveniently as a table:
> [{name="Jimmy", age=45, job="programmer"}, {name="Chicken", age=40, job="programmer"}]
name age job
------- --- ----------
Jimmy 45 programmer
Chicken 40 programmer
> [("Jimmy", 45, "programmer"),("Chicken", 40, "programmer")]
Jimmy 45 programmer
Chicken 40 programmerWe can combine types with variants or sums (the "nameless" form of variants, as tuples are to records). Because "naked" variant introduction is incomplete by definition, we may sometimes need to introduce explicit type annotations:
> |food="pizza"|::|vehicle:int,food:[char]|
|food="pizza"|
> |1="pizza"|::(int+[char])
|1="pizza"|Also, variants can be combined with isorecursive types to make linked lists. These have a convenient form when printed, and built-in functions to avoid the recursive and variant type boilerplate:
> roll(|1=("chicken", roll(|1=("hot dog", roll(|1=("pizza", roll(|0=()|))|))|))|) :: ^x.(()+([char]*x))
"chicken":"hot dog":"pizza":[]
> cons("chicken", cons("hot dog", cons("pizza", nil())))
"chicken":"hot dog":"pizza":[]These types and type constructors together make up the "algebraic data types" common in the ML family of programming languages (SML, ocaml, Haskell, ...). The syntax and names for these types also come from ML and common academic programming language textbooks as in TaPL and PFPL.
As in Haskell, hobbes supports a form of qualified types with type classes and even user-defined constraint resolvers (which we will see in more detail later). Among many other uses, this allows both mixed-type arithmetic and type inference to coexist:
> 0X01+2+3.0+4L+5S
15
> (\\x y z u v.x+y+z+u+v)(0X01,2,3S,4L,5.0)
15We can also use the hi program (a slightly more complex interpreter than the example earlier, distributed with hobbes) to evaluate expressions like this and also inspect their types. For example, the types for the primitive expressions we considered earlier can be queried:
$ hi
hi : an interactive shell for hobbes
type ':h' for help on commands
> :t ()
()
> :t false
bool
> :t 'c'
char
> :t 0Xff
byte
> :t 42S
short
> :t 42
int
> :t 42L
long
> :t 42.0f
float
> :t 42.0
doubleAnd the types for record, array, variant and recursive values:
> :t [1..10]
[int]
> :t "foobar"
[char]
> :t 0xdeadbeef
[byte]
> :t {name="Jimmy", age=45, job="programmer"}
{ name:[char], age:int, job:[char] }
> :t ("Jimmy", 45, "programmer")
([char] * int * [char])
> :t cons("foo",nil())
^x.(() + ([char] * x))Also for qualified types, we can use this feature to view the set of open/unresolved type constraints:
> :t \\x.x.foo
(a.foo::b) => (a) -> b
> :t .foo
(a.foo::b) => (a) -> bHere we see that the type of a simple function which just returns the value of the "foo" field in its input (both in the fully explicit \\x.x.foo form as well as the equivalent .foo shorthand) has a constraint (the part to the left of the => in its type -- the same as in Haskell). In this case, the entire type states that if a type a has a field named foo with type b, the function has type a -> b. The additional complexity of this constraint allows this function to work with any type a meeting that condition. This particular constraint allows a kind of duck typing for functions.
If we're using hobbes inside of a C++ process (as opposed to just running scripts with hi), at some point we will need to bind C++ values (typically functions) to a hobbes::cc instance so that they can be used in dynamic expressions at a later stage. Much of this work is automated, often allowing us to avoid thinking about binding logic.
For example, consider a C++ function that we might have already defined:
int applyDiscreteWeighting(int x, int y) { return x * y; }Now we can import this definition into our previous program, and bind it to our hobbes::cc instance by adding this line:
c.bind("applyDiscreteWeighting", &applyDiscreteWeighting);Then when we run our program, its shell will allow use of this function and will reject any expression using it in a way inconsistent with its type structure:
> applyDiscreteWeighting(3, 4)
12
> applyDiscreteWeighting(3, "pizza")
*** stdin:1,1-22: Cannot unify types: int != [char]In other cases, it may be more convenient to use a type understood natively by hobbes and with dynamic memory allocation:
typedef std::pair*> MyRecord;
const hobbes::array* loadRecords(int key) {
static const char* names[] = { "chicken", "hot dog", "foobar", "jimmy" };
auto rs = hobbes::makeArray(key);
for (size_t i = 0; i size; ++i) {
rs->data[i].first = i;
rs->data[i].second = hobbes::makeString(names[i % 4]);
}
return rs;
}
// [...]
c.bind("loadRecords", &loadRecords);
// [...]These calls to hobbes::makeArray and hobbes::makeString will allocate out of the calling thread's memory region and will produce data that hobbes knows how to generically destructure and print in a convenient form, as we can see by running this in our test program again:
> loadRecords(10)
0 chicken
1 hot dog
2 foobar
3 jimmy
4 chicken
5 hot dog
6 foobar
7 jimmy
8 chicken
9 hot dogIf we'd prefer to represent records by structs with significant field names rather than as tuples, we can instead define our type with the DEFINE_STRUCT macro (this is purely a syntactic difference -- the final runtime representation of tuples and structs is identical in hobbes). For example, if we'd defined the MyRecord type in the previous example this way:
DEFINE_STRUCT(MyRecord,
(size_t, index),
(const hobbes::array*, name)
);And change the initialization code in loadRecords to refer to these fields by name:
// [...]
rs->data[i].index = i;
rs->data[i].name = hobbes::makeString(names[i % 4]);
// [...]Then we can access those fields by name from hobbes and e.g. tables of such records will be printed with a header showing these field names:
> loadRecords(10)
index name
----- -------
0 chicken
1 hot dog
2 foobar
3 jimmy
4 chicken
5 hot dog
6 foobar
7 jimmy
8 chicken
9 hot dogWe can also bind "in reverse" using C function types, so that rather than making our C++ code appear to be valid hobbes symbols, we can make hobbes expressions appear to be valid C++ symbols. For example, consider a C++ binding like:
int iter2(int (*pf)(int,int), int x) {
return pf(pf(x, x), pf(x, x));
}
// [...]
c.bind("iter2", &iter2);
// [...]Here we've bound a function that itself takes a function as input, and we can use the hobbes syntax for anonymous functions to decide logic that this C++ function will use:
> iter2(\\x y.x*x+y, 2)
42Variants can be pass
No open issues yet, or sync has not completed.