blob: b176390d8a36a062fa946cd94dcb50f03b02745d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @author Alexander Astapchuk
*/
/**
* @file
* @brief Contains platform-independent routines for runtime support.
*/
#include "open/vm_method_access.h"
#include "compiler.h"
#include "trace.h"
#include "jet.h"
#include "jit_import_rt.h"
#include "open/vm_ee.h"
#include "port_threadunsafe.h"
#include "EMInterface.h"
#if !defined(_IPF_)
#include "enc_ia32.h"
#endif
namespace Jitrino {
namespace Jet {
bool rt_check_method(JIT_Handle jit, Method_Handle method)
{
char * pinfo = (char*)method_get_info_block_jit(method, jit);
return MethodInfoBlock::is_valid_data(pinfo);
}
static JitFrameContext* dummyCTX = NULL;
static POINTER_SIZE_INT sp_off = (char*)devirt(sp, dummyCTX) - (char*)dummyCTX;
static POINTER_SIZE_INT ip_off = (char*)devirt(gr_x, dummyCTX) - (char*)dummyCTX;
static const POINTER_SIZE_INT bp_off = (char*)devirt(bp, dummyCTX) - (char*)dummyCTX;
static const unsigned bp_idx = ar_idx(bp);
static const unsigned bp_bytes = word_no(bp_idx)*WORD_SIZE/CHAR_BIT;
static const unsigned bp_mask = 1<<bit_no(bp_idx);
static const int bp_spill_off = ((StackFrame*)NULL)->spill(bp);
#ifdef _IA32_
//
static AR ebx = virt(RegName_EBX);
static const unsigned ebx_idx = ar_idx(ebx);
static const unsigned ebx_bytes = word_no(ebx_idx)*WORD_SIZE/CHAR_BIT;
static const unsigned ebx_mask = 1<<bit_no(ebx_idx);
static const int ebx_spill_off = ((StackFrame*)NULL)->spill(ebx);
//
static AR esi = virt(RegName_ESI);
static const unsigned esi_idx = ar_idx(esi);
static const unsigned esi_bytes = word_no(esi_idx)*WORD_SIZE/CHAR_BIT;
static const unsigned esi_mask = 1<<bit_no(esi_idx);
static const int esi_spill_off = ((StackFrame*)NULL)->spill(esi);
//
static AR edi = virt(RegName_EDI);
static const unsigned edi_idx = ar_idx(edi);
static const unsigned edi_bytes = word_no(edi_idx)*WORD_SIZE/CHAR_BIT;
static const unsigned edi_mask = 1<<bit_no(edi_idx);
static const int edi_spill_off = ((StackFrame*)NULL)->spill(edi);
//
#endif // _IA32_
/**
* @brief Prints out a message to identify a program location.
*
* The message includes method name and class, IP and PC of the location.
* Message is preceded with the specified \c name.
* The function uses #dbg and does not print out new-line character.
*/
static void rt_trace(const char * name, Method_Handle meth,
const MethodInfoBlock& infoBlock,
const JitFrameContext * context)
{
void *** pip = devirt(gr_x, context);
char * ip = (char*)**pip;
char * where = ip;
if (context->is_ip_past) {
--where;
}
dbg_rt("%s @ %s::%s @ %p/%u: ",
name,
class_get_name(method_get_class(meth)), method_get_name(meth),
ip, infoBlock.get_pc(where));
}
void rt_unwind(JIT_Handle jit, Method_Handle method,
JitFrameContext * context)
{
char * pinfo = (char*)method_get_info_block_jit(method, jit);
assert(MethodInfoBlock::is_valid_data(pinfo));
MethodInfoBlock infoBlock(pinfo);
StackFrame sframe(infoBlock.get_num_locals(),
infoBlock.get_stack_max(),
infoBlock.get_in_slots());
JitFrameContext saveContextForLogs;
if (infoBlock.get_flags() & DBG_TRACE_RT) {
saveContextForLogs= *context;
}
//void ** psp = (void**)devirt(sp, context);
void ** psp = (void**)((char*)context + sp_off);
char * sp_val = (char*)*psp;
// here, gr_x means 'IP'
//void *** pip = devirt(gr_x, context);
void *** pip = (void***)((char*)context + ip_off);
char * where = (char*)**pip;
if (context->is_ip_past) {
--where;
}
// A special processing - mostly for StackOverflowError:
// if something terrible happens during the stack preparation sequence,
// then the registers are not saved yet, so we can't restore them from
// the JitFrameContext. The good news is that the callee-save registers
// are also untouched yet, so we only need to restore SP and IP
char * meth_start = infoBlock.get_code_start();
unsigned whereLen = (unsigned)(where - meth_start);
if (whereLen<infoBlock.get_warmup_len()) {
*psp = sp_val + sframe.size();
// Now, [sp] = retIP
sp_val = (char*)*psp;
*pip = (void**)sp_val;
sp_val += STACK_SLOT_SIZE; // pop out the retIP
*psp = sp_val;
return;
}
//void *** pbp = devirt(bp, context);
void *** pbp = (void***)((char*)context + bp_off);
char * bp_val = (char*)(**pbp);
assert(!(infoBlock.get_flags() & JMF_SP_FRAME)); // not yet
// restore sp
sp_val = bp_val;
// ^^^ now, sp has the same value as it was on method's entrance (points
// to retIP)
*pip = (void**)sp_val;
//
// restore callee-save regs
//
#ifdef _DEBUG
UNSAFE_REGION_START
// presumption: only GP registers can be callee-save
static bool do_check = true;
for (unsigned i=0; do_check && i<ar_num; i++) {
AR ar = _ar(i);
assert(!is_callee_save(ar) || is_gr(ar));
}
do_check = false;
UNSAFE_REGION_END
#endif
#if defined(_EM64T_) || defined(_IPF_)
// Common version for all platforms but IA32
for (unsigned i=0; i<gr_num; i++) {
AR ar = _gr(i);
if (infoBlock.is_saved(ar)) {
void *** preg = devirt(ar, context);
assert(preg && *preg);
*preg = (void**)(sp_val+sframe.spill(ar));
if (infoBlock.get_flags() & DBG_TRACE_RT) {
dbg_rt("\trt.unwind.%s.%s: [%p]=%p\n",
Encoder::to_str(ar, false).c_str(),
Encoder::to_str(ar, true).c_str(), *preg, **preg);
}
}
}
#else
//
// Highly optimized version for IA32 - the loop of 4 callee-save
// register is unrolled, every constant value is precomputed and
// cached.
//
unsigned map_off, mask;
int spill_off;
//
const char * map = infoBlock.saved_map();
//
//
map_off = bp_bytes; mask = bp_mask; spill_off = bp_spill_off;
// bp is always saved
assert(*(unsigned*)(map+map_off) & mask);
{
context->p_ebp = (unsigned*)(sp_val + spill_off);
}
//
//
map_off = ebx_bytes; mask = ebx_mask; spill_off = ebx_spill_off;
if (*(unsigned*)(map+map_off) & mask) {
context->p_ebx = (unsigned*)(sp_val + spill_off);
}
//
//
map_off = esi_bytes; mask = esi_mask; spill_off = esi_spill_off;
if (*(unsigned*)(map+map_off) & mask) {
context->p_esi = (unsigned*)(sp_val + spill_off);
}
//
//
map_off = edi_bytes; mask = edi_mask; spill_off = edi_spill_off;
if (*(unsigned*)(map+map_off) & mask) {
context->p_edi = (unsigned*)(sp_val + spill_off);
}
#endif
// When needed, restore the whole context including scratch registers
// (normally, under JVMTI for PopFrame functionality)
if (infoBlock.get_compile_params().exe_restore_context_after_unwind) {
for (unsigned i=0; i<ar_num; i++) {
AR ar = _ar(i);
if (is_callee_save(ar) || ar==sp) {
continue;
}
void *** preg = devirt(ar, context);
//FIXME: JVMTI-popframe // assert(preg && *preg);
// ^^ temporarily disabling the assert, will need to uncomment
// when VM also supports all the registers to restore
// Currently, XMM regs are not restored as there are no proper
// fields in JitFrameContext
if (preg && *preg) {
*preg = (void**)(sp_val+sframe.jvmti_register_spill_offset(ar));
if (infoBlock.get_flags() & DBG_TRACE_RT) {
dbg_rt("\trt.JVMTI.unwind.%s.%s: [%p]=%p\n",
Encoder::to_str(ar, false).c_str(),
Encoder::to_str(ar, true).c_str(), *preg, **preg);
}
} // ~if (*preg)
}
}
sp_val += STACK_SLOT_SIZE; // pop out retIP
*psp = sp_val;
if (infoBlock.get_flags() & DBG_TRACE_RT) {
void *** pip = devirt(gr_x, context);
rt_trace("rt.unwind", method, infoBlock, &saveContextForLogs);
dbg_rt("\t=>=> %p; SP=%p\n", **pip, sp_val);
}
}
void rt_enum(JIT_Handle jit, Method_Handle method,
GC_Enumeration_Handle henum, JitFrameContext * context)
{
if (!context->is_ip_past && !StaticConsts::g_jvmtiMode) {
// The IP points directly to the instructions - this must be a
// hardware NPE happened.
// A special case is SOE, which is allowed to happen only at the method start.
// Check the presumptions:
assert(method_get_exc_handler_number(method) == 0 || rt_is_soe_area(jit, method, context));
#ifdef _DEBUG
bool sync = method_is_synchronized(method);
bool inst = !method_is_static(method);
assert(!(sync && inst));
#endif
// Nothing to report here - the method is about to exit.
return;
}
char * pinfo = (char*)method_get_info_block_jit(method, jit);
assert(MethodInfoBlock::is_valid_data(pinfo));
MethodInfoBlock infoBlock(pinfo);
assert(!(infoBlock.get_flags() & JMF_SP_FRAME)); // not yet
void *** pip = (void***)((char*)context + ip_off);
char * where = (char*)**pip;
char * meth_start = infoBlock.get_code_start();
unsigned whereLen = (unsigned)(where - meth_start);
if (whereLen<infoBlock.get_warmup_len()) {
return;
}
if (DBG_TRACE_RT & infoBlock.get_flags()) {
rt_trace("rt.enum.start", method, infoBlock, context);
}
StackFrame frame(infoBlock.get_num_locals(), infoBlock.get_stack_max(),
infoBlock.get_in_slots() );
char * ebp = (char*)**devirt(bp, context);
//
//
//
unsigned * map = (unsigned*)(ebp + frame.info_gc_locals());
for (unsigned i=0; i<infoBlock.get_num_locals(); i++) {
if (!tst(map, i)) {
continue;
}
void **p_obj = (void**)(ebp + frame.local(i));
if (DBG_TRACE_RT & infoBlock.get_flags()) {
//rt_trace("gc.item", method, infoBlock, context);
dbg_rt("\tlocal#%d=>%p (%p)\n", i, p_obj, *p_obj);
}
vm_enumerate_root_reference(p_obj, FALSE);
}
//
// Report input args with objects in it
//
map = (unsigned*)(ebp + frame.info_gc_args());
for (unsigned i=0; i<infoBlock.get_in_slots(); i++) {
if (!tst(map, i)) {
continue;
}
void **p_obj = (void**)(ebp + frame.inargs()+i*STACK_SLOT_SIZE);
if (DBG_TRACE_RT & infoBlock.get_flags()) {
dbg_rt("\tin_arg#%d=>%p (%p)\n", i, p_obj, *p_obj);
}
vm_enumerate_root_reference(p_obj, FALSE);
}
//
//
//
map = (unsigned*)(ebp + frame.info_gc_regs());
unsigned obj_regs = *map;
// only one word is allocated to store a GC map for registers
assert(gr_num < WORD_SIZE);
for (unsigned i=0; i<WORD_SIZE; i++) {
if (obj_regs & 1) {
AR ar = _ar(i);
assert(is_gr(ar) && is_callee_save(ar));
void ** p_obj;
p_obj = *devirt(ar, context);
if (DBG_TRACE_RT & infoBlock.get_flags()) {
//rt_trace("gc.item", method, infoBlock, context);
dbg_rt("\treg#%s#%s=>%p (%p)\n",
Encoder::to_str(ar, false).c_str(),
Encoder::to_str(ar, true).c_str(),
p_obj, *p_obj);
}
vm_enumerate_root_reference(p_obj, FALSE);
}
obj_regs >>= 1;
}
map = (unsigned*)(ebp + frame.info_gc_stack());
unsigned rt_stack_depth = *(unsigned*)(ebp+frame.info_gc_stack_depth());
for (unsigned i=0; i<rt_stack_depth; i++) {
if (!tst(map, i)) {
continue;
}
void ** p_obj;
p_obj = (void**)(ebp + frame.stack_slot(i));
if (DBG_TRACE_RT & infoBlock.get_flags()) {
//rt_trace("gc.item", method, infoBlock, context);
dbg_rt("\tstack#%d=>%p (%p)\n", i, p_obj, *p_obj);
}
vm_enumerate_root_reference(p_obj, FALSE);
}
if (infoBlock.get_flags() & JMF_REPORT_THIS) {
void ** p_obj = (void**)rt_get_address_of_this(jit, method, context);
if (DBG_TRACE_RT & infoBlock.get_flags()) {
//rt_trace("gc.item", method, infoBlock, context);
dbg_rt("\tin.this#=>%p (%p)\n", p_obj, *p_obj);
}
vm_enumerate_root_reference(p_obj, FALSE);
}
if (DBG_TRACE_RT & infoBlock.get_flags()) {
rt_trace("rt.enum.done", method, infoBlock, context);
}
}
void rt_fix_handler_context(JIT_Handle jit, Method_Handle method,
JitFrameContext * context)
{
char * pinfo = (char*)method_get_info_block_jit(method, jit);
assert(MethodInfoBlock::is_valid_data(pinfo));
//
MethodInfoBlock infoBlock(pinfo);
assert(!(infoBlock.get_flags() & JMF_SP_FRAME)); // not yet
void *** pip = (void***)((char*)context + ip_off);
char * where = (char*)**pip;
char * meth_start = infoBlock.get_code_start();
#ifdef _DEBUG
unsigned meth_len = infoBlock.get_code_len();
assert(meth_start <= where);
assert(where < meth_start + meth_len);
#endif
unsigned whereLen = (unsigned)(where - meth_start);
if (whereLen<infoBlock.get_warmup_len()) {
return;
}
StackFrame sframe(infoBlock.get_num_locals(),
infoBlock.get_stack_max(),
infoBlock.get_in_slots());
unsigned frameSize = sframe.size();
void ** psp = (void**)devirt(sp, context);
void *** pbp = devirt(bp, context);
char * bp_val = (char*)(**pbp);
char * sp_val = bp_val - frameSize;
if (infoBlock.get_flags() & DBG_TRACE_RT) {
rt_trace("rt.fix_handler", method, infoBlock, context);
dbg_rt("oldESP=%p ; newESP=%p\n", *psp, sp_val);
}
*psp = sp_val;
}
Boolean rt_is_soe_area(JIT_Handle jit, Method_Handle method, const JitFrameContext * context) {
char * pinfo = (char*)method_get_info_block_jit(method, jit);
assert(MethodInfoBlock::is_valid_data(pinfo));
MethodInfoBlock infoBlock(pinfo);
assert(!(infoBlock.get_flags() & JMF_SP_FRAME)); // not yet
void *** pip = (void***)((char*)context + ip_off);
char * where = (char*)**pip;
char * meth_start = infoBlock.get_code_start();
unsigned whereLen = (unsigned)(where - meth_start);
if (whereLen<infoBlock.get_warmup_len()) {
return 1;
}
return 0;
}
void * rt_get_address_of_this(JIT_Handle jit, Method_Handle method,
const JitFrameContext * context)
{
char * pinfo = (char*)method_get_info_block_jit(method, jit);
assert(MethodInfoBlock::is_valid_data(pinfo));
//
MethodInfoBlock infoBlock(pinfo);
assert(!(infoBlock.get_flags() & JMF_SP_FRAME)); // not yet
void *** pip = (void***)((char*)context + ip_off);
char * where = (char*)**pip;
char * meth_start = infoBlock.get_code_start();
unsigned whereLen = (unsigned)(where - meth_start);
if (whereLen<infoBlock.get_warmup_len()) {
return NULL;
}
// We did not store 'this' specially
if (!(infoBlock.get_flags() & JMF_REPORT_THIS)) {
return NULL;
}
StackFrame stackframe(infoBlock.get_num_locals(),
infoBlock.get_stack_max(),
infoBlock.get_in_slots());
void *** pbp = devirt(bp, context);
char * bp_val = (char*)(**pbp);
void ** p_obj = (void**)(bp_val + stackframe.thiz());
if (infoBlock.get_flags() & DBG_TRACE_RT) {
void ** psp = (void**)devirt(sp, context);
char * sp_val = (char*)*psp;
rt_trace("rt.get_thiz", method, infoBlock, context);
dbg_rt("p_thiz=%p, thiz=%p (sp=%p)\n", p_obj, p_obj ? *p_obj : NULL, sp_val);
}
return p_obj;
}
void rt_bc2native(JIT_Handle jit, Method_Handle method, unsigned short bc_pc,
void ** pip)
{
char * pinfo = (char*)method_get_info_block_jit(method, jit);
assert(MethodInfoBlock::is_valid_data(pinfo));
MethodInfoBlock rtinfo(pinfo);
assert(bc_pc < rtinfo.get_bc_size());
*pip = (void*)rtinfo.get_ip(bc_pc);
if (rtinfo.get_flags() & DBG_TRACE_RT) {
dbg_rt("rt.bc2ip: @ %u => %p\n", bc_pc, *pip);
}
}
void rt_native2bc(JIT_Handle jit, Method_Handle method, const void * ip,
unsigned short * pbc_pc)
{
char * pinfo = (char*)method_get_info_block_jit(method, jit);
assert(MethodInfoBlock::is_valid_data(pinfo));
MethodInfoBlock rtinfo(pinfo);
#ifdef _DEBUG
char * where = (char*)ip;
char * meth_start = rtinfo.get_code_start();
unsigned meth_len = rtinfo.get_code_len();
assert(meth_start <= where);
assert(where < meth_start + meth_len);
#endif
*pbc_pc = (unsigned short)rtinfo.get_pc((char*)ip);
if (rtinfo.get_flags() & DBG_TRACE_RT) {
dbg_rt("rt.ip2bc: @ 0x%p => %d\n", ip, *pbc_pc);
}
}
::OpenExeJpdaError rt_get_local_var(JIT_Handle jit, Method_Handle method,
const ::JitFrameContext *context,
unsigned var_num, VM_Data_Type var_type,
void *value_ptr)
{
//
//FIXME:
// Current implementation will work incorrectly with local variables
// cached on registers and with variables that reuse input args slots.
//
// Need to store regs=>locals into MethodInfo block and also
// locals=>input args (or invent a simple algorithm of how to detect
// this mapping during runtime).
//
char * pinfo = (char*)method_get_info_block_jit(method, jit);
assert(MethodInfoBlock::is_valid_data(pinfo));
MethodInfoBlock infoBlock(pinfo);
if (var_num >= infoBlock.get_num_locals()) {
return EXE_ERROR_INVALID_SLOT;
}
StackFrame frame(infoBlock.get_num_locals(),
infoBlock.get_stack_max(),
infoBlock.get_in_slots());
char * ebp = (char*)**devirt(bp, context);
unsigned * map = (unsigned*)(ebp + frame.info_gc_locals());
uint64* var_ptr_to_64;
U_32* var_ptr_to32;
switch(var_type) {
case VM_DATA_TYPE_INT64:
case VM_DATA_TYPE_UINT64:
case VM_DATA_TYPE_F8:
var_ptr_to_64 = (uint64*)value_ptr;
*var_ptr_to_64 = *(uint64*)(ebp + frame.local(var_num));
break;
case VM_DATA_TYPE_ARRAY:
case VM_DATA_TYPE_CLASS:
if (!tst(map, var_num)) {
return EXE_ERROR_TYPE_MISMATCH;
}
#ifdef _EM64T_
case VM_DATA_TYPE_STRING:
var_ptr_to_64 = (uint64*)value_ptr;
*var_ptr_to_64 = *(uint64*)(ebp + frame.local(var_num));
break;
#endif
default:
var_ptr_to32 = (U_32*)value_ptr;
*var_ptr_to32 = *(U_32*)(ebp + frame.local(var_num));
}
return EXE_ERROR_NONE;
}
::OpenExeJpdaError rt_set_local_var(JIT_Handle jit, Method_Handle method,
const ::JitFrameContext *context,
unsigned var_num, VM_Data_Type var_type,
void *value_ptr)
{
char * pinfo = (char*)method_get_info_block_jit(method, jit);
assert(MethodInfoBlock::is_valid_data(pinfo));
MethodInfoBlock infoBlock(pinfo);
if (var_num >= infoBlock.get_num_locals()) {
return EXE_ERROR_INVALID_SLOT;
}
StackFrame frame(infoBlock.get_num_locals(),
infoBlock.get_stack_max(),
infoBlock.get_in_slots());
char * ebp = (char*)**devirt(bp, context);
uint64* var_ptr_to_64;
U_32* var_ptr_to32;
switch(var_type) {
case VM_DATA_TYPE_INT64:
case VM_DATA_TYPE_UINT64:
case VM_DATA_TYPE_F8:
#ifdef _EM64T_
case VM_DATA_TYPE_ARRAY:
case VM_DATA_TYPE_CLASS:
case VM_DATA_TYPE_STRING:
#endif
var_ptr_to_64 = (uint64*)(ebp + frame.local(var_num));
*var_ptr_to_64 = *(uint64*)value_ptr;
break;
default:
var_ptr_to32 = (U_32*)(ebp + frame.local(var_num));
*var_ptr_to32 = *(U_32*)value_ptr;
}
return EXE_ERROR_NONE;
}
void rt_profile_notification_callback(JIT_Handle jit, PC_Handle pch, Method_Handle mh)
{
JITInstanceContext* jitContext = JITInstanceContext::getContextForJIT(jit);
//heck that profiler type is EB, counters are patched only for Entry Backage profile
if ((jitContext->getProfilingInterface())->getProfileType(pch) == EM_PCTYPE_ENTRY_BACKEDGE) {
//Get MethodInfoBlock of the method to be patched
char * pinfo = (char*)method_get_info_block_jit(mh, jit);
assert(MethodInfoBlock::is_valid_data(pinfo));
MethodInfoBlock infoBlock(pinfo);
if (infoBlock.get_flags() & DBG_TRACE_RT) {
const char* methodName = method_get_name(mh);
const char* className = class_get_name(method_get_class(mh));
dbg_rt("rt.patch_eb_counters: patching method %s.%s\n", className, methodName);
}
//Replace counters with nops in 3 steps:
//1. Atomically replace first 2 bytes of counter instruction with jump to the
//next instruction
//2. Replace all the remaining bytes of counter instruction with nops
//3. Atomically replace jump with 2 nops
U_8* methodAddr = method_get_code_block_jit(mh, jit);
for (U_32 i = 0 ; i<infoBlock.num_profiler_counters; i++) {
U_32 offsetInfo = infoBlock.profiler_counters_map[i];
U_32 codeOffset = ProfileCounterInfo::getInstOffset(offsetInfo);
U_32 patchedSize = ProfileCounterInfo::getInstSize(offsetInfo);
U_8* patchedAddr = methodAddr + codeOffset;
//1. Generate jump to the next instruction
char* jmpIP = (char*)patchedAddr;
char jmpOffset = (char)(patchedSize - 2);
if (((POINTER_SIZE_INT)jmpIP)&0x1) {
jmpIP++;
jmpOffset--;
}
//to guarantee that first replacing will be atomic we have to
//manually write jmp instruction instead of using encoder
*(uint16*)jmpIP = (uint16)((0xeb) | jmpOffset << 8);
assert(((uint16)(int_ptr)(jmpIP) & 0x01)==0);
//2. Put nops instead of inst, 1 byte nops are used here for asm
//readability
char* ip = jmpIP + 2;
for (int i=0; i<jmpOffset; i++) {
EncoderBase::nops(ip+i, 1);
}
//3. Atomically replace jump with nop;
*(uint16*)jmpIP = (uint16)0x9090; //2 nops atomically.
}
infoBlock.release();
}
}
}}; // ~namespace Jitrino::Jet