Finish initial draft of runtime posts.

This commit is contained in:
Danila Fedorin 2019-10-30 14:21:13 -07:00
parent 4b5e2f4454
commit a834fd578e
4 changed files with 260 additions and 69 deletions

View File

@ -0,0 +1,31 @@
#include "../runtime.h"
void f_add(struct stack* s) {
struct node_num* left = (struct node_num*) eval(stack_peek(s, 0));
struct node_num* right = (struct node_num*) eval(stack_peek(s, 1));
stack_push(s, (struct node_base*) alloc_num(left->value + right->value));
}
void f_main(struct stack* s) {
// PushInt 320
stack_push(s, (struct node_base*) alloc_num(320));
// PushInt 6
stack_push(s, (struct node_base*) alloc_num(6));
// PushGlobal f_add (the function for +)
stack_push(s, (struct node_base*) alloc_global(f_add, 2));
struct node_base* left;
struct node_base* right;
// MkApp
left = stack_pop(s);
right = stack_pop(s);
stack_push(s, (struct node_base*) alloc_app(left, right));
// MkApp
left = stack_pop(s);
right = stack_pop(s);
stack_push(s, (struct node_base*) alloc_app(left, right));
}

View File

@ -1,48 +1,7 @@
#include <stdint.h> #include <stdint.h>
#include <stdlib.h>
#include <assert.h> #include <assert.h>
#include <memory.h>
struct stack; #include "runtime.h"
enum node_tag {
NODE_APP,
NODE_NUM,
NODE_GLOBAL,
NODE_IND,
NODE_PACK
};
struct node_base {
enum node_tag tag;
};
struct node_app {
struct node_base base;
struct node_base* left;
struct node_base* right;
};
struct node_num {
struct node_base base;
int32_t value;
};
struct node_global {
struct node_base base;
int32_t arity;
void (*function)(struct stack*);
};
struct node_ind {
struct node_base base;
struct node_base* next;
};
struct node_data {
struct node_base base;
int8_t tag;
struct node_base** array;
};
struct node_base* alloc_node() { struct node_base* alloc_node() {
struct node_base* new_node = malloc(sizeof(struct node_app)); struct node_base* new_node = malloc(sizeof(struct node_app));
@ -50,14 +9,38 @@ struct node_base* alloc_node() {
return new_node; return new_node;
} }
struct stack { struct node_app* alloc_app(struct node_base* l, struct node_base* r) {
size_t size; struct node_app* node = (struct node_app*) alloc_node();
size_t count; node->base.tag = NODE_APP;
struct node_base** data; node->left = l;
}; node->right = r;
return node;
}
struct node_num* alloc_num(int32_t n) {
struct node_num* node = (struct node_num*) alloc_node();
node->base.tag = NODE_NUM;
node->value = n;
return node;
}
struct node_global* alloc_global(void (*f)(struct stack*), int32_t a) {
struct node_global* node = (struct node_global*) alloc_node();
node->base.tag = NODE_GLOBAL;
node->arity = a;
node->function = f;
return node;
}
struct node_ind* alloc_ind(struct node_base* n) {
struct node_ind* node = (struct node_ind*) alloc_node();
node->base.tag = NODE_IND;
node->next = n;
return node;
}
void stack_init(struct stack* s) { void stack_init(struct stack* s) {
s->size = 0; s->size = 4;
s->count = 0; s->count = 0;
s->data = malloc(sizeof(*s->data) * s->size); s->data = malloc(sizeof(*s->data) * s->size);
assert(s->data != NULL); assert(s->data != NULL);
@ -77,7 +60,7 @@ void stack_push(struct stack* s, struct node_base* n) {
struct node_base* stack_pop(struct stack* s) { struct node_base* stack_pop(struct stack* s) {
assert(s->count > 0); assert(s->count > 0);
s->count--; return s->data[--s->count];
} }
struct node_base* stack_peek(struct stack* s, size_t o) { struct node_base* stack_peek(struct stack* s, size_t o) {
@ -105,21 +88,73 @@ void stack_update(struct stack* s, size_t o) {
void stack_alloc(struct stack* s, size_t o) { void stack_alloc(struct stack* s, size_t o) {
while(o--) { while(o--) {
struct node_ind* new_node = (struct node_ind*) alloc_node(); stack_push(s, (struct node_base*) alloc_ind(NULL));
new_node->base.tag = NODE_IND;
new_node->next = NULL;
stack_push(s, (struct node_base*) new_node);
} }
} }
void eval(struct node_base* n); void stack_pack(struct stack* s, size_t n, int8_t t) {
assert(s->count >= n);
struct node_base** data = malloc(sizeof(*data) * n);
assert(data != NULL);
memcpy(data, &s->data[s->count - 1 - n], n * sizeof(*data));
struct node_data* new_node = (struct node_data*) alloc_node();
new_node->array = data;
new_node->base.tag = NODE_DATA;
new_node->tag = t;
stack_popn(s, n);
stack_push(s, (struct node_base*) new_node);
}
void stack_split(struct stack* s, size_t n) {
struct node_data* node = (struct node_data*) stack_pop(s);
for(size_t i = 0; i < n; i++) {
stack_push(s, node->array[i]);
}
}
void unwind(struct stack* s) {
while(1) {
struct node_base* peek = stack_peek(s, 0);
if(peek->tag == NODE_APP) {
struct node_app* n = (struct node_app*) peek;
stack_push(s, n->left);
} else if(peek->tag == NODE_GLOBAL) {
struct node_global* n = (struct node_global*) peek;
assert(s->count > n->arity);
for(size_t i = 1; i <= n->arity; i++) {
s->data[s->count - i]
= ((struct node_app*) s->data[s->count - i - 1])->right;
}
n->function(s);
} else if(peek->tag == NODE_IND) {
struct node_ind* n = (struct node_ind*) peek;
stack_pop(s);
stack_push(s, n->next);
} else {
break;
}
}
}
struct node_base* eval(struct node_base* n) {
struct stack program_stack;
stack_init(&program_stack);
stack_push(&program_stack, n);
unwind(&program_stack);
struct node_base* result = stack_pop(&program_stack);
stack_free(&program_stack);
return result;
}
extern void f_main(struct stack* s); extern void f_main(struct stack* s);
int main(int argc, char** argv) { int main(int argc, char** argv) {
struct node_global* first_node = (struct node_global*) alloc_node(); struct node_global* first_node = alloc_global(f_main, 0);
first_node->base.tag = NODE_GLOBAL; struct node_base* result = eval((struct node_base*) first_node);
first_node->arity = 0; printf("%d\n", ((struct node_num*) result)->value);
first_node->function = f_main;
eval((struct node_base*) first_node);
} }

View File

@ -0,0 +1,69 @@
#pragma once
#include <stdlib.h>
struct stack;
enum node_tag {
NODE_APP,
NODE_NUM,
NODE_GLOBAL,
NODE_IND,
NODE_DATA
};
struct node_base {
enum node_tag tag;
};
struct node_app {
struct node_base base;
struct node_base* left;
struct node_base* right;
};
struct node_num {
struct node_base base;
int32_t value;
};
struct node_global {
struct node_base base;
int32_t arity;
void (*function)(struct stack*);
};
struct node_ind {
struct node_base base;
struct node_base* next;
};
struct node_data {
struct node_base base;
int8_t tag;
struct node_base** array;
};
struct node_base* alloc_node();
struct node_app* alloc_app(struct node_base* l, struct node_base* r);
struct node_num* alloc_num(int32_t n);
struct node_global* alloc_global(void (*f)(struct stack*), int32_t a);
struct node_ind* alloc_ind(struct node_base* n);
struct stack {
size_t size;
size_t count;
struct node_base** data;
};
void stack_free(struct stack* s);
void stack_push(struct stack* s, struct node_base* n);
struct node_base* stack_pop(struct stack* s);
struct node_base* stack_peek(struct stack* s, size_t o);
void stack_popn(struct stack* s, size_t n);
void stack_slide(struct stack* s, size_t n);
void stack_update(struct stack* s, size_t o);
void stack_alloc(struct stack* s, size_t o);
void stack_pack(struct stack* s, size_t n, int8_t t);
void stack_split(struct stack* s, size_t n);
struct node_base* eval(struct node_base* n);

View File

@ -9,12 +9,12 @@ Wikipedia has the following definition for a __runtime__:
> A [runtime] primarily implements portions of an execution model. > A [runtime] primarily implements portions of an execution model.
We know what our execution model is! We talked about it in Part 5 - it's the We know what our execution model is! We talked about it in Part 5 - it's the
lazy graph reduction we've been talking about. Creating and manipulating lazy graph reduction we've specified. Creating and manipulating
graph nodes is slightly above hardware level, and all programs in our graph nodes is slightly above hardware level, and all programs in our
functional language will rely on such manipulation (it's how they run!). Furthermore, functional language will rely on such manipulation (it's how they run!). Furthermore,
most G-machine instructions are also above hardware level (especially unwind!). most G-machine instructions are also above hardware level (especially unwind!).
Push and Slide and other instructions are pretty complex instructions. Push and Slide and other instructions are pretty complex.
Most computers aren't stack machines. We'll have to implement Most computers aren't stack machines. We'll have to implement
our own stack, and whenever a graph-building function will want to modify our own stack, and whenever a graph-building function will want to modify
the stack, it will have to call library routines for our stack implementation: the stack, it will have to call library routines for our stack implementation:
@ -48,7 +48,7 @@ implementation of the G-machine compilation.
We can start working on an implementation of the runtime right now, We can start working on an implementation of the runtime right now,
beginning with the nodes: beginning with the nodes:
{{< codelines "C++" "compiler/07/runtime.c" 5 51 >}} {{< codelines "C++" "compiler/07/runtime.h" 4 50 >}}
We have a variety of different nodes that can be on the stack, but without We have a variety of different nodes that can be on the stack, but without
the magic of C++'s `vtable` and RTTI, we have to take care of the bookkeeping the magic of C++'s `vtable` and RTTI, we have to take care of the bookkeeping
@ -67,7 +67,16 @@ expressions with the results of their evaluation), changing their type.
We then want to be able to change a node without reallocating memory. We then want to be able to change a node without reallocating memory.
Since the biggest node we have is `node_app`, that's the one we choose. Since the biggest node we have is `node_app`, that's the one we choose.
We now move on to implement some stack operations. Let's list them off: Finally, to make it easier to create nodes from our generated code,
we add helper functions like `alloc_num`, which allocate a given
node type, and set its tag and member fields appropriately. We
don't include such a function for `node_data`, since this
node will be created only in one possible way.
Here's the implementation:
{{< codelines "C" "compiler/07/runtime.c" 6 40 >}}
We now move on to implement some stack operations. Let's list them:
* `stack_init` and `stack_free` - one allocates memory for the stack, * `stack_init` and `stack_free` - one allocates memory for the stack,
the other releases it. the other releases it.
@ -79,11 +88,15 @@ next several nodes.
* `stack_update` - turns the node at the offset into an indirection to the result, * `stack_update` - turns the node at the offset into an indirection to the result,
which we will use for lazy evaluation (modifying expressions with their reduced forms). which we will use for lazy evaluation (modifying expressions with their reduced forms).
* `stack_alloc` - allocate indirection nodes on the stack. We will use this later. * `stack_alloc` - allocate indirection nodes on the stack. We will use this later.
* `stack_pack` and `stack_split` - Wrap and unwrap constructors on the stack.
Here's the implementation: We declare these in a header:
{{< codelines "C++" "compiler/07/runtime.c" 53 113 >}} {{< codelines "C" "compiler/07/runtime.h" 52 67 >}}
Let's not talk about how this will connect to the code we generate. To get And implement them as follows:
{{< codelines "C" "compiler/07/runtime.c" 42 116 >}}
Let's now talk about how this will connect to the code we generate. To get
a quick example, consider the `node_global` struct that we have declared above. a quick example, consider the `node_global` struct that we have declared above.
It has a member `function`, which is a __function pointer__ to a function It has a member `function`, which is a __function pointer__ to a function
that takes a stack and returns void. that takes a stack and returns void.
@ -98,8 +111,51 @@ we express a compiled top-level function as a subroutine that takes a stack,
and returns void. A global node holds in it the pointer to the function that it will call. and returns void. A global node holds in it the pointer to the function that it will call.
When our program will start, it will assume that there exists a top-level When our program will start, it will assume that there exists a top-level
function `main` that takes 0 parameters. It will take that function, call it function `f_main` that takes 0 parameters. It will take that function, call it
to produce the initial graph, and then let the unwind loop take care of the evaluation. to produce the initial graph, and then let the unwind loop take care of the evaluation.
Thus, our program will initially look like this: Thus, our program will initially look like this:
{{< codelines "C++" "compiler/07/runtime.c" 117 125 >}} {{< codelines "C" "compiler/07/runtime.c" 154 159 >}}
As we said, we expect an externally-declared subroutine `f_main`. We construct
a global node for `f_main` with arity 0, and then start the execution using a function `eval`.
What's `eval`, though? It's the function that will take care of creating
a new stack, and evaluating the node that is passed to it using
our unwind loop. `eval` itself is pretty terse:
{{< codelines "C" "compiler/07/runtime.c" 144 152 >}}
We create a fresh program stack, start it off with whatever node
we want to evaluate, and have `unwind` take care of the rest.
`unwind` is a direct implementation of the rules from Part 5:
{{< codelines "C" "compiler/07/runtime.c" 118 142 >}}
We can now come up with some simple programs. Let's try
writing out, by hand, `main = { 320 + 6 }`. We end up with:
{{< codeblock "C" "compiler/07/examples/runtime1.c" >}}
If we add to the bottom of our `main` the following code:
```C
printf("%d\n", ((struct node_num*) result)->value);
```
And compile and run our code:
```
gcc runtime.c examples/runtime1.c
./a.out
```
We get the output `326`, which is exactly correct!
We now have a common set of functions and declarations
that serve to support the code we generate from our compiler.
Although this time, we wrote out `f_main` by hand, we will soon
use LLVM to generate code for `f_main` and more. Once we get
that going, we be able to compile our code!
Next time, we will start work on converting our G-machine instructions
into machine code. We will set up LLVM and get our very first
fully functional compiled programs in Part 8 - LLVM.