Files
jak-project/goalc/compiler/compilation/Macro.cpp
T
Matt Dallmeyer 2071c98b55 Fix cases of string formatting with non string literals (#3304)
The logger used in `goalc` tries to print an already-formatted string
`message` using `fmt::print(message);` Usually this doesn't cause
problems, but if you try to print, for example, an exception that has
special characters (notably `{`) it will try to do another round of
formatting/replacements, despite not having any args to replace with,
which ends up throwing another exception. This is why errors when
parsing custom level JSON cause the REPL to exit.

I've hopefully identified all the various instances of this across the
codebase
2024-01-14 12:02:08 +00:00

321 lines
11 KiB
C++

#include "common/goos/PrettyPrinter.h"
#include "goalc/compiler/Compiler.h"
#include "third-party/fmt/core.h"
using namespace goos;
/*!
* Try to find a macro with the given name in the GOOS "goal_env". Return if it succeeded.
*/
bool Compiler::try_getting_macro_from_goos(const goos::Object& macro_name, goos::Object* dest) {
Object macro_obj;
bool got_macro = false;
if (m_goos.eval_symbol(macro_name, m_goos.goal_env.as_env_ptr(), &macro_obj) &&
macro_obj.is_macro()) {
got_macro = true;
}
if (got_macro && dest) {
*dest = macro_obj;
}
return got_macro;
}
/*!
* Expand a macro, then compile the result.
*/
Val* Compiler::compile_goos_macro(const goos::Object& o,
const goos::Object& macro_obj,
const goos::Object& rest,
const goos::Object& name,
Env* env) {
auto macro = macro_obj.as_macro();
Arguments args = m_goos.get_args(o, rest, macro->args);
auto mac_env_obj = EnvironmentObject::make_new();
auto mac_env = mac_env_obj.as_env_ptr();
mac_env->parent_env = m_goos.global_environment.as_env_ptr();
m_goos.set_args_in_env(o, args, macro->args, mac_env);
auto goos_result = m_goos.eval_list_return_last(macro->body, macro->body, mac_env);
// make the macro expanded form point to the source where the macro was used for error messages.
// m_goos.reader.db.inherit_info(o, goos_result);
auto compile_env_for_macro =
env->function_env()->alloc_env<MacroExpandEnv>(env, name.as_symbol(), macro->body, o);
try {
const auto& compile_result = compile(goos_result, compile_env_for_macro);
m_macro_specs.emplace(macro->name, macro->args);
return compile_result;
} catch (CompilerException& ce) {
if (ce.print_err_stack) {
bool good_info = false;
auto info = m_goos.reader.db.get_info_for(o, &good_info);
lg::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "Code:\n");
auto code_str = pretty_print::to_string(goos_result, 120);
if (code_str.size() > 120 * 30) {
code_str = code_str.substr(0, 120 * 30) + "...";
}
lg::print("{}\n", code_str);
lg::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "From macro: ");
lg::print(fg(fmt::color::orange), "{}\n", name.print());
if (good_info) {
lg::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "Used in:\n");
lg::print("{}\n", info);
ce.print_err_stack = false;
}
std::string line(80, '-');
line.push_back('\n');
lg::print("{}", line);
}
throw;
} catch (std::runtime_error& e) {
bool good_info = false;
auto info = m_goos.reader.db.get_info_for(o, &good_info);
lg::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "Code:\n");
auto code_str = pretty_print::to_string(goos_result, 120);
if (code_str.size() > 120 * 30) {
code_str = code_str.substr(0, 120 * 30) + "...";
}
lg::print("{}\n", code_str);
lg::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "From macro: ");
lg::print(fg(fmt::color::orange), "{}\n", name.print());
if (good_info) {
lg::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "Used in:\n");
lg::print("{}\n", info);
}
std::string line(80, '-');
line.push_back('\n');
lg::print("{}", line);
throw;
}
}
/*!
* Compile the #cond form, which is a compile-time conditional statement.
*/
Val* Compiler::compile_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");
}
Val* result = nullptr;
Object lst = rest;
for (;;) {
if (lst.is_pair()) {
Object current_case = lst.as_pair()->car;
if (!current_case.is_pair()) {
throw_compiler_error(lst, "Bad case in #cond");
}
// check condition:
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()) {
return get_none();
}
// got a match!
result = get_none();
for_each_in_list(current_case.as_pair()->cdr, [&](const Object& o) {
result = compile_error_guard(o, env);
if (!dynamic_cast<None*>(result)) {
result = result->to_reg(o, env);
}
});
return result;
} else {
// no match, continue.
lst = lst.as_pair()->cdr;
}
} else if (lst.is_empty_list()) {
return get_none();
} else {
throw_compiler_error(form, "malformed #cond");
}
}
}
/*!
* Compile (quote x) or 'x forms.
* Current only supports 'thing or '(). Static lists/pairs should be added at some point.
*/
Val* Compiler::compile_quote(const goos::Object& form, const goos::Object& rest, Env* env) {
auto args = get_va_no_named(form, rest);
if (args.unnamed.size() != 1) {
throw_compiler_error(form, "invalid number of arguments to compile quote");
}
auto& thing = args.unnamed.front();
switch (thing.type) {
case goos::ObjectType::SYMBOL:
return compile_get_sym_obj(thing.as_symbol().name_ptr, env);
case goos::ObjectType::EMPTY_LIST: {
auto empty_pair = compile_get_sym_obj("_empty_", env);
empty_pair->set_type(m_ts.make_typespec("pair"));
return empty_pair;
}
case goos::ObjectType::PAIR:
return compile_static_pair(thing, env, env->function_env()->segment_for_static_data());
default:
throw_compiler_error(form, "Quote is not yet implemented for {}.", thing.print());
}
return get_none();
}
Val* Compiler::compile_define_constant(const goos::Object& form,
const goos::Object& _rest,
Env* env,
bool goos,
bool goal) {
auto rest = &_rest;
(void)env;
if (!rest->is_pair()) {
throw_compiler_error(form, "invalid constant definition");
}
auto sym = pair_car(*rest).as_symbol();
rest = &pair_cdr(*rest);
// check for potential docstring
SymbolInfo::Metadata sym_meta;
if (rest->is_pair() && pair_car(*rest).is_string() && !pair_cdr(*rest).is_empty_list()) {
std::string docstring = pair_car(*rest).as_string()->data;
sym_meta.docstring = docstring;
rest = &pair_cdr(*rest);
}
auto& value = pair_car(*rest);
rest = &rest->as_pair()->cdr;
if (!rest->is_empty_list()) {
throw_compiler_error(form, "invalid constant definition");
}
// GOAL constant
if (goal) {
if (m_symbol_types.find(sym) != m_symbol_types.end()) {
throw_compiler_error(form,
"Cannot define {} as a constant because "
"it is already the name of a symbol of type {}",
sym.name_ptr, m_symbol_types.at(sym).print());
}
auto existing = m_global_constants.find(sym);
if (existing != m_global_constants.end() && existing->second != value) {
print_compiler_warning("Constant {} has been redefined {} -> {}", sym.name_ptr,
existing->second.print(), value.print());
}
m_global_constants[sym] = value;
}
// GOOS constant
if (goos) {
m_goos.global_environment.as_env()->vars.set(sym, value);
}
// TODO - eventually, it'd be nice if global constants were properly typed
// and this information was propagated
m_symbol_info.add_constant(sym.name_ptr, form, sym_meta);
return get_none();
}
/*!
* Compile defglobalconstant forms, which define a constant in both GOOS and GOAL.
*/
Val* Compiler::compile_defglobalconstant(const goos::Object& form,
const goos::Object& rest,
Env* env) {
return compile_define_constant(form, rest, env, true, true);
}
/*!
* Compile a defconstant form, which defines a constant that is only in GOAL.
*/
Val* Compiler::compile_defconstant(const goos::Object& form, const goos::Object& rest, Env* env) {
return compile_define_constant(form, rest, env, false, true);
}
/*!
* Compile an "mlet" scoped constant/symbol macro form
*/
Val* Compiler::compile_mlet(const goos::Object& form, const goos::Object& rest, Env* env) {
auto defs = pair_car(rest);
auto body = pair_cdr(rest);
auto fenv = env->function_env();
auto menv = fenv->alloc_env<SymbolMacroEnv>(env);
for_each_in_list(defs, [&](const goos::Object& o) {
auto def_args = get_va(form, o);
va_check(form, def_args, {goos::ObjectType::SYMBOL, {}}, {});
menv->macros[def_args.unnamed.at(0).as_symbol()] = def_args.unnamed.at(1);
});
Val* result = get_none();
for_each_in_list(body, [&](const goos::Object& o) {
result = compile_error_guard(o, menv);
if (!dynamic_cast<None*>(result)) {
result = result->to_reg(o, menv);
}
});
return result;
}
Val* Compiler::compile_macro_expand(const goos::Object& form, const goos::Object& rest, Env* env) {
auto& macro = pair_car(rest);
auto& macro_name = pair_car(macro);
if (!try_getting_macro_from_goos(macro_name, nullptr)) {
throw_compiler_error(form, "invalid argument to `macro-expand`: {} does not exist as a macro",
macro_name.print());
}
if (!pair_cdr(rest).is_empty_list()) {
throw_compiler_error(form, "too many arguments to `macro-expand`");
}
auto result = expand_macro_completely(macro, env);
auto code = pretty_print::to_string(result);
lg::print("{}\n", code);
return get_none();
}
bool Compiler::expand_macro_once(const goos::Object& src, goos::Object* out, Env*) {
if (!src.is_pair()) {
return false;
}
auto& first = src.as_pair()->car;
auto& rest = src.as_pair()->cdr;
if (!first.is_symbol()) {
return false;
}
goos::Object macro_obj;
if (!try_getting_macro_from_goos(first, &macro_obj)) {
return false;
}
auto macro = macro_obj.as_macro();
Arguments args = m_goos.get_args(src, rest, macro->args);
auto mac_env_obj = EnvironmentObject::make_new();
auto mac_env = mac_env_obj.as_env_ptr();
mac_env->parent_env = m_goos.global_environment.as_env_ptr();
m_goos.set_args_in_env(src, args, macro->args, mac_env);
auto goos_result = m_goos.eval_list_return_last(macro->body, macro->body, mac_env);
// make the macro expanded form point to the source where the macro was used for error messages.
// m_goos.reader.db.inherit_info(src, goos_result);
*out = goos_result;
return true;
}
goos::Object Compiler::expand_macro_completely(const goos::Object& src, Env* env) {
goos::Object result = src;
while (expand_macro_once(result, &result, env)) {
}
return result;
}