jak-project/goalc/compiler/compilation/ConstantPropagation.cpp
Tyler Wilding 4d751af38e
logs: replace every fmt::print with a lg call instead (#1368)
Favors the `lg` namespace over `fmt` directly, as this will output the
logs to a file / has log levels.

I also made assertion errors go to a file, this unfortunately means
importing `lg` and hence `fmt` which was attempted to be avoided before.
But I'm not sure how else to do this aspect without re-inventing the
file logging.

We have a lot of commented out prints as well that we should probably
cleanup at some point / switch them to trace level and default to `info`
level.

I noticed the pattern of disabling debug logs behind some boolean,
something to consider cleaning up in the future -- if our logs were more
structured (knowing where they are coming from) then a lot this
boilerplate could be eliminated.

Closes #1358
2022-10-01 11:58:36 -04:00

319 lines
11 KiB
C++

#include "goalc/compiler/Compiler.h"
/*!
* Main table for compiler forms that can be constant propagated.
*/
const std::unordered_map<std::string,
Compiler::ConstPropResult (Compiler::*)(const goos::Object& form,
const goos::Object& rest,
Env* env)>
g_const_prop_forms = {
// INLINE ASM
{"begin", &Compiler::const_prop_begin},
{"size-of", &Compiler::const_prop_size_of},
{"#cond", &Compiler::const_prop_gscond}};
// Note: writing const_prop functions is a bit tricky because you have to try expanding macros, but
// if you decide that you can't constant propagate, then there's no way to "undo" any side effects
// from the macro expansion. So the solution is to return a form with all macro expansions already
// applied.
// The result should be a goos Object that is either code to be compiled, or some constant
// integer/string/float/symbol. The const prop functions should emit no code.
/*!
* Constant propagate a form like:
* (begin a b c d ...)
* The head doesn't have to be "begin" and it will still work (the form argument is ignored).
*
* If constant propagation fails, it will return a form like
* (begin a c ..)
* where the head is always begin (even if it wasn't originally), and some of a, b, c...
* may be macro expanded, or omitted if they have no side effects.
*
* If constant propagation succeeds (the expression is a compile time constant with no side effects)
* it will return a single value. The other values don't matter at all.
*
* This applies constant propagation recursively, so elements may be macro expanded and nested
* begins can be eliminated.
*
* This function can generally be used to constant propagate any "body" of code.
*/
Compiler::ConstPropResult Compiler::const_prop_begin(const goos::Object& /*form*/,
const goos::Object& rest,
Env* env) {
ConstPropResult result;
result.has_side_effects = false;
result.value = goos::PairObject::make_new({}, {});
goos::Object* out_it = &result.value.as_pair()->cdr;
const goos::Object* it = &rest;
while (!it->is_empty_list()) {
const goos::Object& obj = it->as_pair()->car;
auto this_elt_prop =
result.has_side_effects ? ConstPropResult{obj, true} : try_constant_propagation(obj, env);
if (this_elt_prop.has_side_effects) {
result.has_side_effects = true;
}
it = &it->as_pair()->cdr;
if (it->is_empty_list() && !result.has_side_effects) {
// can throw out the begin and replace it with the last thing
return this_elt_prop;
}
if (this_elt_prop.has_side_effects) {
*out_it = goos::PairObject::make_new(this_elt_prop.value, {});
out_it = &out_it->as_pair()->cdr;
}
}
result.value.as_pair()->car = m_goos.intern("begin");
*out_it = goos::Object::make_empty_list();
return result;
}
/*!
* Constant propagate a #cond form.
* This will always evaluate the actual conditions.
* In cases where we return none, it gives up constant propagation, as we can't really do anything
* with that.
* In other cases, it tries to constant propagate the body of the matching case.
*/
Compiler::ConstPropResult Compiler::const_prop_gscond(const goos::Object& form,
const goos::Object& rest,
Env* env) {
if (!rest.is_pair()) {
throw_compiler_error(form, "#cond must have at least one clause, which must be a form");
}
goos::Object lst = rest;
for (;;) {
if (lst.is_pair()) {
goos::Object current_case = lst.as_pair()->car;
if (!current_case.is_pair()) {
throw_compiler_error(lst, "Bad case in #cond");
}
// check condition:
goos::Object condition_result = m_goos.eval_with_rewind(
current_case.as_pair()->car, m_goos.global_environment.as_env_ptr());
if (m_goos.truthy(condition_result)) {
if (current_case.as_pair()->cdr.is_empty_list()) {
// would return none, let's just return that this has side effects and let the compiler
// handle it.
return {form, true};
}
// got a match!
return const_prop_begin(current_case, current_case.as_pair()->cdr, env);
} else {
// no match, continue.
lst = lst.as_pair()->cdr;
}
} else if (lst.is_empty_list()) {
return {form, true};
} else {
throw_compiler_error(form, "malformed #cond");
}
}
}
namespace {
size_t code_size(Env* e) {
auto fe = e->function_env();
if (fe) {
return fe->code().size();
} else {
return 0;
}
}
} // namespace
Compiler::ConstPropResult Compiler::try_constant_propagation(const goos::Object& form, Env* env) {
size_t start_size = code_size(env);
auto ret = constant_propagation_dispatch(form, env);
size_t end_size = code_size(env);
if (start_size != end_size) {
lg::print("Compiler bug in constant propagation. Code was generated: {} vs {}\n", start_size,
end_size);
ASSERT(false);
}
return ret;
}
/*!
* Main constant propagation dispatch.
* Note that there are some tricky orders to get right here - we don't want to use a global constant
* when the normal compiler would use a lexical variable.
*/
Compiler::ConstPropResult Compiler::constant_propagation_dispatch(const goos::Object& code,
Env* env) {
// first, expand macros.
// this only does something if code is a pair, and for pairs macros are the first check.
auto expanded = expand_macro_completely(code, env);
switch (expanded.type) {
case goos::ObjectType::INTEGER:
case goos::ObjectType::STRING:
case goos::ObjectType::FLOAT:
// we got a plain value, no code is needed to figure out the value and we can return it
// directly.
return {expanded, false};
case goos::ObjectType::SYMBOL: {
// NOTE: order must match compile_symbol
// #t/#f
// mlet
// lexical
// constant/symbol
// first, try to resolve the symbol in an mlet environment.
auto mlet_env = env->symbol_macro_env();
while (mlet_env) {
auto mlkv = mlet_env->macros.find(expanded.as_symbol());
if (mlkv != mlet_env->macros.end()) {
// we found a match, substitute and keep trying.
return try_constant_propagation(mlkv->second, env);
}
mlet_env = mlet_env->parent()->symbol_macro_env();
}
// see if it's a local variable
auto lexical = env->lexical_lookup(expanded);
if (lexical) {
// if so, that's what we should use, not a constant with the same name.
// give up on constant propagation, we never do it for lexicals (can't really in a single
// pass).
return {expanded, true};
}
// it can either be a global or symbol
const auto& global_constant = m_global_constants.find(expanded.as_symbol());
const auto& existing_symbol = m_symbol_types.find(expanded.as_symbol()->name);
// see if it's a constant
if (global_constant != m_global_constants.end()) {
// check there is no symbol with the same name, this is likely a bug and complain.
if (existing_symbol != m_symbol_types.end()) {
throw_compiler_error(
code,
"Ambiguous symbol: {} is both a global variable and a constant and it "
"is not clear which should be used here.");
}
// got a global constant
return try_constant_propagation(global_constant->second, env);
} else {
// return to the compiler, we can't figure it out.
return {expanded, true};
}
} break;
case goos::ObjectType::PAIR: {
auto pair = expanded.as_pair();
auto head = pair->car;
auto rest = pair->cdr;
// in theory you could write code like:
// ((#if PC_PORT foo bar) ...)
// and you might want the compiler to constant propagate (foo ...)
// but this is not implemented because the logic in compile_function_or_method_call
// is quite complicated and this case seems unlikely to ever be used.
if (head.is_symbol()) {
auto head_sym = head.as_symbol();
// the first thing tried should be macros, but we already did that. And it iterates until
// all are expanded, so we don't need to do it again.
// first try as a goal compiler form
auto kv_gfs = g_const_prop_forms.find(head_sym->name);
if (kv_gfs != g_const_prop_forms.end()) {
return ((*this).*(kv_gfs->second))(expanded, rest, env);
}
const auto& kv_goal = g_goal_forms.find(head_sym->name);
if (kv_goal != g_goal_forms.end()) {
// it's a compiler form that we can't constant propagate.
return {expanded, true};
}
}
return {expanded, true};
}
default:
return {expanded, true};
}
}
s64 Compiler::get_constant_integer_or_error(const goos::Object& in, Env* env) {
auto prop = try_constant_propagation(in, env);
if (prop.value.is_pair()) {
auto head = prop.value.as_pair()->car;
if (head.is_symbol()) {
auto head_sym = head.as_symbol();
auto enum_type = m_ts.try_enum_lookup(head_sym->name);
if (enum_type) {
bool success;
u64 as_enum =
enum_lookup(prop.value, enum_type, prop.value.as_pair()->cdr, false, &success);
if (success) {
return as_enum;
}
}
}
}
if (prop.has_side_effects) {
throw_compiler_error(in, "Value {} cannot be used as a constant.", in.print());
} else {
if (prop.value.is_int()) {
return prop.value.as_int();
} else {
throw_compiler_error(
in, "Value {} cannot be used as a constant integer - it has the wrong type.", in.print());
}
}
}
ValOrConstInt Compiler::get_constant_integer_or_variable(const goos::Object& in, Env* env) {
auto prop = try_constant_propagation(in, env);
if (prop.value.is_pair()) {
auto head = prop.value.as_pair()->car;
if (head.is_symbol()) {
auto head_sym = head.as_symbol();
auto enum_type = m_ts.try_enum_lookup(head_sym->name);
if (enum_type) {
bool success;
u64 as_enum =
enum_lookup(prop.value, enum_type, prop.value.as_pair()->cdr, false, &success);
if (success) {
return ValOrConstInt(as_enum);
}
}
}
}
if (prop.has_side_effects) {
return ValOrConstInt(compile_no_const_prop(prop.value, env));
} else {
if (prop.value.is_int()) {
return ValOrConstInt(prop.value.as_int());
} else {
return ValOrConstInt(compile_no_const_prop(prop.value, env));
}
}
}
ValOrConstFloat Compiler::get_constant_float_or_variable(const goos::Object& in, Env* env) {
auto prop = try_constant_propagation(in, env);
if (prop.has_side_effects) {
return ValOrConstFloat(compile_no_const_prop(prop.value, env));
} else {
if (prop.value.is_float()) {
return ValOrConstFloat(prop.value.as_float());
} else {
return ValOrConstFloat(compile_no_const_prop(prop.value, env));
}
}
}