apfl/src/eval.c

1274 lines
36 KiB
C
Raw Normal View History

#include <assert.h>
#include "apfl.h"
#include "alloc.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
#include "bytecode.h"
#include "compile.h"
#include "context.h"
#include "format.h"
#include "hashmap.h"
#include "matcher.h"
#include "resizable.h"
2022-04-22 21:17:28 +00:00
#include "strings.h"
#include "value.h"
static void evaluate(apfl_ctx ctx, struct func_call_stack_entry *cse);
static void evaluate_matcher(apfl_ctx ctx, struct matcher_call_stack_entry *cse);
static void matcher_init_matching(apfl_ctx ctx, struct matcher *matcher);
static void
stack_must_drop(apfl_ctx ctx, apfl_stackidx index)
{
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(apfl_stack_drop(ctx, index));
}
#define ABSTRACT_GET_ARGUMENT(i, ilist, arg) \
if (*i >= ilist->len) { \
return false; \
} \
\
*arg = ilist->instructions[(*i)++]; \
return true;
#define ABSTRACT_MUST_GET_ARG(get, ctx, i, ilist, arg) \
if (!get(i, ilist, arg)) { \
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.corrupted_bytecode); \
}
static bool
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
get_argument(size_t *i, struct instruction_list *ilist, union instruction_or_arg *arg)
{
ABSTRACT_GET_ARGUMENT(i, ilist, arg)
}
static void
must_get_argument(apfl_ctx ctx, size_t *i, struct instruction_list *ilist, union instruction_or_arg *arg)
{
ABSTRACT_MUST_GET_ARG(get_argument, ctx, i, ilist, arg)
}
static bool
get_matcher_argument(size_t *i, struct matcher_instruction_list *milist, union matcher_instruction_or_arg *arg)
{
ABSTRACT_GET_ARGUMENT(i, milist, arg)
}
static void
must_get_matcher_argument(apfl_ctx ctx, size_t *i, struct matcher_instruction_list *milist, union matcher_instruction_or_arg *arg)
{
ABSTRACT_MUST_GET_ARG(get_matcher_argument, ctx, i, milist, arg)
}
static struct func_call_stack_entry *
get_current_func_cse(apfl_ctx ctx)
{
struct call_stack_entry *cse = apfl_call_stack_cur_entry(ctx);
if (cse == NULL) {
return NULL;
}
return cse->type == CSE_FUNCTION
? &cse->func
: NULL;
}
enum scope_type {
SCOPE_LOCAL,
SCOPE_CLOSUE,
SCOPE_GLOBAL,
};
static struct scope *
get_scope(apfl_ctx ctx, enum scope_type type)
{
struct func_call_stack_entry *func_cse;
switch (type) {
case SCOPE_LOCAL:
if ((func_cse = get_current_func_cse(ctx)) != NULL) {
return func_cse->scope;
}
return NULL;
case SCOPE_CLOSUE:
if ((func_cse = get_current_func_cse(ctx)) != NULL) {
return func_cse->closure_scope;
}
return NULL;
case SCOPE_GLOBAL:
return ctx->globals;
}
assert(false);
return NULL;
}
static struct scope *
get_or_create_local_scope(apfl_ctx ctx)
{
struct func_call_stack_entry *func_cse = get_current_func_cse(ctx);
assert(func_cse != NULL);
if (func_cse->scope != NULL) {
return func_cse->scope;
}
if ((func_cse->scope = apfl_scope_new(&ctx->gc)) == NULL) {
apfl_raise_alloc_error(ctx);
}
return func_cse->scope;
}
static bool
try_variable_get_for_scope_type(apfl_ctx ctx, struct apfl_string *name, enum scope_type type)
{
struct apfl_value value;
struct scope *scope;
if ((scope = get_scope(ctx, type)) != NULL) {
if (apfl_scope_get(scope, name, &value)) {
apfl_stack_must_push(ctx, value);
return true;
}
}
return false;
}
static void
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
variable_get(apfl_ctx ctx, struct apfl_string *name)
{
if (try_variable_get_for_scope_type(ctx, name, SCOPE_LOCAL)) {
return;
}
if (try_variable_get_for_scope_type(ctx, name, SCOPE_CLOSUE)) {
return;
}
if (try_variable_get_for_scope_type(ctx, name, SCOPE_GLOBAL)) {
return;
}
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.variable_doesnt_exist);
}
static bool
try_variable_update_existing_for_scope_type(
apfl_ctx ctx,
struct apfl_string *name,
struct apfl_value value,
enum scope_type type
) {
struct scope *scope;
if ((scope = get_scope(ctx, type)) != NULL) {
if (apfl_scope_update_existing(scope, name, value)) {
return true;
}
}
return false;
}
static void
variable_set_value(apfl_ctx ctx, struct apfl_string *name, bool local, struct apfl_value value)
{
bool was_set = false;
if (!local) {
was_set = try_variable_update_existing_for_scope_type(ctx, name, value, SCOPE_LOCAL)
|| try_variable_update_existing_for_scope_type(ctx, name, value, SCOPE_CLOSUE);
}
if (!was_set) {
struct scope *scope = get_or_create_local_scope(ctx);
assert(scope != NULL /*get_or_create_local_scope should never return NULL*/);
if (!apfl_scope_set(&ctx->gc, scope, name, value)) {
apfl_raise_alloc_error(ctx);
}
}
}
static void
variable_set(apfl_ctx ctx, struct apfl_string *name, bool keep_on_stack, bool local)
{
struct apfl_value value = apfl_stack_must_get(ctx, -1);
variable_set_value(ctx, name, local, value);
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 (keep_on_stack) {
// If the value should be kept on the stack, the value is now in two
// places. We need to set the COW flag to prevent mutations of one copy
// affecting the other one.
value = apfl_value_set_cow_flag(value);
} else {
stack_must_drop(ctx, -1);
}
}
static bool
variable_exists_by_scope_type(apfl_ctx ctx, struct apfl_string *name, enum scope_type type)
{
struct scope *scope;
if ((scope = get_scope(ctx, type)) != NULL) {
if (apfl_scope_has(scope, name)) {
return true;
}
}
return false;
}
static void
variable_new(apfl_ctx ctx, struct apfl_string *name, bool local)
{
if (!local) {
if (variable_exists_by_scope_type(ctx, name, SCOPE_LOCAL)) {
return;
}
if (variable_exists_by_scope_type(ctx, name, SCOPE_CLOSUE)) {
return;
}
}
struct scope *scope = get_or_create_local_scope(ctx);
if (!apfl_scope_create_var(&ctx->gc, scope, name)) {
apfl_raise_alloc_error(ctx);
}
}
static void
func_inner(apfl_ctx ctx, struct instruction_list *body)
{
struct scope *scope = apfl_closure_scope_for_func(ctx);
if (scope == NULL) {
apfl_raise_alloc_error(ctx);
}
if (!apfl_gc_tmproot_add(&ctx->gc, GC_OBJECT_FROM(scope, GC_TYPE_SCOPE))) {
apfl_raise_alloc_error(ctx);
}
struct apfl_value *func_value = apfl_stack_push_placeholder(ctx);
if (func_value == NULL) {
apfl_raise_alloc_error(ctx);
}
if ((func_value->func = apfl_func_new(&ctx->gc, body, scope)) == NULL) {
stack_must_drop(ctx, -1);
apfl_raise_alloc_error(ctx);
}
func_value->type = VALUE_FUNC;
}
static void
func(apfl_ctx ctx, struct instruction_list *body)
{
size_t tmproots = apfl_gc_tmproots_begin(&ctx->gc);
func_inner(ctx, body);
apfl_gc_tmproots_restore(&ctx->gc, tmproots);
}
static void
call_stack_push(apfl_ctx ctx, struct call_stack_entry cse)
{
if (!apfl_resizable_append(
ctx->gc.allocator,
sizeof(struct call_stack_entry),
(void**)&ctx->call_stack.items,
&ctx->call_stack.len,
&ctx->call_stack.cap,
&cse,
1
)) {
apfl_call_stack_entry_deinit(ctx->gc.allocator, &cse);
apfl_raise_alloc_error(ctx);
}
}
static void
call_stack_drop(apfl_ctx ctx)
{
assert(ctx->call_stack.len > 0);
apfl_call_stack_entry_deinit(ctx->gc.allocator, apfl_call_stack_cur_entry(ctx));
assert(
// We're shrinking the memory here, should not fail
apfl_resizable_resize(
ctx->gc.allocator,
sizeof(struct call_stack_entry),
(void **)&ctx->call_stack.items,
&ctx->call_stack.len,
&ctx->call_stack.cap,
ctx->call_stack.len - 1
)
);
}
static void
return_from_function_inner(apfl_ctx ctx)
{
struct apfl_value value;
if (!apfl_stack_pop(ctx, &value, -1)) {
// No return value on the stack. Return nil instead
value = (struct apfl_value) { .type = VALUE_NIL };
}
call_stack_drop(ctx);
apfl_stack_must_push(ctx, value);
}
static void
return_from_function(apfl_ctx ctx)
{
size_t tmproots = apfl_gc_tmproots_begin(&ctx->gc);
return_from_function_inner(ctx);
apfl_gc_tmproots_restore(&ctx->gc, tmproots);
}
static void
prepare_call(apfl_ctx ctx, size_t tmproots, struct apfl_value args, struct call_stack_entry cse)
{
call_stack_push(ctx, cse);
// Note: This pushes args on the stack of the newly created call stack
apfl_stack_must_push(ctx, args);
// Both the function and the args are now rooted again, we can undo the tmproots early.
apfl_gc_tmproots_restore(&ctx->gc, tmproots);
}
// Keep evaluate instructions until we've returned from the current call stack.
// Must not be called with a CSE_CFUNCTION on top of the call stack.
static void
evaluate_until_call_stack_return(apfl_ctx ctx)
{
struct call_stack *call_stack = &ctx->call_stack;
size_t depth_started = call_stack->len;
assert(depth_started > 0);
while (call_stack->len >= depth_started) {
struct call_stack_entry *cse = apfl_call_stack_cur_entry(ctx);
assert(cse != NULL);
switch (cse->type) {
case CSE_CFUNCTION:
assert(false);
break;
case CSE_FUNCTION:
evaluate(ctx, &cse->func);
break;
case CSE_MATCHER:
evaluate_matcher(ctx, &cse->matcher);
break;
}
}
}
static void
must_tmproot_add_value(apfl_ctx ctx, struct apfl_value value)
{
struct gc_object *obj = apfl_value_get_gc_object(value);
if (obj != NULL) {
if (!apfl_gc_tmproot_add(&ctx->gc, obj)) {
apfl_raise_alloc_error(ctx);
}
}
}
static void
call_inner(apfl_ctx ctx, size_t tmproots, apfl_stackidx func_index, apfl_stackidx args_index, bool call_from_apfl)
{
struct apfl_value func = apfl_stack_must_get(ctx, func_index);
must_tmproot_add_value(ctx, func);
struct apfl_value args = apfl_stack_must_get(ctx, args_index);
must_tmproot_add_value(ctx, args);
assert(apfl_stack_drop_multi(ctx, 2, (apfl_stackidx[]){func_index, args_index}));
if (!VALUE_IS_A(func, APFL_VALUE_FUNC)) {
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.not_a_function);
}
if (!VALUE_IS_A(args, APFL_VALUE_LIST)) {
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.not_a_list);
}
switch (func.type) {
case VALUE_FUNC:
prepare_call(ctx, tmproots, args, (struct call_stack_entry) {
.type = CSE_FUNCTION,
.stack = apfl_stack_new(),
.func = {
.pc = 0,
.instructions = func.func->body,
.scope = NULL,
.closure_scope = func.func->scope,
.execution_line = func.func->body->line,
.returning_from_matcher = false,
.matcher = 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
});
if (call_from_apfl) {
// In this case we're already coming from evaluate_until_call_stack_return,
// which will pick up the new stack entry. This way we can avoid doing the recursion in C.
return;
} else {
evaluate_until_call_stack_return(ctx);
}
break;
case VALUE_CFUNC:
prepare_call(ctx, tmproots, args, (struct call_stack_entry) {
.type = CSE_CFUNCTION,
.stack = apfl_stack_new(),
.cfunc = {
.func = func.cfunc,
},
});
func.cfunc->func(ctx);
return_from_function(ctx);
break;
default:
assert(false); // Otherwise the VALUE_IS_A() check for APFL_VALUE_FUNC would have failed
}
}
static void
call(apfl_ctx ctx, apfl_stackidx func_index, apfl_stackidx args_index, bool call_from_apfl)
{
size_t tmproots = apfl_gc_tmproots_begin(&ctx->gc);
call_inner(ctx, tmproots, func_index, args_index, call_from_apfl);
apfl_gc_tmproots_restore(&ctx->gc, tmproots);
}
void
apfl_call(apfl_ctx ctx, apfl_stackidx func_index, apfl_stackidx args_index)
{
call(ctx, func_index, args_index, false);
}
static void
matcher_load(apfl_ctx ctx, struct func_call_stack_entry *cse, struct matcher_instruction_list *milist)
{
assert(cse != NULL);
if ((cse->matcher = apfl_matcher_new(&ctx->gc, milist)) == NULL) {
apfl_raise_alloc_error(ctx);
}
}
static void
matcher_set_val(apfl_ctx ctx, struct matcher *matcher, size_t index)
{
if (matcher == NULL) {
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.corrupted_bytecode);
}
if (index >= matcher->instructions->value_count) {
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.corrupted_bytecode);
}
matcher->values[index] = apfl_stack_must_pop(ctx, -1);
}
static void
variable_set_from_matcher_inner(
apfl_ctx ctx,
struct matcher *matcher,
struct apfl_string *varname,
size_t index,
bool local
) {
if (matcher == NULL) {
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.corrupted_bytecode);
}
if (index >= matcher->instructions->capture_count) {
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.corrupted_bytecode);
}
struct apfl_value value = apfl_value_move(&matcher->captures[index]);
must_tmproot_add_value(ctx, value);
variable_set_value(ctx, varname, local, value);
}
static void
variable_set_from_matcher(
apfl_ctx ctx,
struct matcher *matcher,
struct apfl_string *varname,
size_t index,
bool local
) {
size_t tmproots = apfl_gc_tmproots_begin(&ctx->gc);
variable_set_from_matcher_inner(ctx, matcher, varname, index, local);
apfl_gc_tmproots_restore(&ctx->gc, tmproots);
}
static void
evaluate(apfl_ctx ctx, struct func_call_stack_entry *cse)
{
if (cse->returning_from_matcher) {
assert(cse->matcher != NULL);
if (!cse->matcher->result) {
cse->matcher = NULL;
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.value_doesnt_match);
}
cse->returning_from_matcher = false;
}
union instruction_or_arg arg;
union instruction_or_arg arg2;
size_t *pc = &cse->pc;
struct instruction_list *ilist = cse->instructions;
while (*pc < cse->instructions->len) {
switch (ilist->instructions[(*pc)++].instruction) {
case INSN_NIL:
apfl_push_nil(ctx);
goto continue_loop;
case INSN_TRUE:
apfl_push_bool(ctx, true);
goto continue_loop;
case INSN_FALSE:
apfl_push_bool(ctx, false);
goto continue_loop;
case INSN_NUMBER:
must_get_argument(ctx, pc, ilist, &arg);
apfl_push_number(ctx, arg.number);
goto continue_loop;
case INSN_STRING:
must_get_argument(ctx, pc, ilist, &arg);
apfl_stack_must_push(ctx, (struct apfl_value) {
.type = VALUE_STRING,
.string = arg.string,
});
goto continue_loop;
case INSN_LIST:
must_get_argument(ctx, pc, ilist, &arg);
apfl_list_create(ctx, arg.count);
goto continue_loop;
case INSN_LIST_APPEND:
apfl_list_append(ctx, -2, -1);
goto continue_loop;
case INSN_LIST_EXPAND_INTO:
apfl_list_append_list(ctx, -2, -1);
goto continue_loop;
case INSN_DICT:
apfl_dict_create(ctx);
goto continue_loop;
case INSN_DICT_APPEND_KVPAIR:
apfl_dict_set(ctx, -3, -2, -1);
goto continue_loop;
case INSN_GET_MEMBER:
apfl_get_member(ctx, -2, -1);
goto continue_loop;
case INSN_VAR_NEW:
must_get_argument(ctx, pc, ilist, &arg);
variable_new(ctx, arg.string, false);
goto continue_loop;
case INSN_VAR_NEW_LOCAL:
must_get_argument(ctx, pc, ilist, &arg);
variable_new(ctx, arg.string, true);
goto continue_loop;
case INSN_VAR_GET:
must_get_argument(ctx, pc, ilist, &arg);
variable_get(ctx, arg.string);
goto continue_loop;
case INSN_VAR_SET:
must_get_argument(ctx, pc, ilist, &arg);
variable_set(ctx, arg.string, true, false);
goto continue_loop;
case INSN_VAR_SET_LOCAL:
must_get_argument(ctx, pc, ilist, &arg);
variable_set(ctx, arg.string, true, true);
goto continue_loop;
case INSN_MOVE_TO_LOCAL_VAR:
must_get_argument(ctx, pc, ilist, &arg);
variable_set(ctx, arg.string, false, true);
goto continue_loop;
case INSN_NEXT_LINE:
cse->execution_line++;
goto continue_loop;
case INSN_SET_LINE:
must_get_argument(ctx, pc, ilist, &arg);
cse->execution_line = arg.count;
goto continue_loop;
case INSN_GET_BY_INDEX_KEEP:
must_get_argument(ctx, pc, ilist, &arg);
apfl_get_list_member_by_index(ctx, -1, arg.index);
goto continue_loop;
case INSN_DROP:
if (!apfl_stack_drop(ctx, -1)) {
apfl_raise_invalid_stackidx(ctx);
}
goto continue_loop;
case INSN_CALL:
call(ctx, -2, -1, true);
// By returning from this function, the newly pushed call stack entry (if any) will get picked up by
// evaluate_until_call_stack_return. In case no new CSE was pushed (when a cfunc was called), we'll the
// simply continue with the current call stack.
return;
case INSN_FUNC:
must_get_argument(ctx, pc, ilist, &arg);
func(ctx, arg.body);
goto continue_loop;
case INSN_MATCHER_LOAD:
must_get_argument(ctx, pc, ilist, &arg);
matcher_load(ctx, cse, arg.matcher);
goto continue_loop;
case INSN_MATCHER_SET_VAL:
must_get_argument(ctx, pc, ilist, &arg);
matcher_set_val(ctx, cse->matcher, arg.index);
goto continue_loop;
case INSN_MATCHER_MUST_MATCH:
// matcher_init_matching pushes a new call stack entry for the matcher onto the stack. We rturn from this
// So this new CSE gets executed. By setting returning_from_matcher, we know that we came from the matcher,
// once it returns.
matcher_init_matching(ctx, cse->matcher);
cse->returning_from_matcher = true;
return;
case INSN_MATCHER_DROP:
cse->matcher = NULL;
goto continue_loop;
case INSN_VAR_SET_FROM_MATCHER:
must_get_argument(ctx, pc, ilist, &arg);
must_get_argument(ctx, pc, ilist, &arg2);
variable_set_from_matcher(ctx, cse->matcher, arg.string, arg2.index, false);
goto continue_loop;
case INSN_VAR_SET_LOCAL_FROM_MATCHER:
must_get_argument(ctx, pc, ilist, &arg);
must_get_argument(ctx, pc, ilist, &arg2);
variable_set_from_matcher(ctx, cse->matcher, arg.string, arg2.index, true);
goto continue_loop;
}
assert(false);
continue_loop:;
}
return_from_function(ctx);
}
static void
matcher_stack_push(apfl_ctx ctx, struct matcher_call_stack_entry *cse, struct matcher_stack_entry entry)
{
if (!apfl_resizable_append(
ctx->gc.allocator,
sizeof(struct matcher_stack_entry),
(void **)&cse->matcher_stack,
&cse->matcher_stack_len,
&cse->matcher_stack_cap,
&entry,
1
)) {
apfl_raise_alloc_error(ctx);
}
}
APFL_NORETURN static void
raise_invalid_matcher_state(apfl_ctx ctx)
{
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.invalid_matcher_state);
}
static void
matcher_stack_drop(apfl_ctx ctx, struct matcher_call_stack_entry *cse)
{
if (cse->matcher_stack_len == 0) {
raise_invalid_matcher_state(ctx);
}
assert(
// We're shrinking, should not fail
apfl_resizable_resize(
ctx->gc.allocator,
sizeof(struct matcher_stack_entry),
(void **)&cse->matcher_stack,
&cse->matcher_stack_len,
&cse->matcher_stack_cap,
cse->matcher_stack_len-1
)
);
}
static void
matcher_init_matching_inner(apfl_ctx ctx, struct matcher *matcher)
{
struct apfl_value value = apfl_stack_must_get(ctx, -1);
must_tmproot_add_value(ctx, value);
struct matcher_call_stack_entry matcher_cse = {
.pc = 0,
.matcher = matcher,
.matcher_stack = NULL,
.matcher_stack_len = 0,
.matcher_stack_cap = 0,
};
matcher_stack_push(ctx, &matcher_cse, (struct matcher_stack_entry) {
.mode = MATCHER_MODE_VALUE,
});
call_stack_push(ctx, (struct call_stack_entry) {
.type = CSE_MATCHER,
.stack = apfl_stack_new(),
.matcher = matcher_cse,
});
apfl_stack_must_push(ctx, apfl_value_set_cow_flag(value));
}
static void
matcher_init_matching(apfl_ctx ctx, struct matcher *matcher)
{
size_t tmproots = apfl_gc_tmproots_begin(&ctx->gc);
matcher_init_matching_inner(ctx, matcher);
apfl_gc_tmproots_restore(&ctx->gc, tmproots);
}
static void
matcher_check_index(apfl_ctx ctx, size_t count, size_t index)
{
if (index >= count) {
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.corrupted_bytecode);
}
}
static struct matcher_stack_entry *
matcher_cur_stack_entry(apfl_ctx ctx, struct matcher_call_stack_entry *cse)
{
if (cse->matcher_stack_len == 0) {
raise_invalid_matcher_state(ctx);
}
return &cse->matcher_stack[cse->matcher_stack_len-1];
}
static bool
matcher_current_val_in_mstack(apfl_ctx ctx, struct matcher_stack_entry *mstack, struct apfl_value *value)
{
struct apfl_value cur;
switch (mstack->mode) {
case MATCHER_MODE_VALUE:
case MATCHER_MODE_LIST_REMAINING:
if (!apfl_stack_get(ctx, &cur, -1)) {
raise_invalid_matcher_state(ctx);
}
*value = cur;
return true;
case MATCHER_MODE_STOP:
case MATCHER_MODE_LIST_UNDERFLOW:
return false;
case MATCHER_MODE_LIST_START:
if (!apfl_stack_get(ctx, &cur, -1)) {
raise_invalid_matcher_state(ctx);
}
if (cur.type != VALUE_LIST) {
raise_invalid_matcher_state(ctx);
}
if (mstack->lower >= cur.list->len) {
return false;
}
*value = cur.list->items[mstack->lower];
return true;
case MATCHER_MODE_LIST_END:
if (!apfl_stack_get(ctx, &cur, -1)) {
raise_invalid_matcher_state(ctx);
}
if (cur.type != VALUE_LIST) {
raise_invalid_matcher_state(ctx);
}
if (mstack->upper == 0) {
return NULL;
}
*value = cur.list->items[mstack->upper-1];
return true;
}
raise_invalid_matcher_state(ctx);
}
static bool
matcher_current_val(apfl_ctx ctx, struct matcher_call_stack_entry *cse, struct apfl_value *value)
{
struct matcher_stack_entry *mstack = matcher_cur_stack_entry(ctx, cse);
return matcher_current_val_in_mstack(ctx, mstack, value);
}
static bool
matcher_next(apfl_ctx ctx, struct matcher_call_stack_entry *cse)
{
again:;
struct matcher_stack_entry *mstack = matcher_cur_stack_entry(ctx, cse);
switch (mstack->mode) {
case MATCHER_MODE_VALUE:
mstack->mode = MATCHER_MODE_STOP;
if (!apfl_stack_drop(ctx, -1)) {
raise_invalid_matcher_state(ctx);
}
return true;
case MATCHER_MODE_STOP:
case MATCHER_MODE_LIST_UNDERFLOW:
raise_invalid_matcher_state(ctx);
return false;
case MATCHER_MODE_LIST_START:
mstack->lower++;
return true;
case MATCHER_MODE_LIST_END:
if (mstack->upper <= mstack->lower) {
mstack->mode = MATCHER_MODE_LIST_UNDERFLOW;
return false;
}
mstack->upper--;
return true;
case MATCHER_MODE_LIST_REMAINING:
if (!apfl_stack_drop(ctx, -1)) {
raise_invalid_matcher_state(ctx);
}
matcher_stack_drop(ctx, cse);
goto again; // We also need to advance the previous stack entry,
// like we would do when doing a MATCHER_LEAVE_LIST
}
raise_invalid_matcher_state(ctx);
}
static bool
matcher_enter_list(apfl_ctx ctx, struct matcher_call_stack_entry *cse)
{
struct matcher_stack_entry *mstack = matcher_cur_stack_entry(ctx, cse);
struct apfl_value cur;
if (!matcher_current_val_in_mstack(ctx, mstack, &cur)) {
return false;
}
if (cur.type != VALUE_LIST) {
return false;
}
size_t len = cur.list->len;
apfl_stack_must_push(ctx, cur);
matcher_stack_push(ctx, cse, (struct matcher_stack_entry) {
.mode = MATCHER_MODE_LIST_START,
.lower = 0,
.upper = len,
});
return true;
}
static void
matcher_continue_from_end(apfl_ctx ctx, struct matcher_call_stack_entry *cse)
{
struct matcher_stack_entry *mstack = matcher_cur_stack_entry(ctx, cse);
if (mstack->mode != MATCHER_MODE_LIST_START) {
raise_invalid_matcher_state(ctx);
}
mstack->mode = MATCHER_MODE_LIST_END;
}
static void
matcher_remainding(apfl_ctx ctx, struct matcher_call_stack_entry *cse)
{
struct matcher_stack_entry *mstack = matcher_cur_stack_entry(ctx, cse);
struct apfl_value cur;
if (!apfl_stack_get(ctx, &cur, -1)) {
raise_invalid_matcher_state(ctx);
}
if (
(mstack->mode != MATCHER_MODE_LIST_START && mstack->mode != MATCHER_MODE_LIST_END)
|| cur.type != VALUE_LIST
) {
raise_invalid_matcher_state(ctx);
}
if (mstack->lower > mstack->upper) {
raise_invalid_matcher_state(ctx);
}
struct list_header *cur_list = cur.list;
assert(cur_list->len >= mstack->upper);
size_t len = mstack->upper - mstack->lower;
apfl_list_create(ctx, len);
struct apfl_value new_val = apfl_stack_must_get(ctx, -1);
assert(new_val.type == VALUE_LIST);
struct list_header *new_list = new_val.list;
assert(new_list->cap == len);
assert(new_list->len == 0);
for (size_t i = mstack->lower; i < mstack->upper; i++) {
new_list->items[new_list->len++] = cur_list->items[i];
}
assert(new_list->len == len);
if (!apfl_stack_drop(ctx, -2)) { // Drop the original list
raise_invalid_matcher_state(ctx);
}
mstack->mode = MATCHER_MODE_LIST_REMAINING;
}
static bool
matcher_leave_list(apfl_ctx ctx, struct matcher_call_stack_entry *cse)
{
struct matcher_stack_entry *mstack = matcher_cur_stack_entry(ctx, cse);
if (mstack->mode != MATCHER_MODE_LIST_START) {
raise_invalid_matcher_state(ctx);
}
if (mstack->lower < mstack->upper) {
// List was not completely matched
return false;
}
if (!apfl_stack_drop(ctx, -1)) {
raise_invalid_matcher_state(ctx);
}
matcher_stack_drop(ctx, cse);
return matcher_next(ctx, cse);
}
static void
return_from_matcher(apfl_ctx ctx, bool result)
{
struct call_stack_entry *cse = apfl_call_stack_cur_entry(ctx);
assert(cse != NULL);
assert(cse->type == CSE_MATCHER);
cse->matcher.matcher->result = result;
call_stack_drop(ctx);
cse = apfl_call_stack_cur_entry(ctx);
assert(cse != NULL);
assert(cse->type == CSE_FUNCTION);
}
#define RETURN_WITHOUT_MATCH(ctx) \
do { \
return_from_matcher((ctx), false); \
return; \
} while (0)
#define RETURN_WITHOUT_MATCH_ON_FALSE(ctx, x) \
do { \
if (!(x)) { \
RETURN_WITHOUT_MATCH(ctx); \
} \
} while (0)
static void
evaluate_matcher(apfl_ctx ctx, struct matcher_call_stack_entry *cse)
{
union matcher_instruction_or_arg arg;
size_t *pc = &cse->pc;
struct matcher *matcher = cse->matcher;
struct matcher_instruction_list *milist = matcher->instructions;
while (*pc < milist->len) {
struct apfl_value cur;
switch (milist->instructions[(*pc)++].instruction) {
case MATCHER_CAPTURE:
if (!matcher_current_val(ctx, cse, &cur)) {
RETURN_WITHOUT_MATCH(ctx);
}
must_get_matcher_argument(ctx, pc, milist, &arg);
matcher_check_index(ctx, milist->capture_count, arg.index);
matcher->captures[arg.index] = apfl_value_set_cow_flag(cur);
RETURN_WITHOUT_MATCH_ON_FALSE(ctx, matcher_next(ctx, cse));
goto continue_loop;
case MATCHER_IGNORE:
if (!matcher_current_val(ctx, cse, &cur)) {
RETURN_WITHOUT_MATCH(ctx);
}
RETURN_WITHOUT_MATCH_ON_FALSE(ctx, matcher_next(ctx, cse));
goto continue_loop;
case MATCHER_CHECK_CONST:
if (!matcher_current_val(ctx, cse, &cur)) {
RETURN_WITHOUT_MATCH(ctx);
}
must_get_matcher_argument(ctx, pc, milist, &arg);
matcher_check_index(ctx, milist->value_count, arg.index);
RETURN_WITHOUT_MATCH_ON_FALSE(ctx, apfl_value_eq(matcher->values[arg.index], cur));
goto continue_loop;
case MATCHER_CHECK_PRED:
if (!matcher_current_val(ctx, cse, &cur)) {
RETURN_WITHOUT_MATCH(ctx);
}
must_get_matcher_argument(ctx, pc, milist, &arg);
matcher_check_index(ctx, milist->value_count, arg.index);
apfl_raise_const_error(ctx, APFL_RESULT_ERR, apfl_messages.feature_not_implemented); // TODO: Implement this
goto continue_loop;
case MATCHER_ENTER_LIST:
RETURN_WITHOUT_MATCH_ON_FALSE(ctx, matcher_enter_list(ctx, cse));
goto continue_loop;
case MATCHER_LEAVE_LIST:
RETURN_WITHOUT_MATCH_ON_FALSE(ctx, matcher_leave_list(ctx, cse));
goto continue_loop;
case MATCHER_CONTINUE_FROM_END:
matcher_continue_from_end(ctx, cse);
goto continue_loop;
case MATCHER_REMAINDING:
matcher_remainding(ctx, cse);
goto continue_loop;
}
assert(false);
continue_loop:;
}
return_from_matcher(
ctx,
// We've successfully matched everything, if there's only one stack element left and we're in stop mode
cse->matcher_stack_len == 1 && cse->matcher_stack[0].mode == MATCHER_MODE_STOP
);
}
struct apfl_iterative_runner_data {
apfl_ctx ctx;
apfl_tokenizer_ptr tokenizer;
apfl_parser_ptr parser;
enum apfl_result result;
bool with_error_on_stack;
bool end;
struct scope *scope;
};
static void
iterative_runner_eval_expr_inner(apfl_iterative_runner runner, struct apfl_expr expr)
{
apfl_ctx ctx = runner->ctx;
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 = apfl_instructions_new(&ctx->gc, expr.position.line);
if (ilist == NULL) {
apfl_raise_alloc_error(ctx);
}
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 (!apfl_gc_tmproot_add(&ctx->gc, GC_OBJECT_FROM(ilist, GC_TYPE_INSTRUCTIONS))) {
apfl_raise_alloc_error(ctx);
}
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 apfl_error error;
if (!apfl_compile(&ctx->gc, expr, &error, ilist)) {
apfl_raise_error_object(ctx, error);
}
call_stack_push(ctx, (struct call_stack_entry) {
.type = CSE_FUNCTION,
.stack = apfl_stack_new(),
.func = {
.pc = 0,
.instructions = ilist,
.scope = runner->scope,
.closure_scope = NULL,
.execution_line = ilist->line,
.returning_from_matcher = false,
.matcher = NULL,
},
});
evaluate_until_call_stack_return(ctx);
}
static void
iterative_runner_eval_expr(apfl_iterative_runner runner, struct apfl_expr expr)
{
apfl_ctx ctx = runner->ctx;
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(&ctx->gc);
iterative_runner_eval_expr_inner(runner, expr);
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_gc_tmproots_restore(&ctx->gc, tmproots);
}
2022-04-22 21:13:01 +00:00
bool
apfl_debug_print_val(apfl_ctx ctx, apfl_stackidx index, struct apfl_format_writer w)
{
struct apfl_value value;
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 (!apfl_stack_pop(ctx, &value, index)) {
FMT_TRY(apfl_format_put_string(w, "apfl_debug_print_val: Invalid stack index "));
FMT_TRY(apfl_format_put_int(w, (int)index));
FMT_TRY(apfl_format_put_string(w, "\n"));
2022-04-22 21:13:01 +00:00
return true;
}
return apfl_value_print(value, w);
}
2022-04-21 19:15:20 +00:00
apfl_iterative_runner
apfl_iterative_runner_new(apfl_ctx ctx, struct apfl_source_reader reader)
{
apfl_iterative_runner runner = NULL;
apfl_tokenizer_ptr tokenizer = NULL;
apfl_parser_ptr parser = NULL;
runner = ALLOC_OBJ(ctx->gc.allocator, struct apfl_iterative_runner_data);
2022-04-21 19:15:20 +00:00
if (runner == NULL) {
return NULL;
}
tokenizer = apfl_tokenizer_new(ctx->gc.allocator, reader);
2022-04-21 19:15:20 +00:00
if (tokenizer == NULL) {
goto error;
2022-04-21 19:15:20 +00:00
}
parser = apfl_parser_new(ctx->gc.allocator, apfl_tokenizer_as_token_source(tokenizer));
2022-04-21 19:15:20 +00:00
if (parser == NULL) {
goto error;
}
struct scope *scope = apfl_scope_new(&ctx->gc);
if (scope == NULL) {
goto error;
2022-04-21 19:15:20 +00:00
}
*runner = (struct apfl_iterative_runner_data) {
.ctx = ctx,
.tokenizer = tokenizer,
.parser = parser,
.result = APFL_RESULT_OK,
.end = false,
.scope = scope,
2022-04-21 19:15:20 +00:00
};
if (!apfl_ctx_register_iterative_runner(ctx, runner)) {
goto error;
}
2022-04-21 19:15:20 +00:00
return runner;
error:
FREE_OBJ(ctx->gc.allocator, runner);
apfl_tokenizer_destroy(tokenizer);
apfl_parser_destroy(parser);
return NULL;
2022-04-21 19:15:20 +00:00
}
static void
handle_parse_error(apfl_ctx ctx, struct apfl_error error)
{
enum apfl_result errtype = APFL_RESULT_ERR;
if (apfl_error_is_fatal_type(error.type)) {
errtype = APFL_RESULT_ERR_FATAL;
}
const char *const_str = apfl_error_as_const_string(error);
if (const_str != NULL) {
apfl_raise_const_error(ctx, errtype, const_str);
}
struct apfl_string string;
if (!apfl_error_as_string(error, ctx->gc.allocator, &string)) {
apfl_raise_alloc_error(ctx);
}
if (!apfl_move_string_onto_stack(ctx, string)) {
apfl_raise_alloc_error(ctx);
}
apfl_raise_error_with_type(ctx, -1, errtype);
}
static void
iterative_runner_next_protected(apfl_ctx ctx)
2022-04-21 19:15:20 +00:00
{
apfl_stack_drop(ctx, -1);
apfl_cfunc_self_getslot(ctx, 0);
apfl_iterative_runner runner = apfl_get_userdata(ctx, -1);
2022-04-21 19:15:20 +00:00
switch (apfl_parser_next(runner->parser)) {
case APFL_PARSE_OK:
iterative_runner_eval_expr(runner, apfl_parser_get_expr(runner->parser));
return;
2022-04-21 19:15:20 +00:00
case APFL_PARSE_ERROR:
handle_parse_error(runner->ctx, apfl_parser_get_error(runner->parser));
return;
2022-04-21 19:15:20 +00:00
case APFL_PARSE_EOF:
runner->end = true;
return;
2022-04-21 19:15:20 +00:00
}
assert(false);
}
2022-04-21 19:15:20 +00:00
bool
apfl_iterative_runner_next(apfl_iterative_runner runner)
{
if (runner->end) {
return false;
}
apfl_stack_clear(runner->ctx);
// TODO: It's a bit silly that we build the cfunc every time. Can we be
// smarter and leave it on the stack somewhere? Or save it in the
// runner? Also, what if the construction of the func fails? We're not
// running protected yet :/
apfl_push_cfunc(runner->ctx, iterative_runner_next_protected, 1);
apfl_push_userdata(runner->ctx, runner);
apfl_cfunc_setslot(runner->ctx, -2, 0, -1);
apfl_list_create(runner->ctx, 0);
apfl_call_protected(runner->ctx, -2, -1, &runner->with_error_on_stack);
return !runner->end;
2022-04-21 19:15:20 +00:00
}
enum apfl_result
apfl_iterative_runner_get_result(apfl_iterative_runner runner)
{
return runner->result;
}
bool
apfl_iterative_runner_has_error_on_stack(apfl_iterative_runner runner)
{
return runner->with_error_on_stack;
}
2022-04-21 19:15:20 +00:00
void
apfl_iterative_runner_destroy(apfl_iterative_runner runner)
{
if (runner == NULL) {
return;
}
apfl_parser_destroy(runner->parser);
apfl_tokenizer_destroy(runner->tokenizer);
apfl_ctx_unregister_iterative_runner(runner->ctx, runner);
2022-04-21 19:15:20 +00:00
FREE_OBJ(runner->ctx->gc.allocator, runner);
}
void
apfl_iterative_runner_visit_gc_objects(apfl_iterative_runner runner, gc_visitor visitor, void *opaque)
{
// TODO: It's a bit awkward that this function is defined here but the
// prototype lives in context.h... Maybe we should just merge context
// and eval together? The separation is rather arbitrary anyway :/
if (runner->scope != NULL) {
visitor(opaque, GC_OBJECT_FROM(runner->scope, GC_TYPE_SCOPE));
}
}