2020-12-17 14:51:56 -05:00
|
|
|
#include "vm_core.h"
|
|
|
|
#include "vm_callinfo.h"
|
|
|
|
#include "builtin.h"
|
|
|
|
#include "insns.inc"
|
|
|
|
#include "insns_info.inc"
|
2021-01-20 12:44:24 -05:00
|
|
|
#include "vm_sync.h"
|
2020-12-08 16:54:41 -05:00
|
|
|
#include "ujit_asm.h"
|
2020-12-17 14:51:56 -05:00
|
|
|
#include "ujit_utils.h"
|
2020-12-08 16:54:41 -05:00
|
|
|
#include "ujit_iface.h"
|
|
|
|
#include "ujit_core.h"
|
2020-12-10 16:59:13 -05:00
|
|
|
#include "ujit_codegen.h"
|
2020-12-10 00:06:10 -05:00
|
|
|
|
2021-01-27 13:02:55 -05:00
|
|
|
// Maximum number of versions per block
|
|
|
|
#define MAX_VERSIONS 4
|
|
|
|
|
2020-12-16 17:07:18 -05:00
|
|
|
// Maximum number of branch instructions we can track
|
|
|
|
#define MAX_BRANCHES 32768
|
|
|
|
|
|
|
|
// Registered branch entries
|
|
|
|
branch_t branch_entries[MAX_BRANCHES];
|
|
|
|
uint32_t num_branches = 0;
|
2020-12-10 00:06:10 -05:00
|
|
|
|
2020-12-08 16:54:41 -05:00
|
|
|
/*
|
|
|
|
Get an operand for the adjusted stack pointer address
|
|
|
|
*/
|
|
|
|
x86opnd_t
|
|
|
|
ctx_sp_opnd(ctx_t* ctx, int32_t offset_bytes)
|
|
|
|
{
|
2021-02-09 16:24:06 -05:00
|
|
|
int32_t offset = (ctx->sp_offset * sizeof(VALUE)) + offset_bytes;
|
2020-12-08 16:54:41 -05:00
|
|
|
return mem_opnd(64, REG_SP, offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2021-01-20 16:58:09 -05:00
|
|
|
Push one new value on the temp stack
|
2020-12-08 16:54:41 -05:00
|
|
|
Return a pointer to the new stack top
|
|
|
|
*/
|
|
|
|
x86opnd_t
|
2021-01-20 16:58:09 -05:00
|
|
|
ctx_stack_push(ctx_t* ctx, int type)
|
2020-12-08 16:54:41 -05:00
|
|
|
{
|
2021-01-20 16:58:09 -05:00
|
|
|
// Keep track of the type of the value
|
|
|
|
RUBY_ASSERT(type <= RUBY_T_MASK);
|
|
|
|
if (ctx->stack_size < MAX_TEMP_TYPES)
|
|
|
|
ctx->temp_types[ctx->stack_size] = type;
|
|
|
|
|
|
|
|
ctx->stack_size += 1;
|
2021-02-09 16:24:06 -05:00
|
|
|
ctx->sp_offset += 1;
|
2020-12-08 16:54:41 -05:00
|
|
|
|
|
|
|
// SP points just above the topmost value
|
2021-02-09 16:24:06 -05:00
|
|
|
int32_t offset = (ctx->sp_offset - 1) * sizeof(VALUE);
|
2020-12-08 16:54:41 -05:00
|
|
|
return mem_opnd(64, REG_SP, offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Pop N values off the stack
|
|
|
|
Return a pointer to the stack top before the pop operation
|
|
|
|
*/
|
|
|
|
x86opnd_t
|
|
|
|
ctx_stack_pop(ctx_t* ctx, size_t n)
|
|
|
|
{
|
2021-01-20 16:58:09 -05:00
|
|
|
RUBY_ASSERT(n <= ctx->stack_size);
|
|
|
|
|
2020-12-08 16:54:41 -05:00
|
|
|
// SP points just above the topmost value
|
2021-02-09 16:24:06 -05:00
|
|
|
int32_t offset = (ctx->sp_offset - 1) * sizeof(VALUE);
|
2020-12-08 16:54:41 -05:00
|
|
|
x86opnd_t top = mem_opnd(64, REG_SP, offset);
|
|
|
|
|
2021-01-20 16:58:09 -05:00
|
|
|
// Clear the types of the popped values
|
|
|
|
for (size_t i = 0; i < n; ++i)
|
|
|
|
{
|
|
|
|
size_t idx = ctx->stack_size - i - 1;
|
|
|
|
if (idx < MAX_TEMP_TYPES)
|
|
|
|
ctx->temp_types[idx] = T_NONE;
|
|
|
|
}
|
|
|
|
|
2020-12-10 00:06:10 -05:00
|
|
|
ctx->stack_size -= n;
|
2021-02-09 16:24:06 -05:00
|
|
|
ctx->sp_offset -= n;
|
2020-12-08 16:54:41 -05:00
|
|
|
|
|
|
|
return top;
|
|
|
|
}
|
|
|
|
|
2021-01-20 16:58:09 -05:00
|
|
|
/**
|
|
|
|
Get an operand pointing to a slot on the temp stack
|
|
|
|
*/
|
2020-12-08 16:54:41 -05:00
|
|
|
x86opnd_t
|
|
|
|
ctx_stack_opnd(ctx_t* ctx, int32_t idx)
|
|
|
|
{
|
|
|
|
// SP points just above the topmost value
|
2021-02-09 16:24:06 -05:00
|
|
|
int32_t offset = (ctx->sp_offset - 1 - idx) * sizeof(VALUE);
|
2020-12-08 16:54:41 -05:00
|
|
|
x86opnd_t opnd = mem_opnd(64, REG_SP, offset);
|
|
|
|
|
|
|
|
return opnd;
|
|
|
|
}
|
2020-12-10 16:59:13 -05:00
|
|
|
|
2021-01-20 16:58:09 -05:00
|
|
|
/**
|
|
|
|
Get the type of the topmost value on the temp stack
|
|
|
|
Returns T_NONE if unknown
|
|
|
|
*/
|
|
|
|
int
|
|
|
|
ctx_get_top_type(ctx_t* ctx)
|
|
|
|
{
|
2021-01-22 12:22:34 -05:00
|
|
|
RUBY_ASSERT(ctx->stack_size > 0);
|
|
|
|
|
2021-01-20 16:58:09 -05:00
|
|
|
if (ctx->stack_size > MAX_TEMP_TYPES)
|
|
|
|
return T_NONE;
|
|
|
|
|
|
|
|
return ctx->temp_types[ctx->stack_size - 1];
|
|
|
|
}
|
|
|
|
|
2021-01-22 14:57:44 -05:00
|
|
|
/**
|
|
|
|
Compute a difference score for two context objects
|
|
|
|
Returns 0 if the two contexts are the same
|
|
|
|
Returns > 0 if different but compatible
|
2021-01-22 16:54:43 -05:00
|
|
|
Returns INT_MAX if incompatible
|
2021-01-22 14:57:44 -05:00
|
|
|
*/
|
|
|
|
int ctx_diff(const ctx_t* src, const ctx_t* dst)
|
|
|
|
{
|
|
|
|
if (dst->stack_size != src->stack_size)
|
2021-01-22 16:54:43 -05:00
|
|
|
return INT_MAX;
|
2021-01-22 14:57:44 -05:00
|
|
|
|
2021-02-09 16:24:06 -05:00
|
|
|
if (dst->sp_offset != src->sp_offset)
|
|
|
|
return INT_MAX;
|
|
|
|
|
2021-01-22 14:57:44 -05:00
|
|
|
if (dst->self_is_object != src->self_is_object)
|
2021-01-22 16:54:43 -05:00
|
|
|
return INT_MAX;
|
2021-01-22 14:57:44 -05:00
|
|
|
|
|
|
|
// Difference sum
|
|
|
|
int diff = 0;
|
|
|
|
|
|
|
|
// For each temporary variable
|
|
|
|
for (size_t i = 0; i < MAX_TEMP_TYPES; ++i)
|
|
|
|
{
|
|
|
|
int t_src = src->temp_types[i];
|
|
|
|
int t_dst = dst->temp_types[i];
|
|
|
|
|
|
|
|
if (t_dst != t_src)
|
|
|
|
{
|
|
|
|
// It's OK to lose some type information
|
|
|
|
if (t_dst == T_NONE)
|
|
|
|
diff += 1;
|
|
|
|
else
|
2021-01-22 16:54:43 -05:00
|
|
|
return INT_MAX;
|
2021-01-22 14:57:44 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return diff;
|
|
|
|
}
|
|
|
|
|
2021-03-04 12:05:18 -05:00
|
|
|
static rb_ujit_block_array_t
|
|
|
|
get_version_array(const rb_iseq_t *iseq, unsigned idx)
|
2021-02-12 17:12:18 -05:00
|
|
|
{
|
|
|
|
struct rb_iseq_constant_body *body = iseq->body;
|
2021-03-04 12:05:18 -05:00
|
|
|
|
2021-02-12 17:12:18 -05:00
|
|
|
if (rb_darray_size(body->ujit_blocks) == 0) {
|
|
|
|
return NULL;
|
|
|
|
}
|
2021-03-04 12:05:18 -05:00
|
|
|
|
2021-02-12 17:12:18 -05:00
|
|
|
RUBY_ASSERT((unsigned)rb_darray_size(body->ujit_blocks) == body->iseq_size);
|
|
|
|
return rb_darray_get(body->ujit_blocks, idx);
|
|
|
|
}
|
|
|
|
|
2021-03-04 12:05:18 -05:00
|
|
|
// Count the number of block versions matching a given blockid
|
|
|
|
static size_t get_num_versions(blockid_t blockid)
|
|
|
|
{
|
|
|
|
return rb_darray_size(get_version_array(blockid.iseq, blockid.idx));
|
|
|
|
}
|
|
|
|
|
2021-02-19 15:03:12 -05:00
|
|
|
// Keep track of a block version. Block should be fully constructed.
|
2021-02-12 17:12:18 -05:00
|
|
|
static void
|
|
|
|
add_block_version(blockid_t blockid, block_t* block)
|
2021-01-24 23:08:11 -05:00
|
|
|
{
|
2021-02-05 15:49:02 -05:00
|
|
|
// Function entry blocks must have stack size 0
|
|
|
|
RUBY_ASSERT(!(block->blockid.idx == 0 && block->ctx.stack_size > 0));
|
2021-02-12 17:12:18 -05:00
|
|
|
const rb_iseq_t *iseq = block->blockid.iseq;
|
|
|
|
struct rb_iseq_constant_body *body = iseq->body;
|
|
|
|
|
2021-02-16 11:15:29 -05:00
|
|
|
// Ensure ujit_blocks is initialized for this iseq
|
2021-02-12 17:12:18 -05:00
|
|
|
if (rb_darray_size(body->ujit_blocks) == 0) {
|
|
|
|
// Initialize ujit_blocks to be as wide as body->iseq_encoded
|
2021-02-16 21:03:20 -05:00
|
|
|
int32_t casted = (int32_t)body->iseq_size;
|
|
|
|
if ((unsigned)casted != body->iseq_size) {
|
|
|
|
rb_bug("iseq too large");
|
|
|
|
}
|
|
|
|
if (!rb_darray_make(&body->ujit_blocks, casted)) {
|
|
|
|
rb_bug("allocation failed");
|
2021-02-12 17:12:18 -05:00
|
|
|
}
|
2021-02-16 11:15:29 -05:00
|
|
|
|
2021-02-25 15:10:38 -05:00
|
|
|
#if RUBY_DEBUG
|
2021-02-16 11:15:29 -05:00
|
|
|
// First block compiled for this iseq
|
|
|
|
rb_compiled_iseq_count++;
|
2021-02-25 15:10:38 -05:00
|
|
|
#endif
|
2021-02-12 17:12:18 -05:00
|
|
|
}
|
2021-02-05 15:49:02 -05:00
|
|
|
|
2021-02-12 17:12:18 -05:00
|
|
|
block_t *first_version = get_first_version(iseq, blockid.idx);
|
2021-01-24 23:08:11 -05:00
|
|
|
|
2021-02-12 17:12:18 -05:00
|
|
|
// If there exists a version for this block id
|
2021-01-24 23:08:11 -05:00
|
|
|
if (first_version != NULL) {
|
2021-02-12 17:12:18 -05:00
|
|
|
// Link to the next version in a linked list
|
2021-01-24 23:08:11 -05:00
|
|
|
RUBY_ASSERT(block->next == NULL);
|
|
|
|
block->next = first_version;
|
|
|
|
}
|
|
|
|
|
2021-02-12 17:12:18 -05:00
|
|
|
// Make new block the first version
|
|
|
|
rb_darray_set(body->ujit_blocks, blockid.idx, block);
|
2021-01-24 23:08:11 -05:00
|
|
|
RUBY_ASSERT(find_block_version(blockid, &block->ctx) != NULL);
|
2021-02-12 17:12:18 -05:00
|
|
|
|
|
|
|
{
|
|
|
|
// By writing the new block to the iseq, the iseq now
|
|
|
|
// contains new references to Ruby objects. Run write barriers.
|
|
|
|
RB_OBJ_WRITTEN(iseq, Qundef, block->dependencies.iseq);
|
|
|
|
RB_OBJ_WRITTEN(iseq, Qundef, block->dependencies.cc);
|
|
|
|
RB_OBJ_WRITTEN(iseq, Qundef, block->dependencies.cme);
|
2021-02-19 15:03:12 -05:00
|
|
|
|
2021-02-25 15:10:38 -05:00
|
|
|
// Run write barriers for all objects in generated code.
|
2021-02-19 15:03:12 -05:00
|
|
|
uint32_t *offset_element;
|
|
|
|
rb_darray_foreach(block->gc_object_offsets, offset_idx, offset_element) {
|
|
|
|
uint32_t offset_to_value = *offset_element;
|
|
|
|
uint8_t *value_address = cb_get_ptr(cb, offset_to_value);
|
|
|
|
|
|
|
|
VALUE object;
|
|
|
|
memcpy(&object, value_address, SIZEOF_VALUE);
|
|
|
|
RB_OBJ_WRITTEN(iseq, Qundef, object);
|
|
|
|
}
|
2021-02-12 17:12:18 -05:00
|
|
|
}
|
2021-01-24 23:08:11 -05:00
|
|
|
}
|
|
|
|
|
2020-12-16 17:07:18 -05:00
|
|
|
// Retrieve a basic block version for an (iseq, idx) tuple
|
2021-01-14 13:33:19 -05:00
|
|
|
block_t* find_block_version(blockid_t blockid, const ctx_t* ctx)
|
2020-12-16 17:07:18 -05:00
|
|
|
{
|
2021-03-04 12:05:18 -05:00
|
|
|
rb_ujit_block_array_t versions = get_version_array(iseq, block->blockid.idx);
|
2020-12-16 17:07:18 -05:00
|
|
|
|
2021-01-22 16:54:43 -05:00
|
|
|
// Best match found
|
|
|
|
block_t* best_version = NULL;
|
|
|
|
int best_diff = INT_MAX;
|
2021-01-08 15:18:03 -05:00
|
|
|
|
2021-01-22 16:54:43 -05:00
|
|
|
// For each version matching the blockid
|
2021-03-04 12:05:18 -05:00
|
|
|
block_t **element;
|
|
|
|
rb_darray_foreach(versions, idx, element) {
|
|
|
|
block_t *version = *element;
|
|
|
|
int diff = ctx_diff(ctx, version->ctx);
|
|
|
|
|
|
|
|
// Note that we always prefer the first matching
|
|
|
|
// version because of inline-cache chains
|
|
|
|
if (diff < best_diff) {
|
2021-01-22 16:54:43 -05:00
|
|
|
best_version = version;
|
|
|
|
best_diff = diff;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return best_version;
|
2020-12-16 17:07:18 -05:00
|
|
|
}
|
2021-01-27 13:02:55 -05:00
|
|
|
|
2021-02-18 11:54:37 -05:00
|
|
|
void
|
|
|
|
ujit_branches_update_references(void)
|
|
|
|
{
|
|
|
|
for (uint32_t i = 0; i < num_branches; i++) {
|
|
|
|
branch_entries[i].targets[0].iseq = (const void *)rb_gc_location((VALUE)branch_entries[i].targets[0].iseq);
|
|
|
|
branch_entries[i].targets[1].iseq = (const void *)rb_gc_location((VALUE)branch_entries[i].targets[1].iseq);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-08 15:18:03 -05:00
|
|
|
// Compile a new block version immediately
|
2021-03-01 20:43:58 -05:00
|
|
|
block_t* gen_block_version(blockid_t blockid, const ctx_t* start_ctx, rb_execution_context_t* ec)
|
2021-01-08 15:18:03 -05:00
|
|
|
{
|
2021-01-18 17:03:04 -05:00
|
|
|
// Copy the context to avoid mutating it
|
|
|
|
ctx_t ctx_copy = *start_ctx;
|
|
|
|
ctx_t* ctx = &ctx_copy;
|
2021-01-14 13:33:19 -05:00
|
|
|
|
2021-01-18 17:03:04 -05:00
|
|
|
// Allocate a new block version object
|
|
|
|
block_t* first_block = calloc(1, sizeof(block_t));
|
|
|
|
first_block->blockid = blockid;
|
|
|
|
memcpy(&first_block->ctx, ctx, sizeof(ctx_t));
|
2021-01-08 15:18:03 -05:00
|
|
|
|
2021-01-18 17:03:04 -05:00
|
|
|
// Block that is currently being compiled
|
|
|
|
block_t* block = first_block;
|
2021-01-14 13:33:19 -05:00
|
|
|
|
2021-01-18 17:03:04 -05:00
|
|
|
// Generate code for the first block
|
2021-03-01 20:43:58 -05:00
|
|
|
ujit_gen_block(ctx, block, ec);
|
2021-01-08 15:18:03 -05:00
|
|
|
|
|
|
|
// Keep track of the new block version
|
2021-01-25 15:28:49 -05:00
|
|
|
add_block_version(block->blockid, block);
|
2021-01-18 17:03:04 -05:00
|
|
|
|
|
|
|
// For each successor block to compile
|
|
|
|
for (;;) {
|
|
|
|
// If no branches were generated, stop
|
|
|
|
if (num_branches == 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the last branch entry
|
|
|
|
uint32_t branch_idx = num_branches - 1;
|
|
|
|
branch_t* last_branch = &branch_entries[num_branches - 1];
|
|
|
|
|
|
|
|
// If there is no next block to compile, stop
|
|
|
|
if (last_branch->dst_addrs[0] || last_branch->dst_addrs[1]) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (last_branch->targets[0].iseq == NULL) {
|
|
|
|
rb_bug("invalid target for last branch");
|
|
|
|
}
|
|
|
|
|
2021-02-05 15:49:02 -05:00
|
|
|
// Use the context from the branch
|
|
|
|
*ctx = last_branch->target_ctxs[0];
|
|
|
|
|
2021-01-18 17:03:04 -05:00
|
|
|
// Allocate a new block version object
|
|
|
|
block = calloc(1, sizeof(block_t));
|
|
|
|
block->blockid = last_branch->targets[0];
|
|
|
|
memcpy(&block->ctx, ctx, sizeof(ctx_t));
|
|
|
|
|
|
|
|
// Generate code for the current block
|
2021-03-01 20:43:58 -05:00
|
|
|
ujit_gen_block(ctx, block, ec);
|
2021-01-18 17:03:04 -05:00
|
|
|
|
|
|
|
// Keep track of the new block version
|
2021-01-25 15:28:49 -05:00
|
|
|
add_block_version(block->blockid, block);
|
2021-01-18 17:03:04 -05:00
|
|
|
|
|
|
|
// Patch the last branch address
|
|
|
|
last_branch->dst_addrs[0] = cb_get_ptr(cb, block->start_pos);
|
2021-02-19 16:04:23 -05:00
|
|
|
rb_darray_append(&block->incoming, branch_idx);
|
|
|
|
|
2021-01-22 13:29:09 -05:00
|
|
|
RUBY_ASSERT(block->start_pos == last_branch->end_pos);
|
2021-01-18 17:03:04 -05:00
|
|
|
}
|
2021-01-08 15:18:03 -05:00
|
|
|
|
2021-01-18 17:03:04 -05:00
|
|
|
return first_block;
|
2021-01-08 15:18:03 -05:00
|
|
|
}
|
|
|
|
|
2021-01-13 14:14:16 -05:00
|
|
|
// Generate a block version that is an entry point inserted into an iseq
|
2021-03-01 20:43:58 -05:00
|
|
|
uint8_t* gen_entry_point(const rb_iseq_t *iseq, uint32_t insn_idx, rb_execution_context_t *ec)
|
2021-01-13 14:14:16 -05:00
|
|
|
{
|
|
|
|
// The entry context makes no assumptions about types
|
2021-01-18 17:03:04 -05:00
|
|
|
blockid_t blockid = { iseq, insn_idx };
|
2021-01-14 13:33:19 -05:00
|
|
|
|
2021-01-18 17:03:04 -05:00
|
|
|
// Write the interpreter entry prologue
|
|
|
|
uint8_t* code_ptr = ujit_entry_prologue();
|
2021-01-13 14:14:16 -05:00
|
|
|
|
2021-01-18 17:03:04 -05:00
|
|
|
// Try to generate code for the entry block
|
2021-03-01 20:43:58 -05:00
|
|
|
block_t* block = gen_block_version(blockid, &DEFAULT_CTX, ec);
|
2021-01-13 14:14:16 -05:00
|
|
|
|
2021-01-13 15:18:35 -05:00
|
|
|
// If we couldn't generate any code
|
2021-01-18 17:03:04 -05:00
|
|
|
if (block->end_idx == insn_idx)
|
2021-01-13 15:18:35 -05:00
|
|
|
{
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2021-01-13 14:14:16 -05:00
|
|
|
return code_ptr;
|
|
|
|
}
|
|
|
|
|
2020-12-16 21:45:51 -05:00
|
|
|
// Called by the generated code when a branch stub is executed
|
|
|
|
// Triggers compilation of branches and code patching
|
2021-03-01 20:43:58 -05:00
|
|
|
uint8_t* branch_stub_hit(uint32_t branch_idx, uint32_t target_idx, rb_execution_context_t* ec)
|
2020-12-16 21:45:51 -05:00
|
|
|
{
|
2021-01-20 12:44:24 -05:00
|
|
|
uint8_t* dst_addr;
|
|
|
|
|
|
|
|
RB_VM_LOCK_ENTER();
|
|
|
|
|
2021-01-22 13:29:09 -05:00
|
|
|
RUBY_ASSERT(branch_idx < num_branches);
|
|
|
|
RUBY_ASSERT(target_idx < 2);
|
2020-12-17 14:51:56 -05:00
|
|
|
branch_t *branch = &branch_entries[branch_idx];
|
|
|
|
blockid_t target = branch->targets[target_idx];
|
2021-01-27 13:02:55 -05:00
|
|
|
const ctx_t* target_ctx = &branch->target_ctxs[target_idx];
|
2020-12-17 14:51:56 -05:00
|
|
|
|
|
|
|
//fprintf(stderr, "\nstub hit, branch idx: %d, target idx: %d\n", branch_idx, target_idx);
|
2021-01-19 13:28:52 -05:00
|
|
|
//fprintf(stderr, "blockid.iseq=%p, blockid.idx=%d\n", target.iseq, target.idx);
|
2020-12-16 21:45:51 -05:00
|
|
|
|
2021-03-02 12:03:11 -05:00
|
|
|
// Update the PC in the current CFP, because it
|
|
|
|
// may be out of sync in JITted code
|
|
|
|
ec->cfp->pc = iseq_pc_at_idx(target.iseq, target.idx);
|
|
|
|
|
2020-12-16 21:45:51 -05:00
|
|
|
// If either of the target blocks will be placed next
|
2020-12-17 14:51:56 -05:00
|
|
|
if (cb->write_pos == branch->end_pos)
|
2020-12-16 21:45:51 -05:00
|
|
|
{
|
2021-01-07 17:09:25 -05:00
|
|
|
//fprintf(stderr, "target idx %d will be placed next\n", target_idx);
|
2020-12-17 14:51:56 -05:00
|
|
|
branch->shape = (uint8_t)target_idx;
|
2020-12-16 21:45:51 -05:00
|
|
|
|
|
|
|
// Rewrite the branch with the new, potentially more compact shape
|
2020-12-17 14:51:56 -05:00
|
|
|
cb_set_pos(cb, branch->start_pos);
|
|
|
|
branch->gen_fn(cb, branch->dst_addrs[0], branch->dst_addrs[1], branch->shape);
|
2021-01-22 13:29:09 -05:00
|
|
|
RUBY_ASSERT(cb->write_pos <= branch->end_pos);
|
2020-12-16 21:45:51 -05:00
|
|
|
}
|
|
|
|
|
2021-01-27 13:02:55 -05:00
|
|
|
// Limit the number of block versions
|
|
|
|
ctx_t generic_ctx = DEFAULT_CTX;
|
|
|
|
generic_ctx.stack_size = target_ctx->stack_size;
|
2021-02-09 16:24:06 -05:00
|
|
|
generic_ctx.sp_offset = target_ctx->sp_offset;
|
2021-03-04 12:05:18 -05:00
|
|
|
if (get_num_versions(target) >= MAX_VERSIONS - 1)
|
2021-01-27 13:02:55 -05:00
|
|
|
{
|
|
|
|
fprintf(stderr, "version limit hit in branch_stub_hit\n");
|
|
|
|
target_ctx = &generic_ctx;
|
|
|
|
}
|
|
|
|
|
2020-12-16 21:45:51 -05:00
|
|
|
// Try to find a compiled version of this block
|
2021-01-14 13:33:19 -05:00
|
|
|
block_t* p_block = find_block_version(target, target_ctx);
|
2020-12-16 21:45:51 -05:00
|
|
|
|
|
|
|
// If this block hasn't yet been compiled
|
2021-01-14 13:33:19 -05:00
|
|
|
if (!p_block)
|
2020-12-16 21:45:51 -05:00
|
|
|
{
|
2021-03-01 20:43:58 -05:00
|
|
|
p_block = gen_block_version(target, target_ctx, ec);
|
2020-12-16 21:45:51 -05:00
|
|
|
}
|
|
|
|
|
2021-01-13 15:18:35 -05:00
|
|
|
// Add this branch to the list of incoming branches for the target
|
2021-02-19 16:04:23 -05:00
|
|
|
rb_darray_append(&p_block->incoming, branch_idx);
|
2021-01-13 15:18:35 -05:00
|
|
|
|
2021-01-08 15:18:03 -05:00
|
|
|
// Update the branch target address
|
2021-01-20 12:44:24 -05:00
|
|
|
dst_addr = cb_get_ptr(cb, p_block->start_pos);
|
2021-01-12 14:56:43 -05:00
|
|
|
branch->dst_addrs[target_idx] = dst_addr;
|
2020-12-17 14:51:56 -05:00
|
|
|
|
2020-12-16 21:45:51 -05:00
|
|
|
// Rewrite the branch with the new jump target address
|
2021-01-22 13:29:09 -05:00
|
|
|
RUBY_ASSERT(branch->dst_addrs[0] != NULL);
|
2021-01-12 14:56:43 -05:00
|
|
|
uint32_t cur_pos = cb->write_pos;
|
2020-12-17 14:51:56 -05:00
|
|
|
cb_set_pos(cb, branch->start_pos);
|
|
|
|
branch->gen_fn(cb, branch->dst_addrs[0], branch->dst_addrs[1], branch->shape);
|
2021-01-22 13:29:09 -05:00
|
|
|
RUBY_ASSERT(cb->write_pos <= branch->end_pos);
|
2021-01-14 13:33:19 -05:00
|
|
|
branch->end_pos = cb->write_pos;
|
2020-12-16 21:45:51 -05:00
|
|
|
cb_set_pos(cb, cur_pos);
|
|
|
|
|
2021-01-20 12:44:24 -05:00
|
|
|
RB_VM_LOCK_LEAVE();
|
|
|
|
|
2020-12-16 21:45:51 -05:00
|
|
|
// Return a pointer to the compiled block version
|
2021-01-12 14:56:43 -05:00
|
|
|
return dst_addr;
|
2020-12-16 21:45:51 -05:00
|
|
|
}
|
|
|
|
|
2020-12-16 17:07:18 -05:00
|
|
|
// Get a version or stub corresponding to a branch target
|
2021-01-08 15:18:03 -05:00
|
|
|
uint8_t* get_branch_target(
|
|
|
|
blockid_t target,
|
|
|
|
const ctx_t* ctx,
|
|
|
|
uint32_t branch_idx,
|
|
|
|
uint32_t target_idx
|
|
|
|
)
|
2020-12-16 17:07:18 -05:00
|
|
|
{
|
2021-01-19 13:28:52 -05:00
|
|
|
//fprintf(stderr, "get_branch_target, block (%p, %d)\n", target.iseq, target.idx);
|
|
|
|
|
2021-01-14 13:33:19 -05:00
|
|
|
block_t* p_block = find_block_version(target, ctx);
|
2020-12-16 17:07:18 -05:00
|
|
|
|
2021-01-14 13:33:19 -05:00
|
|
|
if (p_block)
|
2021-01-12 14:56:43 -05:00
|
|
|
{
|
2021-01-13 15:18:35 -05:00
|
|
|
// Add an incoming branch for this version
|
2021-02-19 16:04:23 -05:00
|
|
|
rb_darray_append(&p_block->incoming, branch_idx);
|
2021-01-13 15:18:35 -05:00
|
|
|
|
2021-02-19 16:04:23 -05:00
|
|
|
// Return a pointer to the compiled code
|
2021-01-14 13:33:19 -05:00
|
|
|
return cb_get_ptr(cb, p_block->start_pos);
|
2021-01-12 14:56:43 -05:00
|
|
|
}
|
2020-12-16 17:07:18 -05:00
|
|
|
|
|
|
|
// Generate an outlined stub that will call
|
|
|
|
// branch_stub_hit(uint32_t branch_idx, uint32_t target_idx)
|
2020-12-17 14:51:56 -05:00
|
|
|
uint8_t* stub_addr = cb_get_ptr(ocb, ocb->write_pos);
|
|
|
|
|
|
|
|
// Save the ujit registers
|
|
|
|
push(ocb, REG_CFP);
|
|
|
|
push(ocb, REG_EC);
|
|
|
|
push(ocb, REG_SP);
|
|
|
|
push(ocb, REG_SP);
|
|
|
|
|
2021-03-01 20:43:58 -05:00
|
|
|
// Call branch_stub_hit(branch_idx, target_idx, ec)
|
|
|
|
mov(ocb, C_ARG_REGS[2], REG_EC);
|
|
|
|
mov(ocb, C_ARG_REGS[1], imm_opnd(target_idx));
|
|
|
|
mov(ocb, C_ARG_REGS[0], imm_opnd(branch_idx));
|
2020-12-16 21:45:51 -05:00
|
|
|
call_ptr(ocb, REG0, (void *)&branch_stub_hit);
|
2020-12-16 17:07:18 -05:00
|
|
|
|
2020-12-17 14:51:56 -05:00
|
|
|
// Restore the ujit registers
|
|
|
|
pop(ocb, REG_SP);
|
|
|
|
pop(ocb, REG_SP);
|
|
|
|
pop(ocb, REG_EC);
|
|
|
|
pop(ocb, REG_CFP);
|
|
|
|
|
2020-12-16 21:45:51 -05:00
|
|
|
// Jump to the address returned by the
|
|
|
|
// branch_stub_hit call
|
|
|
|
jmp_rm(ocb, RAX);
|
2020-12-16 17:07:18 -05:00
|
|
|
|
|
|
|
return stub_addr;
|
|
|
|
}
|
|
|
|
|
2021-01-08 15:18:03 -05:00
|
|
|
void gen_branch(
|
|
|
|
const ctx_t* src_ctx,
|
|
|
|
blockid_t target0,
|
|
|
|
const ctx_t* ctx0,
|
|
|
|
blockid_t target1,
|
|
|
|
const ctx_t* ctx1,
|
|
|
|
branchgen_fn gen_fn
|
|
|
|
)
|
2020-12-16 17:07:18 -05:00
|
|
|
{
|
2021-01-22 13:29:09 -05:00
|
|
|
RUBY_ASSERT(target0.iseq != NULL);
|
2021-02-09 16:24:06 -05:00
|
|
|
//RUBY_ASSERT(target1.iseq != NULL);
|
2021-01-22 13:29:09 -05:00
|
|
|
RUBY_ASSERT(num_branches < MAX_BRANCHES);
|
2021-01-18 17:03:04 -05:00
|
|
|
uint32_t branch_idx = num_branches++;
|
2021-01-13 15:18:35 -05:00
|
|
|
|
2021-01-19 11:11:11 -05:00
|
|
|
// Get the branch targets or stubs
|
|
|
|
uint8_t* dst_addr0 = get_branch_target(target0, ctx0, branch_idx, 0);
|
2021-02-09 16:24:06 -05:00
|
|
|
uint8_t* dst_addr1 = ctx1? get_branch_target(target1, ctx1, branch_idx, 1):NULL;
|
2021-01-19 11:11:11 -05:00
|
|
|
|
|
|
|
// Call the branch generation function
|
|
|
|
uint32_t start_pos = cb->write_pos;
|
|
|
|
gen_fn(cb, dst_addr0, dst_addr1, SHAPE_DEFAULT);
|
|
|
|
uint32_t end_pos = cb->write_pos;
|
|
|
|
|
|
|
|
// Register this branch entry
|
|
|
|
branch_t branch_entry = {
|
|
|
|
start_pos,
|
|
|
|
end_pos,
|
|
|
|
*src_ctx,
|
|
|
|
{ target0, target1 },
|
2021-02-09 16:24:06 -05:00
|
|
|
{ *ctx0, ctx1? *ctx1:DEFAULT_CTX },
|
2021-01-19 11:11:11 -05:00
|
|
|
{ dst_addr0, dst_addr1 },
|
|
|
|
gen_fn,
|
|
|
|
SHAPE_DEFAULT
|
|
|
|
};
|
|
|
|
|
|
|
|
branch_entries[branch_idx] = branch_entry;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
gen_jump_branch(codeblock_t* cb, uint8_t* target0, uint8_t* target1, uint8_t shape)
|
|
|
|
{
|
|
|
|
switch (shape)
|
|
|
|
{
|
|
|
|
case SHAPE_NEXT0:
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SHAPE_NEXT1:
|
2021-01-22 13:29:09 -05:00
|
|
|
RUBY_ASSERT(false);
|
2021-01-19 11:11:11 -05:00
|
|
|
break;
|
|
|
|
|
|
|
|
case SHAPE_DEFAULT:
|
|
|
|
jmp_ptr(cb, target0);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void gen_direct_jump(
|
|
|
|
const ctx_t* ctx,
|
|
|
|
blockid_t target0
|
|
|
|
)
|
|
|
|
{
|
2021-01-22 13:29:09 -05:00
|
|
|
RUBY_ASSERT(target0.iseq != NULL);
|
|
|
|
RUBY_ASSERT(num_branches < MAX_BRANCHES);
|
2021-01-19 11:11:11 -05:00
|
|
|
uint32_t branch_idx = num_branches++;
|
|
|
|
|
|
|
|
// Branch targets or stub adddress
|
2021-01-13 15:18:35 -05:00
|
|
|
uint8_t* dst_addr0;
|
|
|
|
|
2021-01-18 17:03:04 -05:00
|
|
|
// Shape of the branch
|
|
|
|
uint8_t branch_shape;
|
|
|
|
|
2021-01-19 11:11:11 -05:00
|
|
|
// Branch start and end positions
|
|
|
|
uint32_t start_pos;
|
|
|
|
uint32_t end_pos;
|
|
|
|
|
2021-01-27 13:02:55 -05:00
|
|
|
// Limit the number of block versions
|
|
|
|
ctx_t generic_ctx = DEFAULT_CTX;
|
|
|
|
generic_ctx.stack_size = ctx->stack_size;
|
2021-02-09 16:24:06 -05:00
|
|
|
generic_ctx.sp_offset = ctx->sp_offset;
|
2021-03-04 12:05:18 -05:00
|
|
|
if (get_num_versions(target0) >= MAX_VERSIONS - 1)
|
2021-01-27 13:02:55 -05:00
|
|
|
{
|
2021-02-18 11:54:37 -05:00
|
|
|
fprintf(stderr, "version limit hit in gen_direct_jump\n");
|
2021-01-27 13:02:55 -05:00
|
|
|
ctx = &generic_ctx;
|
|
|
|
}
|
|
|
|
|
2021-01-19 11:11:11 -05:00
|
|
|
block_t* p_block = find_block_version(target0, ctx);
|
|
|
|
|
|
|
|
// If the version already exists
|
|
|
|
if (p_block)
|
2021-01-13 15:18:35 -05:00
|
|
|
{
|
2021-02-19 16:04:23 -05:00
|
|
|
rb_darray_append(&p_block->incoming, branch_idx);
|
2021-01-19 11:11:11 -05:00
|
|
|
dst_addr0 = cb_get_ptr(cb, p_block->start_pos);
|
|
|
|
branch_shape = SHAPE_DEFAULT;
|
|
|
|
|
|
|
|
// Call the branch generation function
|
|
|
|
start_pos = cb->write_pos;
|
|
|
|
gen_jump_branch(cb, dst_addr0, NULL, branch_shape);
|
|
|
|
end_pos = cb->write_pos;
|
2021-01-13 15:18:35 -05:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-01-19 11:11:11 -05:00
|
|
|
// The target block will follow next
|
|
|
|
// It will be compiled in gen_block_version()
|
|
|
|
dst_addr0 = NULL;
|
|
|
|
branch_shape = SHAPE_NEXT0;
|
|
|
|
start_pos = cb->write_pos;
|
|
|
|
end_pos = cb->write_pos;
|
2021-01-13 15:18:35 -05:00
|
|
|
}
|
2020-12-16 17:07:18 -05:00
|
|
|
|
|
|
|
// Register this branch entry
|
|
|
|
branch_t branch_entry = {
|
|
|
|
start_pos,
|
|
|
|
end_pos,
|
2021-01-19 11:11:11 -05:00
|
|
|
*ctx,
|
|
|
|
{ target0, BLOCKID_NULL },
|
|
|
|
{ *ctx, *ctx },
|
|
|
|
{ dst_addr0, NULL },
|
|
|
|
gen_jump_branch,
|
2021-01-18 17:03:04 -05:00
|
|
|
branch_shape
|
2020-12-16 17:07:18 -05:00
|
|
|
};
|
|
|
|
|
2021-01-18 17:03:04 -05:00
|
|
|
branch_entries[branch_idx] = branch_entry;
|
2021-01-12 17:03:54 -05:00
|
|
|
}
|
|
|
|
|
2021-03-03 14:58:42 -05:00
|
|
|
// Create a stub to force the code up to this point to be executed
|
|
|
|
void defer_compilation(
|
|
|
|
block_t* block,
|
|
|
|
ctx_t* cur_ctx,
|
|
|
|
uint32_t insn_idx
|
|
|
|
)
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
RUBY_ASSERT(num_branches < MAX_BRANCHES);
|
|
|
|
uint32_t branch_idx = num_branches++;
|
|
|
|
|
|
|
|
// Register this branch entry
|
|
|
|
branch_t branch_entry = {
|
|
|
|
start_pos,
|
|
|
|
end_pos,
|
|
|
|
*ctx,
|
|
|
|
{ target0, BLOCKID_NULL },
|
|
|
|
{ *ctx, *ctx },
|
|
|
|
{ dst_addr0, NULL },
|
|
|
|
gen_jump_branch,
|
|
|
|
branch_shape
|
|
|
|
};
|
|
|
|
|
|
|
|
branch_entries[branch_idx] = branch_entry;
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-02-12 17:12:18 -05:00
|
|
|
// Remove all references to a block then free it.
|
|
|
|
void
|
|
|
|
ujit_free_block(block_t *block)
|
|
|
|
{
|
|
|
|
ujit_unlink_method_lookup_dependency(block);
|
2021-02-25 15:10:38 -05:00
|
|
|
ujit_block_assumptions_free(block);
|
|
|
|
|
2021-02-19 16:04:23 -05:00
|
|
|
rb_darray_free(block->incoming);
|
2021-02-19 15:49:23 -05:00
|
|
|
rb_darray_free(block->gc_object_offsets);
|
2021-02-25 15:10:38 -05:00
|
|
|
|
|
|
|
free(block);
|
2021-02-12 17:12:18 -05:00
|
|
|
}
|
|
|
|
|
2021-03-04 12:05:18 -05:00
|
|
|
// Remove a block version without reordering the version array
|
|
|
|
static bool
|
|
|
|
block_array_remove(rb_ujit_block_array_t block_array, block_t *block)
|
|
|
|
{
|
|
|
|
block_t **element;
|
|
|
|
bool shifting = false;
|
|
|
|
rb_darray_foreach(block_array, idx, element) {
|
|
|
|
if (*element == block) {
|
|
|
|
shifting = true;
|
|
|
|
}
|
|
|
|
else if (shifting) {
|
|
|
|
rb_darray_set(block_array, idx - 1, *element);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shifting) {
|
|
|
|
rb_darray_pop(block_array);
|
|
|
|
}
|
|
|
|
return shifting;
|
|
|
|
}
|
|
|
|
|
2021-01-12 17:03:54 -05:00
|
|
|
// Invalidate one specific block version
|
2021-01-29 12:07:18 -05:00
|
|
|
void
|
|
|
|
invalidate_block_version(block_t* block)
|
2021-01-12 17:03:54 -05:00
|
|
|
{
|
2021-02-12 17:12:18 -05:00
|
|
|
const rb_iseq_t *iseq = block->blockid.iseq;
|
|
|
|
|
2021-02-17 13:08:53 -05:00
|
|
|
// fprintf(stderr, "invalidating block (%p, %d)\n", block->blockid.iseq, block->blockid.idx);
|
|
|
|
// fprintf(stderr, "block=%p\n", block);
|
2021-01-14 16:58:20 -05:00
|
|
|
|
2021-03-04 12:05:18 -05:00
|
|
|
// Remove this block from the version array
|
|
|
|
rb_ujit_block_array_t versions = get_version_array(iseq, block->blockid.idx);
|
|
|
|
RUBY_ASSERT(rb_darray_size(versions) > 0);
|
|
|
|
RB_UNUSED_VAR(bool removed);
|
|
|
|
removed = block_array_remove(versions, block);
|
|
|
|
RUBY_ASSERT(removed);
|
2021-01-12 17:03:54 -05:00
|
|
|
|
2021-01-14 16:58:20 -05:00
|
|
|
// Get a pointer to the generated code for this block
|
2021-01-14 13:33:19 -05:00
|
|
|
uint8_t* code_ptr = cb_get_ptr(cb, block->start_pos);
|
2021-01-12 17:03:54 -05:00
|
|
|
|
2021-01-14 13:33:19 -05:00
|
|
|
// For each incoming branch
|
2021-02-19 16:04:23 -05:00
|
|
|
uint32_t* branch_idx;
|
|
|
|
rb_darray_foreach(block->incoming, i, branch_idx)
|
2021-01-14 13:33:19 -05:00
|
|
|
{
|
2021-02-19 16:04:23 -05:00
|
|
|
//uint32_t branch_idx = block->incoming[i];
|
|
|
|
branch_t* branch = &branch_entries[*branch_idx];
|
2021-01-14 13:33:19 -05:00
|
|
|
uint32_t target_idx = (branch->dst_addrs[0] == code_ptr)? 0:1;
|
2021-01-19 13:28:52 -05:00
|
|
|
//fprintf(stderr, "branch_idx=%d, target_idx=%d\n", branch_idx, target_idx);
|
|
|
|
//fprintf(stderr, "blockid.iseq=%p, blockid.idx=%d\n", block->blockid.iseq, block->blockid.idx);
|
2021-01-14 13:33:19 -05:00
|
|
|
|
|
|
|
// Create a stub for this branch target
|
|
|
|
branch->dst_addrs[target_idx] = get_branch_target(
|
|
|
|
block->blockid,
|
|
|
|
&block->ctx,
|
2021-02-19 16:04:23 -05:00
|
|
|
*branch_idx,
|
2021-01-14 13:33:19 -05:00
|
|
|
target_idx
|
|
|
|
);
|
|
|
|
|
|
|
|
// Check if the invalidated block immediately follows
|
|
|
|
bool target_next = block->start_pos == branch->end_pos;
|
|
|
|
|
|
|
|
if (target_next)
|
|
|
|
{
|
2021-01-19 13:28:52 -05:00
|
|
|
// The new block will no longer be adjacent
|
2021-01-14 13:33:19 -05:00
|
|
|
branch->shape = SHAPE_DEFAULT;
|
|
|
|
}
|
2021-01-12 17:03:54 -05:00
|
|
|
|
2021-01-14 13:33:19 -05:00
|
|
|
// Rewrite the branch with the new jump target address
|
2021-01-22 13:29:09 -05:00
|
|
|
RUBY_ASSERT(branch->dst_addrs[0] != NULL);
|
2021-01-14 13:33:19 -05:00
|
|
|
uint32_t cur_pos = cb->write_pos;
|
|
|
|
cb_set_pos(cb, branch->start_pos);
|
|
|
|
branch->gen_fn(cb, branch->dst_addrs[0], branch->dst_addrs[1], branch->shape);
|
|
|
|
branch->end_pos = cb->write_pos;
|
|
|
|
cb_set_pos(cb, cur_pos);
|
2021-01-12 17:03:54 -05:00
|
|
|
|
2021-01-14 13:33:19 -05:00
|
|
|
if (target_next && branch->end_pos > block->end_pos)
|
|
|
|
{
|
|
|
|
rb_bug("ujit invalidate rewrote branch past block end");
|
|
|
|
}
|
|
|
|
}
|
2021-01-12 17:03:54 -05:00
|
|
|
|
2021-01-14 13:33:19 -05:00
|
|
|
uint32_t idx = block->blockid.idx;
|
2021-02-12 17:12:18 -05:00
|
|
|
// FIXME: the following says "if", but it's unconditional.
|
|
|
|
// If the block is an entry point, it needs to be unmapped from its iseq
|
2021-03-02 12:03:11 -05:00
|
|
|
VALUE* entry_pc = iseq_pc_at_idx(iseq, idx);
|
2021-01-14 13:33:19 -05:00
|
|
|
int entry_opcode = opcode_at_pc(iseq, entry_pc);
|
2021-01-12 17:03:54 -05:00
|
|
|
|
2021-01-14 13:33:19 -05:00
|
|
|
// TODO: unmap_addr2insn in ujit_iface.c? Maybe we can write a function to encompass this logic?
|
|
|
|
// Should check how it's used in exit and side-exit
|
|
|
|
const void * const *handler_table = rb_vm_get_insns_address_table();
|
|
|
|
void* handler_addr = (void*)handler_table[entry_opcode];
|
|
|
|
iseq->body->iseq_encoded[idx] = (VALUE)handler_addr;
|
2021-01-12 17:03:54 -05:00
|
|
|
|
2021-01-14 13:33:19 -05:00
|
|
|
// TODO:
|
2021-02-11 15:27:33 -05:00
|
|
|
// May want to recompile a new entry point (for interpreter entry blocks)
|
|
|
|
// This isn't necessary for correctness
|
|
|
|
|
|
|
|
// FIXME:
|
2021-01-14 13:33:19 -05:00
|
|
|
// Call continuation addresses on the stack can also be atomically replaced by jumps going to the stub.
|
2021-01-12 17:03:54 -05:00
|
|
|
|
2021-02-12 17:12:18 -05:00
|
|
|
ujit_free_block(block);
|
2021-01-19 13:28:52 -05:00
|
|
|
|
2021-02-17 13:08:53 -05:00
|
|
|
// fprintf(stderr, "invalidation done\n");
|
2020-12-16 17:07:18 -05:00
|
|
|
}
|
|
|
|
|
2020-12-10 16:59:13 -05:00
|
|
|
void
|
|
|
|
ujit_init_core(void)
|
|
|
|
{
|
2021-02-12 17:12:18 -05:00
|
|
|
// Nothing yet
|
2020-12-10 16:59:13 -05:00
|
|
|
}
|