jak-project/goalc/compiler/compilation/ControlFlow.cpp

360 lines
13 KiB
C++
Raw Normal View History

/*!
* @file ControlFlow.cpp
* Compiler forms related to conditional branching and control flow.
*/
#include "common/goos/ParseHelpers.h"
2020-09-13 17:34:02 -04:00
#include "goalc/compiler/Compiler.h"
2020-09-13 17:34:02 -04:00
/*!
* Convert an expression into a GoalCondition for use in a conditional branch.
*
2020-09-13 17:34:02 -04:00
* The reason for this design is to allow an optimization for
* (if (< a b) ...) to be compiled without actually computing a true/false value for the (< a b)
* expression. Instead, it will generate a cmp + jle sequence of instructions, which is much faster.
* In particular, getting GOAL "true" requires a few instructions, so it's to avoid this when
* possible.
2020-09-13 17:34:02 -04:00
*
* This can be applied to _any_ GOAL form, and will return a GoalCondition which can be used with a
* Branch IR to branch if the condition is true/false. When possible it applies the optimization
* mentioned above, but will be fine in other cases too. I believe the original GOAL compiler had a
* similar system.
*
* Will branch if the condition is true and the invert flag is false.
* Will branch if the condition is false and the invert flag is true.
*/
Condition Compiler::compile_condition(const goos::Object& condition, Env* env, bool invert) {
Condition gc;
// These are special conditions that can be optimized into a cmp + jxx instruction.
const std::unordered_map<std::string, ConditionKind> conditions_inverted = {
{"!=", ConditionKind::EQUAL}, {"eq?", ConditionKind::NOT_EQUAL},
{"neq?", ConditionKind::EQUAL}, {"=", ConditionKind::NOT_EQUAL},
{">", ConditionKind::LEQ}, {"<", ConditionKind::GEQ},
{">=", ConditionKind::LT}, {"<=", ConditionKind::GT}};
const std::unordered_map<std::string, ConditionKind> conditions_normal = {
{"!=", ConditionKind::NOT_EQUAL}, {"eq?", ConditionKind::EQUAL},
{"neq?", ConditionKind::NOT_EQUAL}, {"=", ConditionKind::EQUAL},
{">", ConditionKind::GT}, {"<", ConditionKind::LT},
{">=", ConditionKind::GEQ}, {"<=", ConditionKind::LEQ}};
// we may have gotten a macro as a condition, expand it first.
// note : use the `condition` arg for errors still so that the user can actually tell what code
// went wrong!
auto new_c = expand_macro_completely(condition, env);
2020-09-13 17:34:02 -04:00
// possibly a form with an optimizable condition?
if (new_c.is_pair()) {
auto& first = pair_car(new_c);
auto& rest = pair_cdr(new_c);
2020-09-13 17:34:02 -04:00
if (first.is_symbol()) {
auto fas = first.as_symbol();
// if there's a not, we can just try again to get an optimization with the invert flipped.
if (fas == "not") {
2020-09-13 17:34:02 -04:00
if (!pair_cdr(rest).is_empty_list()) {
throw_compiler_error(condition, "A condition with \"not\" can have only one argument");
2020-09-13 17:34:02 -04:00
}
return compile_condition(pair_car(rest), env, !invert);
2020-09-13 17:34:02 -04:00
}
auto& conditions = invert ? conditions_inverted : conditions_normal;
auto nc_kv = conditions.find(fas.name_ptr);
2020-09-13 17:34:02 -04:00
if (nc_kv != conditions.end()) {
// it is an optimizable condition!
gc.kind = nc_kv->second;
// get args...
auto args = get_va(rest, rest);
va_check(rest, args, {{}, {}}, {});
auto first_arg = compile_error_guard(args.unnamed.at(0), env);
auto second_arg = compile_error_guard(args.unnamed.at(1), env);
if (is_number(first_arg->type())) {
// it's a numeric comparison, so we may need to coerce.
auto math_mode = get_math_mode(first_arg->type());
// there is no support for comparing bintegers, so we turn the binteger comparison into an
// integer.
if (is_binteger(first_arg->type())) {
first_arg = number_to_integer(condition, first_arg, env);
2020-09-13 17:34:02 -04:00
}
// convert second one to appropriate type as needed
if (is_number(second_arg->type())) {
second_arg = to_math_type(condition, second_arg, math_mode, env);
2020-09-13 17:34:02 -04:00
}
}
// use signed comparison only if first argument is a signed integer (or coerced binteger)
// (floating point ignores this)
gc.is_signed = is_singed_integer_or_binteger(first_arg->type());
// pick between a floating point and an integer comparison.
if (is_float(first_arg->type())) {
gc.a = first_arg->to_fpr(condition, env);
gc.b = second_arg->to_fpr(condition, env);
2020-09-13 17:34:02 -04:00
gc.is_float = true;
} else {
gc.a = first_arg->to_gpr(condition, env);
gc.b = second_arg->to_gpr(condition, env);
2020-09-13 17:34:02 -04:00
}
if (gc.a->type() == TypeSpec("none") || gc.b->type() == TypeSpec("none")) {
throw_compiler_error(condition, "Cannot use none-typed variable in a condition.");
}
2020-09-13 17:34:02 -04:00
return gc;
}
}
}
// not something we can process more. Just evaluate as normal and check if we get false.
2020-09-13 17:34:02 -04:00
// todo - it's possible to optimize a false comparison because the false offset is zero
gc.kind = invert ? ConditionKind::EQUAL : ConditionKind::NOT_EQUAL;
gc.a = compile_error_guard(condition, env)->to_gpr(condition, env);
if (gc.a->type() == TypeSpec("none")) {
throw_compiler_error(condition, "Cannot use none-typed variable in a condition.");
}
gc.b = compile_get_sym_obj("#f", env)->to_gpr(condition, env);
2020-09-13 17:34:02 -04:00
return gc;
}
/*!
* Compile a comparison when we explicitly want a boolean result. This is used whenever a condition
* _isn't_ used as a branch condition. Like (set! x (< 1 2))
*
* TODO, this could be optimized quite a bit.
*/
2020-09-13 17:34:02 -04:00
Val* Compiler::compile_condition_as_bool(const goos::Object& form,
const goos::Object& rest,
Env* env) {
(void)rest;
auto c = compile_condition(form, env, true);
auto result = compile_get_sym_obj("#f", env)->to_gpr(form, env); // todo - can be optimized.
Label label(env->function_env(), -5);
2020-09-13 17:34:02 -04:00
auto branch_ir = std::make_unique<IR_ConditionalBranch>(c, label);
auto branch_ir_ref = branch_ir.get();
env->emit(form, std::move(branch_ir));
2020-09-13 17:34:02 -04:00
// move true
env->emit(form, std::make_unique<IR_RegSet>(result, compile_get_sym_obj("#t", env)->to_gpr(
form, env))); // todo, can be optimized
2020-09-13 17:34:02 -04:00
branch_ir_ref->label.idx = branch_ir_ref->label.func->code().size();
branch_ir_ref->mark_as_resolved();
return result;
}
/*!
* The when-goto form is a better version of (if condition (goto x))
* It compiles into a single conditional branch.
*/
2020-09-13 17:34:02 -04:00
Val* Compiler::compile_when_goto(const goos::Object& form, const goos::Object& _rest, Env* env) {
(void)form;
auto* rest = &_rest;
auto condition_code = pair_car(*rest);
rest = &pair_cdr(*rest);
auto label = symbol_string(pair_car(*rest));
expect_empty_list(pair_cdr(*rest));
// compile as condition (will set flags register with a cmp instruction)
auto condition = compile_condition(condition_code, env, false);
auto branch = std::make_unique<IR_ConditionalBranch>(condition, Label());
env->function_env()->unresolved_cond_gotos.push_back({branch.get(), label});
env->emit(form, std::move(branch));
2020-09-13 17:34:02 -04:00
return get_none();
}
/*!
* The Scheme/Lisp "cond" form.
* Works like you expect. Return type is the lowest common ancestor of all possible return values.
* If no cases match and there's no else, returns #f.
* TODO - how should the return type work if #f can possibly be returned?
*/
2020-09-13 17:34:02 -04:00
Val* Compiler::compile_cond(const goos::Object& form, const goos::Object& rest, Env* env) {
auto result = env->make_gpr(m_ts.make_typespec("object"));
auto fenv = env->function_env();
2020-09-13 17:34:02 -04:00
auto end_label = fenv->alloc_unnamed_label();
end_label->func = fenv;
end_label->idx = -3; // placeholder
bool got_else = false;
std::vector<TypeSpec> case_result_types;
for_each_in_list(rest, [&](const goos::Object& o) {
[opengoal] make `none` a child of `object` (#3001) Previously, `object` and `none` were both top-level types. This made decompilation rather messy as they have no LCA and resulted in a lot of variables coming out as type `none` which is very very wrong and additionally there were plenty of casts to `object`. This changes it so `none` becomes a child of `object` (it is still represented by `NullType` which remains unusable in compilation). This change makes `object` the sole top-level type, and the type that can represent *any* GOAL object. I believe this matches the original GOAL built-in type structure. A function that has a return type of `object` can now return an integer or a `none` at the same time. However, keep in mind that the return value of `(none)` is still undefined, just as before. This also makes a cast to `object` meaningless in 90% of the situations it showed up in (as every single thing is already an `object`) and the decompiler will no longer emit them. Casts to `none` are also reduced. Yay! Additionally, state handlers also don't get the final `(none)` printed out anymore. The return type of a state handler is completely meaningless outside the event handler (which is return type `object` anyway) so there are no limitations on what the last form needs to be. I did this instead of making them return `object` to trick the decompiler into not trying to output a variable to be used as a return value (internally, in the decompiler they still have return type `none`, but they have `object` elsewhere). Fixes #1703 Fixes #830 Fixes #928
2023-09-22 05:54:49 -04:00
const auto& test = pair_car(o);
const auto& clauses = pair_cdr(o);
2020-09-13 17:34:02 -04:00
if (got_else) {
throw_compiler_error(form, "Cond from cannot have any cases after else.");
2020-09-13 17:34:02 -04:00
}
if (test.is_symbol("else")) {
2020-09-13 17:34:02 -04:00
got_else = true;
}
if (got_else) {
// just set the output to this.
Val* case_result = get_none();
for_each_in_list(clauses, [&](const goos::Object& clause) {
case_result = compile_error_guard(clause, env);
if (!dynamic_cast<None*>(case_result)) {
case_result = case_result->to_reg(clause, env);
}
2020-09-13 17:34:02 -04:00
});
case_result_types.push_back(case_result->type());
// optimization - if we get junk, don't bother moving it, just leave junk in return.
if (!is_none(case_result)) {
// todo, what does GOAL do here? does it matter?
2023-01-14 11:04:15 -05:00
env->emit(o, std::make_unique<IR_RegSet>(result, case_result->to_reg(o, env)));
2020-09-13 17:34:02 -04:00
}
} else {
// CONDITION CHECK
auto condition = compile_condition(test, env, true);
// BRANCH FWD
auto branch_ir = std::make_unique<IR_ConditionalBranch>(condition, Label());
auto branch_ir_ref = branch_ir.get();
branch_ir->mark_as_resolved();
env->emit(test, std::move(branch_ir));
2020-09-13 17:34:02 -04:00
// CODE
Val* case_result = get_none();
[opengoal] make `none` a child of `object` (#3001) Previously, `object` and `none` were both top-level types. This made decompilation rather messy as they have no LCA and resulted in a lot of variables coming out as type `none` which is very very wrong and additionally there were plenty of casts to `object`. This changes it so `none` becomes a child of `object` (it is still represented by `NullType` which remains unusable in compilation). This change makes `object` the sole top-level type, and the type that can represent *any* GOAL object. I believe this matches the original GOAL built-in type structure. A function that has a return type of `object` can now return an integer or a `none` at the same time. However, keep in mind that the return value of `(none)` is still undefined, just as before. This also makes a cast to `object` meaningless in 90% of the situations it showed up in (as every single thing is already an `object`) and the decompiler will no longer emit them. Casts to `none` are also reduced. Yay! Additionally, state handlers also don't get the final `(none)` printed out anymore. The return type of a state handler is completely meaningless outside the event handler (which is return type `object` anyway) so there are no limitations on what the last form needs to be. I did this instead of making them return `object` to trick the decompiler into not trying to output a variable to be used as a return value (internally, in the decompiler they still have return type `none`, but they have `object` elsewhere). Fixes #1703 Fixes #830 Fixes #928
2023-09-22 05:54:49 -04:00
const goos::Object* case_clause = nullptr;
2020-09-13 17:34:02 -04:00
for_each_in_list(clauses, [&](const goos::Object& clause) {
case_result = compile_error_guard(clause, env);
[opengoal] make `none` a child of `object` (#3001) Previously, `object` and `none` were both top-level types. This made decompilation rather messy as they have no LCA and resulted in a lot of variables coming out as type `none` which is very very wrong and additionally there were plenty of casts to `object`. This changes it so `none` becomes a child of `object` (it is still represented by `NullType` which remains unusable in compilation). This change makes `object` the sole top-level type, and the type that can represent *any* GOAL object. I believe this matches the original GOAL built-in type structure. A function that has a return type of `object` can now return an integer or a `none` at the same time. However, keep in mind that the return value of `(none)` is still undefined, just as before. This also makes a cast to `object` meaningless in 90% of the situations it showed up in (as every single thing is already an `object`) and the decompiler will no longer emit them. Casts to `none` are also reduced. Yay! Additionally, state handlers also don't get the final `(none)` printed out anymore. The return type of a state handler is completely meaningless outside the event handler (which is return type `object` anyway) so there are no limitations on what the last form needs to be. I did this instead of making them return `object` to trick the decompiler into not trying to output a variable to be used as a return value (internally, in the decompiler they still have return type `none`, but they have `object` elsewhere). Fixes #1703 Fixes #830 Fixes #928
2023-09-22 05:54:49 -04:00
case_clause = &clause;
2020-09-13 17:34:02 -04:00
});
[opengoal] make `none` a child of `object` (#3001) Previously, `object` and `none` were both top-level types. This made decompilation rather messy as they have no LCA and resulted in a lot of variables coming out as type `none` which is very very wrong and additionally there were plenty of casts to `object`. This changes it so `none` becomes a child of `object` (it is still represented by `NullType` which remains unusable in compilation). This change makes `object` the sole top-level type, and the type that can represent *any* GOAL object. I believe this matches the original GOAL built-in type structure. A function that has a return type of `object` can now return an integer or a `none` at the same time. However, keep in mind that the return value of `(none)` is still undefined, just as before. This also makes a cast to `object` meaningless in 90% of the situations it showed up in (as every single thing is already an `object`) and the decompiler will no longer emit them. Casts to `none` are also reduced. Yay! Additionally, state handlers also don't get the final `(none)` printed out anymore. The return type of a state handler is completely meaningless outside the event handler (which is return type `object` anyway) so there are no limitations on what the last form needs to be. I did this instead of making them return `object` to trick the decompiler into not trying to output a variable to be used as a return value (internally, in the decompiler they still have return type `none`, but they have `object` elsewhere). Fixes #1703 Fixes #830 Fixes #928
2023-09-22 05:54:49 -04:00
if (case_clause && !dynamic_cast<None*>(case_result)) {
case_result = case_result->to_reg(*case_clause, env);
}
2020-09-13 17:34:02 -04:00
case_result_types.push_back(case_result->type());
if (!is_none(case_result)) {
// todo, what does GOAL do here?
2023-01-14 11:04:15 -05:00
env->emit(o, std::make_unique<IR_RegSet>(result, case_result->to_reg(o, env)));
2020-09-13 17:34:02 -04:00
}
// GO TO END
auto ir_goto_end = std::make_unique<IR_GotoLabel>(end_label);
env->emit(o, std::move(ir_goto_end));
2020-09-13 17:34:02 -04:00
// PATCH BRANCH FWD
branch_ir_ref->label.idx = fenv->code().size();
}
});
if (!got_else) {
[opengoal] make `none` a child of `object` (#3001) Previously, `object` and `none` were both top-level types. This made decompilation rather messy as they have no LCA and resulted in a lot of variables coming out as type `none` which is very very wrong and additionally there were plenty of casts to `object`. This changes it so `none` becomes a child of `object` (it is still represented by `NullType` which remains unusable in compilation). This change makes `object` the sole top-level type, and the type that can represent *any* GOAL object. I believe this matches the original GOAL built-in type structure. A function that has a return type of `object` can now return an integer or a `none` at the same time. However, keep in mind that the return value of `(none)` is still undefined, just as before. This also makes a cast to `object` meaningless in 90% of the situations it showed up in (as every single thing is already an `object`) and the decompiler will no longer emit them. Casts to `none` are also reduced. Yay! Additionally, state handlers also don't get the final `(none)` printed out anymore. The return type of a state handler is completely meaningless outside the event handler (which is return type `object` anyway) so there are no limitations on what the last form needs to be. I did this instead of making them return `object` to trick the decompiler into not trying to output a variable to be used as a return value (internally, in the decompiler they still have return type `none`, but they have `object` elsewhere). Fixes #1703 Fixes #830 Fixes #928
2023-09-22 05:54:49 -04:00
// if no else clause, return #f.
2020-09-13 17:34:02 -04:00
auto get_false = std::make_unique<IR_LoadSymbolPointer>(result, "#f");
env->emit(form, std::move(get_false));
2020-09-13 17:34:02 -04:00
}
if (case_result_types.empty()) {
[opengoal] make `none` a child of `object` (#3001) Previously, `object` and `none` were both top-level types. This made decompilation rather messy as they have no LCA and resulted in a lot of variables coming out as type `none` which is very very wrong and additionally there were plenty of casts to `object`. This changes it so `none` becomes a child of `object` (it is still represented by `NullType` which remains unusable in compilation). This change makes `object` the sole top-level type, and the type that can represent *any* GOAL object. I believe this matches the original GOAL built-in type structure. A function that has a return type of `object` can now return an integer or a `none` at the same time. However, keep in mind that the return value of `(none)` is still undefined, just as before. This also makes a cast to `object` meaningless in 90% of the situations it showed up in (as every single thing is already an `object`) and the decompiler will no longer emit them. Casts to `none` are also reduced. Yay! Additionally, state handlers also don't get the final `(none)` printed out anymore. The return type of a state handler is completely meaningless outside the event handler (which is return type `object` anyway) so there are no limitations on what the last form needs to be. I did this instead of making them return `object` to trick the decompiler into not trying to output a variable to be used as a return value (internally, in the decompiler they still have return type `none`, but they have `object` elsewhere). Fixes #1703 Fixes #830 Fixes #928
2023-09-22 05:54:49 -04:00
// completely empty cond
result->set_type(TypeSpec("none"));
} else {
[opengoal] make `none` a child of `object` (#3001) Previously, `object` and `none` were both top-level types. This made decompilation rather messy as they have no LCA and resulted in a lot of variables coming out as type `none` which is very very wrong and additionally there were plenty of casts to `object`. This changes it so `none` becomes a child of `object` (it is still represented by `NullType` which remains unusable in compilation). This change makes `object` the sole top-level type, and the type that can represent *any* GOAL object. I believe this matches the original GOAL built-in type structure. A function that has a return type of `object` can now return an integer or a `none` at the same time. However, keep in mind that the return value of `(none)` is still undefined, just as before. This also makes a cast to `object` meaningless in 90% of the situations it showed up in (as every single thing is already an `object`) and the decompiler will no longer emit them. Casts to `none` are also reduced. Yay! Additionally, state handlers also don't get the final `(none)` printed out anymore. The return type of a state handler is completely meaningless outside the event handler (which is return type `object` anyway) so there are no limitations on what the last form needs to be. I did this instead of making them return `object` to trick the decompiler into not trying to output a variable to be used as a return value (internally, in the decompiler they still have return type `none`, but they have `object` elsewhere). Fixes #1703 Fixes #830 Fixes #928
2023-09-22 05:54:49 -04:00
auto lca = m_ts.lowest_common_ancestor(case_result_types);
if (!got_else && lca == TypeSpec("none")) {
// least common ancestor was none, but there is still the #f from the missing else case.
// elevate cond to an `object` overall so we can capture that #f!
// (`object` is the direct ancestor of `none` and the ancestor to all types overall)
// TODO : what does goal do here?
lca = TypeSpec("object");
}
result->set_type(coerce_to_reg_type(lca));
}
2020-09-13 17:34:02 -04:00
2023-01-14 11:04:15 -05:00
// maybe use 128-bit register
if (result->type().base_type() != "none" &&
m_ts.lookup_type_allow_partial_def(result->type())->get_load_size() == 16) {
result->change_class(RegClass::INT_128);
}
2020-09-13 17:34:02 -04:00
// PATCH END
end_label->idx = fenv->code().size();
return result;
}
Val* Compiler::compile_and_or(const goos::Object& form, const goos::Object& rest, Env* env) {
std::string op_name = form.as_pair()->car.as_symbol().name_ptr;
bool is_and = false;
if (op_name == "and") {
is_and = true;
} else if (op_name == "or") {
is_and = false;
} else {
throw_compiler_error(form, "compile_and_or got an invalid operation {}", op_name);
}
if (rest.is_empty_list()) {
throw_compiler_error(form, "and/or form must have at least one element");
}
auto result = env->make_gpr(m_ts.make_typespec("object")); // temp type for now.
auto fenv = env->function_env();
auto end_label = fenv->alloc_unnamed_label();
end_label->func = fenv;
end_label->idx = -4; // placeholder
std::vector<TypeSpec> case_result_types;
case_result_types.push_back(TypeSpec("symbol")); // can always return #f.
std::vector<IR_ConditionalBranch*> branch_irs;
auto n_elts = goos::list_length(rest);
int i = 0;
for_each_in_list(rest, [&](const goos::Object& o) {
// get the result of this case, put it in the main result and remember the type
auto temp = compile_error_guard(o, env)->to_gpr(o, env);
case_result_types.push_back(temp->type());
env->emit_ir<IR_RegSet>(o, result, temp);
// no need check if we are the last element.
if (i != n_elts - 1) {
// now, check.
Condition gc;
gc.is_signed = false;
gc.is_float = false;
gc.a = result;
gc.b = compile_get_sym_obj("#f", env)->to_gpr(o, env); // todo, optimize
if (is_and) {
// for and we abort if we get a false:
gc.kind = ConditionKind::EQUAL;
} else {
// for or, we abort when we get truthy
gc.kind = ConditionKind::NOT_EQUAL;
}
// jump to end
auto branch = std::make_unique<IR_ConditionalBranch>(gc, Label());
branch_irs.push_back(branch.get());
env->emit(o, std::move(branch));
}
i++;
});
// now patch branches
end_label->idx = fenv->code().size();
for (auto* br : branch_irs) {
br->label = *end_label;
br->mark_as_resolved();
}
// and set the result type
result->set_type(m_ts.lowest_common_ancestor(case_result_types));
2020-09-13 17:34:02 -04:00
return result;
}