Load from globals only at the top level, do rest with closures

This commit is contained in:
Konstantin Nazarov 2024-08-31 19:42:27 +01:00
parent 4ab6142ca5
commit 5279b44441
Signed by: knazarov
GPG key ID: 4CFE0A42FA409C22
5 changed files with 91 additions and 35 deletions

View file

@ -6,10 +6,10 @@
struct Context {
Context() {}
Context(Value&& env, Array&& constants, Dict&& constants_dict,
Context(Value&& fname, Array&& constants, Dict&& constants_dict,
Dict&& variables_dict, Array&& closures, Dict&& closures_dict,
Dict&& globals_dict, Context* parent, bool toplevel)
: env(std::move(env)),
: fname(std::move(fname)),
constants(std::move(constants)),
constants_dict(std::move(constants_dict)),
variables_dict(std::move(variables_dict)),
@ -21,7 +21,7 @@ struct Context {
toplevel(toplevel) {}
static Result<Context> create() {
auto env = TRY(Nil::create());
auto fname = TRY(Nil::create());
auto constants = TRY(Array::create());
auto constants_dict = TRY(Dict::create());
auto variables_dict = TRY(Dict::create());
@ -29,14 +29,14 @@ struct Context {
auto closures_dict = TRY(Dict::create());
auto globals_dict = TRY(Dict::create());
return Context(std::move(env), std::move(constants),
return Context(std::move(fname), std::move(constants),
std::move(constants_dict), std::move(variables_dict),
std::move(closures), std::move(closures_dict),
std::move(globals_dict), 0, true);
}
static Result<Context> create(Context& parent) {
auto env = TRY(Nil::create());
auto fname = TRY(Nil::create());
auto constants = TRY(Array::create());
auto constants_dict = TRY(Dict::create());
auto variables_dict = TRY(Dict::create());
@ -44,7 +44,7 @@ struct Context {
auto closures_dict = TRY(Dict::create());
auto globals_dict = TRY(Dict::create());
return Context(std::move(env), std::move(constants),
return Context(std::move(fname), std::move(constants),
std::move(constants_dict), std::move(variables_dict),
std::move(closures), std::move(closures_dict),
std::move(globals_dict), &parent, false);
@ -123,13 +123,17 @@ struct Context {
return idx.value().to<Int64>()->value();
}
if (!parent) return ERROR(KeyError);
auto maybe_var = parent->get_var(sym);
// If parent has no variable with name <sym>, then we need to propagate the
// closure to it. The variable may be defined somewhere in outer scope.
if (!maybe_var.has_value()) {
TRY(parent->get_closure(sym));
// We need to try and look up a symbol in the parent scope. If there's no
// parent scope, we will still create a closure, but that closure will be
// resolved dynamically at runtime.
if (parent) {
auto maybe_var = parent->get_var(sym);
// If parent has no variable with name <sym>, then we need to propagate
// the closure to it. The variable may be defined somewhere in outer
// scope.
if (!maybe_var.has_value()) {
TRY(parent->get_closure(sym));
}
}
int64_t i = closures.size();
@ -144,7 +148,7 @@ struct Context {
return Result<void>();
}
Value env;
Value fname;
Array constants;
Dict constants_dict;
Dict variables_dict;
@ -580,6 +584,7 @@ Result<Expression> Compiler::compile_fn(Context& context, Symbol& op,
if (maybe_name.is<Symbol>()) {
name = TRY(maybe_name.copy());
ctx.fname = TRY(name.copy());
first = TRY(first.rest());
@ -665,7 +670,7 @@ Result<Expression> Compiler::compile_fn(Context& context, Symbol& op,
// (such as in case of 3 nested functions where a var we close over is
// defined in the outermost function.
auto maybe_closure = context.get_closure(sym);
if (maybe_closure.has_value()) {
if (maybe_closure.has_value() && !context.toplevel) {
int64_t closure_id = maybe_closure.value();
uint64_t vr = context.alloc_reg();
TRY(ex_res.add_opcode(Oc::ClosureLoad, {0, (int64_t)vr},
@ -673,7 +678,10 @@ Result<Expression> Compiler::compile_fn(Context& context, Symbol& op,
continue;
}
return ERROR(NotImplemented);
int64_t c = TRY(context.add_const(sym));
uint64_t vr = context.alloc_reg();
TRY(ex_res.add_opcode(Oc::GlobalLoad, {0, (int64_t)vr}, {1, (int64_t)c}));
}
TRY(ex_res.add_opcode(Oc::MakeClosure, {0, (int64_t)reg},
@ -780,8 +788,17 @@ Result<Expression> Compiler::compile_function_call(Context& context,
auto first = TRY(expr.first());
auto param = TRY(expr.rest());
auto fun_ex = TRY(compile_expr(context, first));
TRY(ex.add_code(fun_ex.code));
bool is_self_call = false;
uint64_t firstreg = context.maxreg;
if (first.is<Symbol>() && TRY(first.cmp(context.fname)) == 0) {
int64_t r = context.alloc_reg();
int64_t c = TRY(context.add_const(TRY(Nil::create())));
TRY(ex.add_opcode(Oc::Mov, {0, (int64_t)r}, {1, (int64_t)c}));
is_self_call = true;
} else {
auto fun_ex = TRY(compile_expr(context, first));
TRY(ex.add_code(fun_ex.code));
}
int64_t maxreg = context.maxreg;
while (!param.is<Nil>()) {
@ -796,10 +813,15 @@ Result<Expression> Compiler::compile_function_call(Context& context,
param = TRY(param.rest());
}
TRY(ex.add_opcode(Oc::Call, {0, (int64_t)fun_ex.reg},
{0, (int64_t)context.maxreg}));
if (is_self_call) {
TRY(ex.add_opcode(Oc::SelfCall, {0, (int64_t)firstreg},
{0, (int64_t)context.maxreg}));
} else {
TRY(ex.add_opcode(Oc::Call, {0, (int64_t)firstreg},
{0, (int64_t)context.maxreg}));
}
ex.reg = fun_ex.reg;
ex.reg = firstreg;
context.maxreg = maxreg;
return ex;
}
@ -864,19 +886,6 @@ Result<Expression> Compiler::compile_symbol(Context& context,
return std::move(ex);
}
auto maybe_closure = context.get_closure(TRY(value.copy()));
if (!maybe_closure.has_error()) {
auto var_closure = maybe_closure.value();
uint64_t reg = context.alloc_reg();
TRY(ex.add_opcode(Oc::ClosureLoad, {0, (int64_t)reg},
{0, (int64_t)var_closure}));
ex.reg = reg;
return std::move(ex);
}
auto maybe_stdlib_fun = get_stdlib_function(*value.to<Symbol>());
if (!maybe_stdlib_fun.has_error()) {
@ -890,8 +899,25 @@ Result<Expression> Compiler::compile_symbol(Context& context,
return std::move(ex);
}
// Most variable references in function bodies will fall back to loading the
// accessed variables from parent closures.
if (!context.toplevel) {
auto maybe_closure = context.get_closure(TRY(value.copy()));
if (!maybe_closure.has_error()) {
auto var_closure = maybe_closure.value();
uint64_t reg = context.alloc_reg();
TRY(ex.add_opcode(Oc::ClosureLoad, {0, (int64_t)reg},
{0, (int64_t)var_closure}));
ex.reg = reg;
return std::move(ex);
}
}
// Otherwise treat unknown symbol as a global and try to load it from the
// global scope
// global scope. This usually happens in toplevel expressions.
int64_t c = TRY(context.add_const(TRY(value.copy())));

View file

@ -39,6 +39,8 @@ op_t get_op(Oc op) {
return op_t{"less-equal", OpcodeType::Reg2I};
case Oc::Call:
return op_t{"call", OpcodeType::Reg2};
case Oc::SelfCall:
return op_t{"selfcall", OpcodeType::Reg2};
case Oc::TailCall:
return op_t{"tailcall", OpcodeType::Reg1I};
case Oc::Ret:

View file

@ -33,6 +33,7 @@ enum class Oc : uint8_t {
LessEqual,
// Function calls
Call,
SelfCall,
TailCall,
// Return from function call
Ret,

View file

@ -156,6 +156,29 @@ Result<void> VM::vm_call(Opcode& oc) {
return ERROR(TypeMismatch);
}
Result<void> VM::vm_selfcall(Opcode& oc) {
uint64_t reg_start = (uint64_t)oc.arg1().arg;
uint64_t reg_end = (uint64_t)oc.arg2().arg;
if (_fun.arity() != (reg_end - reg_start - 1)) {
return ERROR(ArgumentCountMismatch);
}
uint64_t old_base = _base;
Value fun_val = TRY(_fun.copy());
Value oldbase_val = TRY(Int64::create(old_base));
Value pc_val = TRY(Int64::create(_pc));
_callstack.set(_callstack.gettop(), fun_val);
_callstack.set(_callstack.gettop(), oldbase_val);
_callstack.set(_callstack.gettop(), pc_val);
_pc = 0;
_base = _base + reg_start;
return Result<void>();
}
Result<void> VM::vm_ret(Opcode& oc) {
if (_callstack.gettop() == 0) {
_res = TRY(getreg((uint64_t)oc.arg1().arg));
@ -267,6 +290,9 @@ Result<void> VM::step() {
case Oc::Call:
TRY(vm_call(oc));
break;
case Oc::SelfCall:
TRY(vm_selfcall(oc));
break;
case Oc::MakeClosure:
TRY(vm_make_closure(oc));
break;

View file

@ -26,6 +26,7 @@ class VM {
Result<void> vm_sub(Opcode& oc);
Result<void> vm_div(Opcode& oc);
Result<void> vm_call(Opcode& oc);
Result<void> vm_selfcall(Opcode& oc);
Result<void> vm_call_lisp(Opcode& oc, Function& fun);
Result<void> vm_call_stdlib(Opcode& oc, StdlibFunction& fun);
Result<void> vm_ret(Opcode& oc);