apfl/src/compile.c

1072 lines
40 KiB
C
Raw Normal View History

Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "apfl.h"
#include "compile.h"
#include "bytecode.h"
#include "resizable.h"
#include "strings.h"
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
struct compiler {
struct gc *gc;
struct apfl_error error;
size_t line;
};
#define TRY(b) do { if (!(b)) { return false; } } while(0)
#define MALLOC_FAIL_IF_NULL(compiler, x) \
do { \
if ((x) == NULL) { \
compiler->error = apfl_error_simple(APFL_ERR_MALLOC_FAILED); \
return false; \
} \
} while (0)
static bool compile_expr(struct compiler *, struct apfl_expr *, struct instruction_list *, struct apfl_string *name);
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
static bool
malloc_failure_on_false(struct compiler *compiler, bool b)
{
if (!b) {
compiler->error = apfl_error_simple(APFL_ERR_MALLOC_FAILED);
}
return b;
}
static bool
ilist_ensure_cap(struct compiler *compiler, struct instruction_list *ilist, size_t n)
{
return malloc_failure_on_false(compiler, apfl_resizable_ensure_cap_for_more_elements(
compiler->gc->allocator,
sizeof(union instruction_or_arg),
(void **)&ilist->instructions,
ilist->len,
&ilist->cap,
n
));
}
#define ILIST_PUT(ilist, member, data, dbgfmt, ...) \
assert(ilist->cap > 0); \
assert(ilist->len < ilist->cap); \
\
ilist->instructions[ilist->len] = (union instruction_or_arg) { \
.member = data, \
}; \
ilist->len++;
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
static void
ilist_put_insn(struct instruction_list *ilist, enum instruction instruction)
{
ILIST_PUT(ilist, instruction, instruction, "put_insn: %s", apfl_instruction_to_string(instruction))
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
}
static void
ilist_put_number(struct instruction_list *ilist, apfl_number number)
{
ILIST_PUT(ilist, number, number, "put_number: %lf", number)
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
}
static void
ilist_put_count(struct instruction_list *ilist, size_t count)
{
ILIST_PUT(ilist, count, count, "put_count: %d", (int)count)
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
}
static void
ilist_put_string(struct instruction_list *ilist, struct apfl_string *string)
{
ILIST_PUT(ilist, string, string, "put_string: " APFL_STR_FMT, APFL_STR_FMT_ARGS(*string))
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
}
static void
ilist_put_body(struct instruction_list *ilist, struct instruction_list *body)
{
ILIST_PUT(ilist, body, body, "put_body: %p", (void *)body)
}
static void
ilist_put_index(struct instruction_list *ilist, size_t index)
{
ILIST_PUT(ilist, index, index, "put_index: %d", (int)index)
}
static void
ilist_put_matcher(struct instruction_list *ilist, struct matcher_instruction_list *milist)
{
ILIST_PUT(ilist, matcher, milist, "put_matcher: %p", (void *)milist)
}
static bool
milist_ensure_cap(struct compiler *compiler, struct matcher_instruction_list *milist, size_t n)
{
return malloc_failure_on_false(compiler, apfl_resizable_ensure_cap_for_more_elements(
compiler->gc->allocator,
sizeof(union matcher_instruction_or_arg),
(void **)&milist->instructions,
milist->len,
&milist->cap,
n
));
}
#define MILIST_PUT(milist, member, value) \
assert(milist->cap > 0); \
assert(milist->len < milist->cap); \
\
milist->instructions[milist->len] = (union matcher_instruction_or_arg) { \
.member = value, \
}; \
milist->len++;
static void
milist_put_insn(struct matcher_instruction_list *milist, enum matcher_instruction instruction)
{
MILIST_PUT(milist, instruction, instruction)
}
static void
milist_put_index(struct matcher_instruction_list *milist, size_t index)
{
MILIST_PUT(milist, index, index)
}
static void
milist_put_len(struct matcher_instruction_list *milist, size_t len)
{
MILIST_PUT(milist, len, len)
}
static void
milist_put_string(struct matcher_instruction_list *milist, struct apfl_string *string)
{
MILIST_PUT(milist, string, string)
}
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
static bool
string_move_into_new(struct compiler *compiler, struct apfl_string **out, struct apfl_string *in)
{
struct apfl_string *str = apfl_string_move_into_new_gc_string(compiler->gc, in);
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
if (str == NULL) {
compiler->error = apfl_error_simple(APFL_ERR_MALLOC_FAILED);
return false;
}
*out = str;
return true;
}
static bool
compile_constant(struct compiler *compiler, struct apfl_expr_const *constant, struct instruction_list *ilist)
{
switch (constant->type) {
case APFL_EXPR_CONST_NIL:
TRY(ilist_ensure_cap(compiler, ilist, 1));
ilist_put_insn(ilist, INSN_NIL);
return true;
case APFL_EXPR_CONST_BOOLEAN:
TRY(ilist_ensure_cap(compiler, ilist, 1));
ilist_put_insn(ilist, constant->boolean ? INSN_TRUE : INSN_FALSE);
return true;
case APFL_EXPR_CONST_NUMBER:
TRY(ilist_ensure_cap(compiler, ilist, 2));
ilist_put_insn(ilist, INSN_NUMBER);
ilist_put_number(ilist, constant->number);
return true;
case APFL_EXPR_CONST_STRING:
TRY(ilist_ensure_cap(compiler, ilist, 2));
struct apfl_string *str;
TRY(string_move_into_new(compiler, &str, &constant->string));
ilist_put_insn(ilist, INSN_STRING);
ilist_put_string(ilist, str);
return true;
}
assert(false);
return false;
}
static bool
compile_list(struct compiler *compiler, struct apfl_expr_list *list, struct instruction_list *ilist)
{
TRY(ilist_ensure_cap(compiler, ilist, 2));
ilist_put_insn(ilist, INSN_LIST);
ilist_put_count(ilist, list->len);
for (size_t i = 0; i < list->len; i++) {
struct apfl_expr_list_item *item = &list->items[i];
TRY(compile_expr(compiler, item->expr, ilist, NULL));
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
TRY(ilist_ensure_cap(compiler, ilist, 1));
ilist_put_insn(ilist, item->expand ? INSN_LIST_EXPAND_INTO : INSN_LIST_APPEND);
}
return true;
}
static bool
compile_dict(struct compiler *compiler, struct apfl_expr_dict *dict, struct instruction_list *ilist)
{
TRY(ilist_ensure_cap(compiler, ilist, 1));
ilist_put_insn(ilist, INSN_DICT);
for (size_t i = 0; i < dict->len; i++) {
struct apfl_expr_dict_pair *pair = &dict->items[i];
TRY(compile_expr(compiler, pair->k, ilist, NULL));
TRY(compile_expr(compiler, pair->v, ilist, NULL));
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
TRY(ilist_ensure_cap(compiler, ilist, 1));
ilist_put_insn(ilist, INSN_DICT_APPEND_KVPAIR);
}
return true;
}
static bool
compile_dot(struct compiler *compiler, struct apfl_expr_dot *dot, struct instruction_list *ilist)
{
TRY(compile_expr(compiler, dot->lhs, ilist, NULL));
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
TRY(ilist_ensure_cap(compiler, ilist, 3));
struct apfl_string *str;
TRY(string_move_into_new(compiler, &str, &dot->rhs));
ilist_put_insn(ilist, INSN_STRING);
ilist_put_string(ilist, str);
ilist_put_insn(ilist, INSN_GET_MEMBER);
return true;
}
static bool
compile_at(struct compiler *compiler, struct apfl_expr_pair *at, struct instruction_list *ilist)
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
{
TRY(compile_expr(compiler, at->lhs, ilist, NULL));
TRY(compile_expr(compiler, at->rhs, ilist, NULL));
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
TRY(ilist_ensure_cap(compiler, ilist, 1));
ilist_put_insn(ilist, INSN_GET_MEMBER);
return true;
}
static bool
compile_var(struct compiler *compiler, struct apfl_string *var, struct instruction_list *ilist)
{
TRY(ilist_ensure_cap(compiler, ilist, 2));
struct apfl_string *str;
TRY(string_move_into_new(compiler, &str, var));
ilist_put_insn(ilist, INSN_VAR_GET);
ilist_put_string(ilist, str);
return true;
}
static bool
compile_pair(struct compiler *compiler, struct apfl_expr_pair *pair, struct instruction_list *ilist)
{
TRY(compile_expr(compiler, pair->lhs, ilist, NULL));
TRY(compile_expr(compiler, pair->rhs, ilist, NULL));
TRY(ilist_ensure_cap(compiler, ilist, 1));
ilist_put_insn(ilist, INSN_BUILD_PAIR);
return true;
}
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
static bool
compile_simple_assignment(
struct compiler *compiler,
struct apfl_string *var,
struct apfl_expr *rhs,
struct instruction_list *ilist,
enum instruction new_insn,
enum instruction set_insn
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
) {
TRY(ilist_ensure_cap(compiler, ilist, 2));
struct apfl_string *str;
TRY(string_move_into_new(compiler, &str, var));
ilist_put_insn(ilist, new_insn);
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
ilist_put_string(ilist, str);
TRY(compile_expr(compiler, rhs, ilist, str));
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
TRY(ilist_ensure_cap(compiler, ilist, 2));
ilist_put_insn(ilist, set_insn);
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
ilist_put_string(ilist, str);
return true;
}
static struct instruction_list *
2023-02-16 20:41:02 +00:00
tmp_ilist(struct compiler *compiler, size_t line, struct apfl_string *filename)
{
struct instruction_list *ilist;
if (
2023-01-30 21:50:01 +00:00
(ilist = apfl_instructions_new(compiler->gc, line, filename)) == NULL
|| !apfl_gc_tmproot_add(
compiler->gc,
GC_OBJECT_FROM(ilist, GC_TYPE_INSTRUCTIONS)
)
) {
return NULL;
}
return ilist;
}
static struct matcher_instruction_list *
tmp_milist(struct compiler *compiler)
{
struct matcher_instruction_list *milist;
if (
(milist = apfl_matcher_instructions_new(compiler->gc)) == NULL
|| !apfl_gc_tmproot_add(
compiler->gc,
GC_OBJECT_FROM(milist, GC_TYPE_MATCHER_INSTRUCTIONS)
)
) {
return NULL;
}
return milist;
}
struct compile_matchable_args {
struct instruction_list *ilist;
struct matcher_instruction_list *milist;
bool local;
};
static bool
compile_assignable(
struct compiler *compiler,
struct apfl_position position,
struct apfl_expr_assignable *assignable,
struct compile_matchable_args args
);
static enum matcher_instruction
matcher_capture_to_var(bool local)
{
return local ? MATCHER_CAPTURE_TO_VAR_LOCAL : MATCHER_CAPTURE_TO_VAR;
}
static enum matcher_instruction
matcher_capture_to_var_with_path(bool local)
{
return local ? MATCHER_CAPTURE_TO_VAR_LOCAL_WITH_PATH : MATCHER_CAPTURE_TO_VAR_WITH_PATH;
}
static enum instruction
insn_var_new(bool local)
{
return local ? INSN_VAR_NEW_LOCAL : INSN_VAR_NEW;
}
static bool
compile_assignable_var_path(
struct compiler *compiler,
struct apfl_expr_assignable_var_or_member *var_or_member,
struct compile_matchable_args args,
size_t *path_len,
struct apfl_string **varname
) {
size_t index;
struct apfl_string *dot_rhs = NULL;
switch (var_or_member->type) {
case APFL_EXPR_ASSIGNABLE_VAR_OR_MEMBER_VAR:
TRY(ilist_ensure_cap(compiler, args.ilist, 2));
TRY(string_move_into_new(compiler, varname, &var_or_member->var));
ilist_put_insn(args.ilist, insn_var_new(args.local));
ilist_put_string(args.ilist, *varname);
return true;
case APFL_EXPR_ASSIGNABLE_VAR_OR_MEMBER_DOT:
TRY(compile_assignable_var_path(compiler, var_or_member->dot.lhs, args, path_len, varname));
index = args.milist->value_count++;
++*path_len;
TRY(ilist_ensure_cap(compiler, args.ilist, 4));
TRY(string_move_into_new(compiler, &dot_rhs, &var_or_member->dot.rhs));
ilist_put_insn(args.ilist, INSN_STRING);
ilist_put_string(args.ilist, dot_rhs);
ilist_put_insn(args.ilist, INSN_MATCHER_SET_VAL);
ilist_put_insn(args.ilist, index);
return true;
case APFL_EXPR_ASSIGNABLE_VAR_OR_MEMBER_AT:
TRY(compile_assignable_var_path(compiler, var_or_member->dot.lhs, args, path_len, varname));
index = args.milist->value_count++;
++*path_len;
TRY(compile_expr(compiler, var_or_member->at.rhs, args.ilist, NULL));
TRY(ilist_ensure_cap(compiler, args.ilist, 2));
ilist_put_insn(args.ilist, INSN_MATCHER_SET_VAL);
ilist_put_index(args.ilist, index);
return true;
}
assert(false);
return false;
}
static bool
compile_assignable_var_or_member(
struct compiler *compiler,
struct apfl_expr_assignable_var_or_member *var_or_member,
struct compile_matchable_args args
) {
/* TODO: What should happen, if we try to match the same variable twice?
*
* Right now, the second capture will overwrite the first one, but it would
* be pretty sweet if this would actually compare the value with the first
* capture, you could write == in apfl itself then:
*
* == := {
* x x -> true
* x y -> false
* }
*/
args.milist->capture_count++;
size_t path_start = args.milist->value_count;
size_t path_len = 0;
struct apfl_string *varname = NULL;
TRY(compile_assignable_var_path(compiler, var_or_member, args, &path_len, &varname));
assert(varname != NULL /* if compile_assignable_var_path succeeded, it should have set varname */);
if (path_len == 0) {
TRY(milist_ensure_cap(compiler, args.milist, 2));
milist_put_insn(args.milist, matcher_capture_to_var(args.local));
milist_put_string(args.milist, varname);
} else {
TRY(milist_ensure_cap(compiler, args.milist, 4));
milist_put_insn(args.milist, matcher_capture_to_var_with_path(args.local));
milist_put_string(args.milist, varname);
milist_put_index(args.milist, path_start);
milist_put_len(args.milist, path_len);
}
return true;
}
static bool
compile_matchable_constant(
struct compiler *compiler,
struct apfl_expr_const *constant,
struct compile_matchable_args args
) {
size_t index = args.milist->value_count++;
TRY(compile_constant(compiler, constant, args.ilist));
TRY(ilist_ensure_cap(compiler, args.ilist, 2));
ilist_put_insn(args.ilist, INSN_MATCHER_SET_VAL);
ilist_put_index(args.ilist, index);
TRY(milist_ensure_cap(compiler, args.milist, 3));
milist_put_insn(args.milist, MATCHER_CHECK_CONST);
milist_put_index(args.milist, index);
milist_put_insn(args.milist, MATCHER_IGNORE);
return true;
}
#define DEF_COMPILE_ABSTRACT_MATCHABLE_PREDICATE(name, compile_matchable, predtype) \
static bool \
name( \
struct compiler *compiler, \
struct apfl_position position, \
predtype *predicate, \
struct compile_matchable_args args \
) { \
size_t index = args.milist->value_count++; \
\
TRY(compile_expr(compiler, predicate->rhs, args.ilist, NULL)); \
TRY(ilist_ensure_cap(compiler, args.ilist, 2)); \
ilist_put_insn(args.ilist, INSN_MATCHER_SET_VAL); \
ilist_put_index(args.ilist, index); \
\
TRY(milist_ensure_cap(compiler, args.milist, 2)); \
milist_put_insn(args.milist, MATCHER_CHECK_PRED); \
milist_put_index(args.milist, index); \
\
return compile_matchable(compiler, position, predicate->lhs, args); \
} \
DEF_COMPILE_ABSTRACT_MATCHABLE_PREDICATE(
compile_assignable_predicate,
compile_assignable,
struct apfl_expr_assignable_predicate
)
#define DEF_COMPILE_ABSTRACT_MATCHABLE_LIST(name, compile_matchable, listtype, listmemb, listitemtype, listitemmemb) \
static bool \
name##_expand( \
struct compiler *compiler, \
struct apfl_position position, \
listtype *list, \
struct compile_matchable_args args, \
size_t expand_at \
) { \
assert(expand_at < list->len); \
listitemtype *expand_item = &list->listmemb[expand_at]; \
assert(expand_item->expand); \
\
TRY(milist_ensure_cap(compiler, args.milist, 1)); \
milist_put_insn(args.milist, MATCHER_CONTINUE_FROM_END); \
\
for (size_t i = list->len; i-- > expand_at+1; ) { \
listitemtype *item = &list->listmemb[i]; \
\
if (item->expand) { \
compiler->error = (struct apfl_error) { \
.type = APFL_ERR_ONLY_ONE_EXPAND_ALLOWED, \
.position = position, \
}; \
return false; \
} \
\
TRY(compile_matchable(compiler, position, &item->listitemmemb, args)); \
} \
\
TRY(milist_ensure_cap(compiler, args.milist, 1)); \
milist_put_insn(args.milist, MATCHER_REMAINDING); \
\
TRY(compile_matchable(compiler, position, &expand_item->listitemmemb, args)); \
\
return true; \
} \
\
static bool \
name( \
struct compiler *compiler, \
struct apfl_position position, \
listtype *list, \
struct compile_matchable_args args \
) { \
TRY(milist_ensure_cap(compiler, args.milist, 1)); \
milist_put_insn(args.milist, MATCHER_ENTER_LIST); \
\
for (size_t i = 0; i < list->len; i++) { \
listitemtype *item = &list->listmemb[i]; \
\
if (item->expand) { \
return name##_expand( \
compiler, \
position, \
list, \
args, \
i \
); \
} \
\
TRY(compile_matchable(compiler, position, &item->listitemmemb, args)); \
} \
\
TRY(milist_ensure_cap(compiler, args.milist, 1)); \
milist_put_insn(args.milist, MATCHER_LEAVE_LIST); \
return true; \
} \
DEF_COMPILE_ABSTRACT_MATCHABLE_LIST(
compile_assignable_list,
compile_assignable,
struct apfl_expr_assignable_list,
items,
struct apfl_expr_assignable_list_item,
assignable
)
#define DEF_COMPILE_ABSTRACT_MATCHABLE_PAIR(name, compile_matchable, pairtype) \
static bool \
name( \
struct compiler *compiler, \
struct apfl_position position, \
pairtype *pair, \
struct compile_matchable_args args \
) { \
TRY(milist_ensure_cap(compiler, args.milist, 2)); \
milist_put_insn(args.milist, MATCHER_UNPACK_PAIR); \
\
TRY(compile_matchable(compiler, position, pair->lhs, args)); \
TRY(compile_matchable(compiler, position, pair->rhs, args)); \
\
return true; \
}
DEF_COMPILE_ABSTRACT_MATCHABLE_PAIR(
compile_assignable_pair,
compile_assignable,
struct apfl_expr_assignable_pair
)
#define DEF_COMPILE_ABSTRACT_MATCHABLE_TAGGED(name, compile_matchable, taggedtype) \
static bool \
name( \
struct compiler *compiler, \
struct apfl_position position, \
taggedtype *tagged, \
struct compile_matchable_args args \
) { \
size_t index = args.milist->value_count++; \
\
TRY(compile_expr(compiler, tagged->lhs, args.ilist, NULL)); \
TRY(ilist_ensure_cap(compiler, args.ilist, 2)); \
ilist_put_insn(args.ilist, INSN_MATCHER_SET_VAL); \
ilist_put_index(args.ilist, index); \
\
TRY(milist_ensure_cap(compiler, args.milist, 4)); \
milist_put_insn(args.milist, MATCHER_UNPACK_PAIR); \
milist_put_insn(args.milist, MATCHER_CHECK_CONST); \
milist_put_index(args.milist, index); \
milist_put_insn(args.milist, MATCHER_IGNORE); \
\
return compile_matchable(compiler, position, tagged->rhs, args); \
}
DEF_COMPILE_ABSTRACT_MATCHABLE_TAGGED(
compile_assignable_tagged,
compile_assignable,
struct apfl_expr_assignable_tagged
)
static bool
compile_matchable_blank(struct compiler *compiler, struct matcher_instruction_list *milist)
{
TRY(milist_ensure_cap(compiler, milist, 1));
milist_put_insn(milist, MATCHER_IGNORE);
return true;
}
static bool
compile_assignable(
struct compiler *compiler,
struct apfl_position position,
struct apfl_expr_assignable *assignable,
struct compile_matchable_args args
) {
switch (assignable->type) {
case APFL_EXPR_ASSIGNABLE_VAR_OR_MEMBER:
return compile_assignable_var_or_member(compiler, &assignable->var_or_member, args);
case APFL_EXPR_ASSIGNABLE_CONSTANT:
return compile_matchable_constant(compiler, &assignable->constant, args);
case APFL_EXPR_ASSIGNABLE_PREDICATE:
return compile_assignable_predicate(compiler, position, &assignable->predicate, args);
case APFL_EXPR_ASSIGNABLE_LIST:
return compile_assignable_list(compiler, position, &assignable->list, args);
case APFL_EXPR_ASSIGNABLE_PAIR:
return compile_assignable_pair(compiler, position, &assignable->pair, args);
case APFL_EXPR_ASSIGNABLE_TAGGED:
return compile_assignable_tagged(compiler, position, &assignable->tagged, args);
case APFL_EXPR_ASSIGNABLE_BLANK:
return compile_matchable_blank(compiler, args.milist);
}
assert(false);
return false;
}
static bool
compile_assignment_lhs(
struct compiler *compiler,
struct apfl_expr_assignable *assignable,
bool local,
struct apfl_position position,
struct instruction_list *ilist
) {
// By ensuring the capacity early, we can avoid tmprooting the milist, since
// we can immediately append it to the already GC-rooted ilist.
TRY(ilist_ensure_cap(compiler, ilist, 2));
struct matcher_instruction_list *milist = apfl_matcher_instructions_new(compiler->gc);
MALLOC_FAIL_IF_NULL(compiler, milist);
ilist_put_insn(ilist, INSN_MATCHER_PUSH);
ilist_put_matcher(ilist, milist);
return compile_assignable(compiler, position, assignable, (struct compile_matchable_args) {
.ilist = ilist,
.milist = milist,
.local = local,
});
}
static bool
compile_complex_assignment(
struct compiler *compiler,
struct apfl_expr_assignment *assignment,
struct apfl_position position,
struct instruction_list *ilist
) {
TRY(compile_assignment_lhs(
compiler,
&assignment->lhs,
assignment->local,
position,
ilist
));
TRY(compile_expr(compiler, assignment->rhs, ilist, NULL));
TRY(ilist_ensure_cap(compiler, ilist, 2));
ilist_put_insn(ilist, INSN_DUP);
ilist_put_insn(ilist, INSN_MATCHER_MUST_MATCH);
return true;
}
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
static bool
compile_assignment(
struct compiler *compiler,
struct apfl_expr_assignment *assignment,
struct apfl_position position,
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
struct instruction_list *ilist
) {
if (
assignment->lhs.type == APFL_EXPR_ASSIGNABLE_VAR_OR_MEMBER
&& assignment->lhs.var_or_member.type == APFL_EXPR_ASSIGNABLE_VAR_OR_MEMBER_VAR
) {
return compile_simple_assignment(
compiler,
&assignment->lhs.var_or_member.var,
assignment->rhs,
ilist,
assignment->local ? INSN_VAR_NEW_LOCAL : INSN_VAR_NEW,
assignment->local ? INSN_VAR_SET_LOCAL : INSN_VAR_SET
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
);
}
size_t tmproots = apfl_gc_tmproots_begin(compiler->gc);
bool ok = compile_complex_assignment(compiler, assignment, position, ilist);
apfl_gc_tmproots_restore(compiler->gc, tmproots);
return ok;
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
}
static bool
compile_call(struct compiler *compiler, struct apfl_expr_call *call, struct instruction_list *ilist)
{
TRY(compile_expr(compiler, call->callee, ilist, NULL));
TRY(compile_list(compiler, &call->arguments, ilist));
TRY(ilist_ensure_cap(compiler, ilist, 1));
ilist_put_insn(ilist, INSN_CALL);
return true;
}
static bool
compile_body(struct compiler *compiler, struct apfl_expr_body *body, struct instruction_list *ilist)
{
// TODO: This leaves the results of all expressions on the value stack.
// The runtime will clean this up, but we van be less wasteful here.
for (size_t i = 0; i < body->len; i++) {
TRY(compile_expr(compiler, &body->items[i], ilist, NULL));
}
return true;
}
static bool
compile_simple_func_inner(
struct compiler *compiler,
struct apfl_expr_body *func,
struct instruction_list *ilist,
size_t line,
struct apfl_string *name
) {
struct instruction_list *body_ilist = NULL;
2023-01-30 21:50:01 +00:00
MALLOC_FAIL_IF_NULL(compiler, (body_ilist = tmp_ilist(compiler, line, ilist->filename)));
TRY(compile_body(compiler, func, body_ilist));
TRY(ilist_ensure_cap(compiler, ilist, 6));
ilist_put_insn(ilist, INSN_FUNC);
ilist_put_count(ilist, 1);
ilist_put_insn(ilist, INSN_FUNC_ADD_SUBFUNC_ANYARGS);
ilist_put_body(ilist, body_ilist);
if (name != NULL) {
TRY(ilist_ensure_cap(compiler, ilist, 2));
ilist_put_insn(ilist, INSN_FUNC_SET_NAME);
ilist_put_string(ilist, name);
}
return true;
}
static bool
compile_param(
struct compiler *compiler,
struct apfl_position position,
struct apfl_expr_param *param,
struct compile_matchable_args args
);
DEF_COMPILE_ABSTRACT_MATCHABLE_LIST(
compile_param_list,
compile_param,
struct apfl_expr_params,
params,
struct apfl_expr_params_item,
param
)
DEF_COMPILE_ABSTRACT_MATCHABLE_PREDICATE(
compile_param_predicate,
compile_param,
struct apfl_expr_param_predicate
)
DEF_COMPILE_ABSTRACT_MATCHABLE_PAIR(
compile_param_pair,
compile_param,
struct apfl_expr_param_pair
)
DEF_COMPILE_ABSTRACT_MATCHABLE_TAGGED(
compile_param_tagged,
compile_param,
struct apfl_expr_param_tagged
)
static bool
compile_param_var(
struct compiler *compiler,
struct apfl_string *var,
struct compile_matchable_args args
) {
args.milist->capture_count++;
/* TODO: What should happen, if we try to match the same variable twice?
*
* Right now, the second capture will overwrite the first one, but it would
* be pretty sweet if this would actually compare the value with the first
* capture, you could write == in apfl itself then:
*
* == := {
* x x -> true
* x y -> false
* }
*/
TRY(milist_ensure_cap(compiler, args.milist, 2));
struct apfl_string *varname = NULL;
TRY(string_move_into_new(compiler, &varname, var));
milist_put_insn(args.milist, MATCHER_CAPTURE_TO_VAR_LOCAL);
milist_put_string(args.milist, varname);
return true;
}
static bool
compile_param(
struct compiler *compiler,
struct apfl_position position,
struct apfl_expr_param *param,
struct compile_matchable_args args
) {
switch (param->type) {
case APFL_EXPR_PARAM_VAR:
return compile_param_var(compiler, &param->var, args);
case APFL_EXPR_PARAM_CONSTANT:
return compile_matchable_constant(compiler, &param->constant, args);
case APFL_EXPR_PARAM_LIST:
return compile_param_list(compiler, position, &param->list, args);
case APFL_EXPR_PARAM_PREDICATE:
return compile_param_predicate(compiler, position, &param->predicate, args);
case APFL_EXPR_PARAM_PAIR:
return compile_param_pair(compiler, position, &param->pair, args);
case APFL_EXPR_PARAM_TAGGED:
return compile_param_tagged(compiler, position, &param->tagged, args);
case APFL_EXPR_PARAM_BLANK:
return compile_matchable_blank(compiler, args.milist);
}
assert(false);
return false;
}
static bool
compile_simple_func(
struct compiler *compiler,
struct apfl_expr_body *func,
struct instruction_list *ilist,
size_t line,
struct apfl_string *name
) {
size_t tmproots = apfl_gc_tmproots_begin(compiler->gc);
bool ok = compile_simple_func_inner(compiler, func, ilist, line, name);
apfl_gc_tmproots_restore(compiler->gc, tmproots);
return ok;
}
static bool
compile_subfunc(struct compiler *compiler, struct apfl_expr_subfunc *subfunc, struct instruction_list *ilist, struct apfl_position position)
{
struct instruction_list *body_ilist = NULL;
2023-01-30 21:50:01 +00:00
MALLOC_FAIL_IF_NULL(compiler, (body_ilist = tmp_ilist(compiler, position.line, ilist->filename)));
struct matcher_instruction_list *milist = NULL;
MALLOC_FAIL_IF_NULL(compiler, (milist = tmp_milist(compiler)));
TRY(ilist_ensure_cap(compiler, ilist, 2));
ilist_put_insn(ilist, INSN_MATCHER_PUSH);
ilist_put_matcher(ilist, milist);
TRY(compile_param_list(compiler, position, &subfunc->params, (struct compile_matchable_args) {
.ilist = ilist,
.milist = milist,
.local = true,
}));
TRY(compile_body(compiler, &subfunc->body, body_ilist));
TRY(ilist_ensure_cap(compiler, ilist, 2));
ilist_put_insn(ilist, INSN_FUNC_ADD_SUBFUNC);
ilist_put_body(ilist, body_ilist);
return true;
}
static bool
compile_complex_func(
struct compiler *compiler,
struct apfl_expr_complex_func *func,
struct instruction_list *ilist,
struct apfl_position position,
struct apfl_string *name
) {
TRY(ilist_ensure_cap(compiler, ilist, 2));
ilist_put_insn(ilist, INSN_FUNC);
ilist_put_count(ilist, func->len);
for (size_t i = 0; i < func->len; i++) {
struct apfl_expr_subfunc *subfunc = &func->subfuncs[i];
size_t tmproots = apfl_gc_tmproots_begin(compiler->gc);
bool ok = compile_subfunc(compiler, subfunc, ilist, position);
apfl_gc_tmproots_restore(compiler->gc, tmproots);
TRY(ok);
}
if (name != NULL) {
TRY(ilist_ensure_cap(compiler, ilist, 2));
ilist_put_insn(ilist, INSN_FUNC_SET_NAME);
ilist_put_string(ilist, name);
}
return true;
}
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
static bool
compile_expr(
struct compiler *compiler,
struct apfl_expr *expr,
struct instruction_list *ilist,
struct apfl_string *name
) {
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
size_t new_line = (size_t)expr->position.line;
if (new_line != compiler->line) {
if (new_line == compiler->line + 1) {
TRY(ilist_ensure_cap(compiler, ilist, 1));
ilist_put_insn(ilist, INSN_NEXT_LINE);
} else {
TRY(ilist_ensure_cap(compiler, ilist, 2));
ilist_put_insn(ilist, INSN_SET_LINE);
ilist_put_count(ilist, new_line);
}
compiler->line = new_line;
}
switch (expr->type) {
case APFL_EXPR_CALL:
return compile_call(compiler, &expr->call, ilist);
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
case APFL_EXPR_SIMPLE_FUNC:
return compile_simple_func(compiler, &expr->simple_func, ilist, new_line, name);
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
case APFL_EXPR_COMPLEX_FUNC:
return compile_complex_func(compiler, &expr->complex_func, ilist, expr->position, name);
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
case APFL_EXPR_CONSTANT:
return compile_constant(compiler, &expr->constant, ilist);
case APFL_EXPR_BLANK:
TRY(ilist_ensure_cap(compiler, ilist, 1));
ilist_put_insn(ilist, INSN_NIL);
return true;
case APFL_EXPR_LIST:
return compile_list(compiler, &expr->list, ilist);
case APFL_EXPR_DICT:
return compile_dict(compiler, &expr->dict, ilist);
case APFL_EXPR_DOT:
return compile_dot(compiler, &expr->dot, ilist);
case APFL_EXPR_AT:
return compile_at(compiler, &expr->at, ilist);
case APFL_EXPR_VAR:
return compile_var(compiler, &expr->var, ilist);
case APFL_EXPR_PAIR:
return compile_pair(compiler, &expr->pair, ilist);
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
case APFL_EXPR_ASSIGNMENT:
return compile_assignment(compiler, &expr->assignment, expr->position, ilist);
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
}
assert(false);
return false;
}
bool
apfl_compile(struct gc *gc, struct apfl_expr expr, struct apfl_error *error_out, struct instruction_list *out)
{
struct compiler compiler = {
.gc = gc,
.line = out->line,
};
bool ok = compile_expr(&compiler, &expr, out, NULL);
Implement mark&sweep garbage collection and bytecode compilation Instead of the previous refcount base garbage collection, we're now using a basic tri-color mark&sweep collector. This is done to support cyclical value relationships in the future (functions can form cycles, all values implemented up to this point can not). The collector maintains a set of roots and a set of objects (grouped into blocks). The GC enabled objects are no longer allocated manually, but will be allocated by the GC. The GC also wraps an allocator, this way the GC knows, if we ran out of memory and will try to get out of this situation by performing a full collection cycle. The tri-color abstraction was chosen for two reasons: - We don't have to maintain a list of objects that need to be marked, we can simply grab the next grey one. - It should allow us to later implement incremental collection (right now we only do a stop-the-world collection). This also switches to a bytecode based evaluation of the code: We no longer directly evaluate the AST, but first compile it into a series of instructions, that are evaluated in a separate step. This was done in preparation for inplementing functions: We only need to turn a function body into instructions instead of evaluating the node again with each call of the function. Also, since an instruction list is implemented as a GC object, this then removes manual memory management of the function body and it's child nodes. Since the GC and the bytecode go hand in hand, this was done in one (giant) commit. As a downside, we've now lost the ability do do list matching on assignments. I've already started to work on implementing this in the new architecture, but left it out of this commit, as it's already quite a large commit :)
2022-04-11 20:24:22 +00:00
apfl_expr_deinit(gc->allocator, &expr);
if (!ok) {
*error_out = compiler.error;
}
return ok;
}
bool
apfl_compile_whole_file(
struct gc *gc,
struct apfl_parser *parser,
struct apfl_error *error,
struct instruction_list *out
) {
for (;;) {
switch (apfl_parser_next(parser)) {
case APFL_PARSE_OK: {
if (!apfl_compile(
gc,
apfl_parser_get_expr(parser),
error,
out
)) {
return false;
}
break;
}
case APFL_PARSE_ERROR:
*error = apfl_parser_get_error(parser);
return false;
case APFL_PARSE_EOF:
return true;
}
}
}