From: Evgeniy Temirgaleev via Tarantool-patches <tarantool-patches@dev.tarantool.org>
To: "Sergey Kaplun" <skaplun@tarantool.org>
Cc: tarantool-patches@dev.tarantool.org
Subject: Re: [Tarantool-patches] [PATCH luajit 1/3] dbg: introduce lj-ir, lj-jslots, lj-trace dumpers
Date: Sun, 28 Jun 2026 04:03:30 +0300 [thread overview]
Message-ID: <1782608610.240669306@f725.i.mail.ru> (raw)
In-Reply-To: <20260625202903.3157425-2-skaplun@tarantool.org>
[-- Attachment #1: Type: text/plain, Size: 46392 bytes --]
Hi, Sergey!
Thanks for the patch. Please, see my comments.
--
Best regards,
Evgeniy Temirgaleev
>
> От кого: Sergey Kaplun <skaplun@tarantool.org>
> Кому: Sergey Bronnikov <sergeyb@tarantool.org>, Evgeniy Temirgaleev <e.temirgaleev@tarantool.org
> >
> Копия: tarantool-patches@dev.tarantool.org, Sergey Kaplun <skaplun@tarantool.org
> >
> Дата: Четверг, 25 июня 2026, 23:29 +03:00
> This patch adds dumpers for a single IR instruction (`lj-ir`), as well
> as for all bytecodes inside one trace (`lj-trace`). Its dump is quite
> similar to the -jdump flag but also reports types of register operands
> (`ref`, `lit`, `cst`) and operation mode (`N`, `A`, `W`, etc.).
> The `lj-trace` command accepts optional /rs flags to dump registers
> associated with IR and snapshots for the trace correspondingly.
> The `lj-ir` command can be used for dumping IR constants as well.
> The `lj-jslots` command dumps the content of `J->slot`. It is useful to
> simplify debugging of `rec_check_slots()` assertion failures.
>
> For LLDB value, the `__getitem__` metamethod now accepts bool keys.
> Also, `__index__` is set to allow lldb.value to be used as an index
> without explicit conversion to int. Old GDB versions (below 7.12) are
> not supported because of the gdb.Value lacks the `__index__` metamethod
> and can't be monkey-patched. The support for these versions may be added
> by demand.
>
> Part of tarantool/tarantool#4808
> ---
> src/luajit_dbg.py | 1216 ++++++++++++++++-
> .../debug-extension-tests.py | 365 +++++
> 2 files changed, 1570 insertions(+), 11 deletions(-)
>
> diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py
> index 2edb199a..fd6ca8a5 100644
> --- a/src/luajit_dbg.py
> +++ b/src/luajit_dbg.py
> @@ -58,6 +58,26 @@ class Debugger(object):
> self.LLDB = True
> return super(Debugger, self).__new__(_LLDBDebugger)
>
> + def parse_flags(self, raw_flags, permitted_flags):
> + flags = {}
> + for flag in raw_flags:
> + if flag not in permitted_flags:
> + raise self.error('Unrecongnized option: "{}"'.format(flag))
> + flags[flag] = True
> + return flags
> +
> + def extract_flags(self, arg, permitted_flags):
> + if not arg:
> + return None, None
> + flags = {}
> + if arg.startswith('/'):
> + match = re.match(r'/(\S*)\s+(.*)$', arg)
> + if not match:
> + return arg, flags
> + raw_flags, arg = match.group(1, 2)
> + flags = self.parse_flags(raw_flags, permitted_flags)
> + return arg, flags
> +
> def configure(self):
> global PADDING, LJ_TISNUM
> if not self.check_libluajit():
> @@ -70,6 +90,17 @@ class Debugger(object):
> self.write('luajit_dbg.py failed to load: '
> 'no debugging symbols found for libluajit\n')
> return False
> +
> + # Setup arch.
> + try:
> + self.arch = str(self.eval('LJ_ARCH_NAME')).split('"')[1]
> + except Exception:
> + try:
> + self.arch = self.detect_arch()
> + except Exception:
> + # Setup on demand if necessary.
> + pass
> +
> return True
>
> def initialize_extension(self, commands):
> @@ -99,21 +130,42 @@ class Debugger(object):
> '''Return the content of the string by the given pointer.'''
> pass
>
> + @abc.abstractmethod
> + def address(self, obj):
> + '''Return the address in memory of the given object.'''
> + pass
> +
> @abc.abstractmethod
> def lookup_global(self, symbol):
> '''Look up the global C symbol by the given name.'''
> pass
>
> + @abc.abstractmethod
> + def member_by_offset(self, typename, offset, prev_name=None):
> + '''Look up the global C symbol by the given name.'''
> + pass
> +
> @abc.abstractmethod
> def eval(self, command):
> '''Parse and evaluate the given debugger command.'''
> pass
>
> + @abc.abstractmethod
> + def detect_arch(self):
> + '''Detect the CPU architecture and canonicalize it to the LuaJIT
> + notation.'''
> + pass
> +
> @abc.abstractmethod
> def write(self, msg):
> '''Print the message.'''
> pass
>
> + @abc.abstractmethod
> + def error(self, msg):
> + '''Create the error object with message.'''
> + pass
> +
> @abc.abstractmethod
> def check_libluajit(self):
> '''Check that libluajit is loaded.
> @@ -172,10 +224,50 @@ class _GDBDebugger(Debugger):
> # A string is printed with a pointer to it. Just strip it.
> return re.sub(r'^0x[a-f0-9]+\s+(?=")', '', str(strptr))
>
> + def address(self, obj):
> + return obj.address
> +
> def lookup_global(self, symbol):
> variable, _ = gdb.lookup_symbol(symbol)
> return variable.value() if variable else None
>
> + def member_by_offset(self, tp, offset, prev_name=None):
> + if isinstance(tp, str):
> + tp = self._dbgtype(tp)
> + assert offset < tp.sizeof, 'offset is bigger than object size'
> + if tp.code == gdb.TYPE_CODE_TYPEDEF:
> + tp = tp.strip_typedefs()
> + if tp.code == gdb.TYPE_CODE_STRUCT:
> + fields = tp.fields()
> + for n_field in range(len(fields)):
> + islast = n_field == (len(fields) - 1)
> + field = fields[n_field]
> + start_field = field.bitpos / 8
> + end_field = fields[n_field + 1].bitpos / 8 if not islast \
> + else tp.sizeof
> + if start_field <= offset and offset < end_field:
> + next_name = self.member_by_offset(
> + field.type,
> + offset - start_field,
> + prev_name=field.name
> + )
> + return '.{field}{suffix}'.format(
> + field=field.name,
> + suffix=next_name if next_name else ''
> + )
> + elif tp.code == gdb.TYPE_CODE_ARRAY:
> + # Get array field type.
> + target = tp.target()
> + tsize = target.sizeof
> + idx = int(offset // tsize)
> + next_name = self.member_by_offset(target, offset - idx * tsize)
> + idxname = idx_name(prev_name)
> + if idxname and idx in idxname:
> + idx = idxname[idx]
> + return '[{}]{}'.format(idx, next_name if next_name else '')
> + else:
> + return None
> +
> def eval(self, command):
> if not command:
> return None
> @@ -185,9 +277,23 @@ class _GDBDebugger(Debugger):
> raise gdb.GdbError('table argument empty')
> return ret
>
> + def detect_arch(self):
> + if hasattr(self, 'arch'):
> + return self.arch
> + target = str(gdb.execute('info target', False, True))
> + if re.match('.*x86-64.*', target, flags=re.DOTALL):
> + return 'x64'
> + elif re.match('.*aarch64.*', target, flags=re.DOTALL):
> + return 'arm64'
> + else:
> + return ''
> +
> def write(self, msg):
> gdb.write(msg)
>
> + def error(self, errmsg):
> + return gdb.GdbError(errmsg)
> +
> def check_libluajit(self):
> # XXX Fragile: Though connecting the callback looks bad,
> # it respects both Python 2 and Python 3 (see #4828).
> @@ -322,8 +428,26 @@ class _LLDBDebugger(Debugger):
> def lldb__getitem__(lldbval, key):
> if type(key) is lldb.value:
> key = int(key)
> + if type(key) is bool:
> + key = int(key)
> if type(key) is int:
> # Allow array access.
> + ltp = lldbval.sbvalue.GetType()
> + # XXX: LLDB in versions 17 - 19 can't use an array
> + # object as the initializer for `lldb.value` since
> + # `GetValue()` for it returns `None` leading to
> + # the invalid result. See
> + # https://github.com/llvm/llvm-project/pull/90144.
> + if (self.version < 17 or self.version > 19) or \
> + ltp.GetTypeClass() != lldb.eTypeClassArray:
> + pass
> + else:
> + ptr_tp = ltp.GetArrayElementType().GetPointerType()
> + lldbval = self._lldb_value_from_raw(
> + lldbval.sbvalue.GetLoadAddress(),
> + ptr_tp.GetByteSize(),
> + ptr_tp
> + )
> if key >= 0 and not lldbval.sbvalue.TypeIsPointerType():
> return lldb.value(
> lldbval.sbvalue.GetValueForExpressionPath('[%i]' % key)
> @@ -349,6 +473,9 @@ class _LLDBDebugger(Debugger):
> def lldb__gt__(lldbval, other):
> return int(lldbval) > int(other)
>
> + def lldb__index__(lldbval):
> + return int(lldbval)
> +
> def lldb__le__(lldbval, other):
> return int(lldbval) <= int(other)
>
> @@ -406,6 +533,7 @@ class _LLDBDebugger(Debugger):
> lldb.value.__ge__ = lldb__ge__
> lldb.value.__getitem__ = lldb__getitem__
> lldb.value.__gt__ = lldb__gt__
> + lldb.value.__index__ = lldb__index__
> lldb.value.__le__ = lldb__le__
> lldb.value.__lt__ = lldb__lt__
> lldb.value.__str__ = lldb__str__
> @@ -474,6 +602,9 @@ class _LLDBDebugger(Debugger):
> def cstr(self, strptr):
> return strptr.sbvalue.summary
>
> + def address(self, obj):
> + return lldb.value(obj.sbvalue.address_of)
> +
> def lookup_global(self, symbol):
> sbvalue = self.target.FindFirstGlobalVariable(symbol)
> tp = sbvalue.GetType()
> @@ -492,6 +623,46 @@ class _LLDBDebugger(Debugger):
> ptr_tp
> )
>
> + def member_by_offset(self, tp, offset, prev_name=None):
> + if isinstance(tp, str):
> + tp = self._dbgtype(tp)
> + assert offset < tp.GetByteSize(), 'offset is bigger than object size'
> + tp = tp.GetCanonicalType()
> + if tp.GetTypeClass() == lldb.eTypeClassStruct:
> + len_fields = tp.GetNumberOfFields()
> + for n_field in range(len_fields):
> + islast = n_field == (len_fields - 1)
> + field = tp.GetFieldAtIndex(n_field)
> + start_field = field.GetOffsetInBytes()
> + if not islast:
> + end_field = tp.GetFieldAtIndex(
> + n_field + 1
> + ).GetOffsetInBytes()
> + else:
> + end_field = tp.GetByteSize()
> + if start_field <= offset and offset < end_field:
> + next_name = self.member_by_offset(
> + field.GetType(),
> + offset - start_field,
> + prev_name=field.GetName()
> + )
> + return '.{field}{suffix}'.format(
> + field=field.GetName(),
> + suffix=next_name if next_name else ''
> + )
> + if tp.GetTypeClass() == lldb.eTypeClassArray:
>
Typo?: elif
>
> + # Get array field type.
> + target = tp.GetArrayElementType()
> + tsize = target.GetByteSize()
> + idx = int(offset // tsize)
> + next_name = self.member_by_offset(target, offset - idx * tsize)
> + idxname = idx_name(prev_name)
> + if idxname and idx in idxname:
> + idx = idxname[idx]
> + return '[{}]{}'.format(idx, next_name if next_name else '')
> + else:
> + return None
> +
> def eval(self, command):
> if not command:
> return None
> @@ -502,9 +673,23 @@ class _LLDBDebugger(Debugger):
> ret = frame.EvaluateExpression(command)
> return ret
>
> + def detect_arch(self):
> + if hasattr(self, 'arch'):
> + return self.arch
> + target = self.target.GetTriple().split('-')[0]
> + if target == 'x86_64':
> + return 'x64'
> + elif target == 'arm64' or target == 'aarch64':
> + return 'arm64'
> + else:
> + return ''
> +
> def write(self, msg):
> sys.stdout.write(msg)
>
> + def error(self, errmsg):
> + return Exception(errmsg)
> +
> def check_libluajit(self):
> # TODO: Implement postpone loading for LLDB too.
> return True
> @@ -997,6 +1182,86 @@ def J(g):
> return dbg.cast('jit_State *', dbg.cast('char *', g) - g_offset +
> J_offset)
>
>
> +# Matched `MMDEF(_)`.
> +MM_NAMES = [
> + 'index',
> + 'newindex',
> + 'gc',
> + 'mode',
> + 'eq',
> + 'len',
> + 'lt',
> + 'le',
> + 'concat',
> + 'call',
> + 'add',
> + 'sub',
> + 'mul',
> + 'div',
> + 'mod',
> + 'pow',
> + 'unm',
> + 'metatable',
> + 'tostring',
> + # TODO: depends on LJ_HASFFI, see `MMDEF_FFI(_)`.
> + 'new',
> + # TODO: depends on LJ_52 || LJ_HASFFI, see `MMDEF_PAIRS(_)`.
> + 'pairs',
> + 'ipairs',
> +]
> +
> +
> +GCROOT_MMNAME = 0
> +GCROOT_BASEMT = GCROOT_MMNAME + len(MM_NAMES)
> +GCROOT_IO_INPUT = GCROOT_BASEMT + i2notu32(LJ_T['NUMX']) + 1
> +GCROOT_IO_OUTPUT = GCROOT_IO_INPUT + 1
> +
> +
> +# Get the name of the index in the predefined arrays.
> +def idx_name(field_name):
> + # Don't use **{ to be compatible with Python 2.
> + gcroot = {}
> + gcroot.update({
> + i: 'GCROOT_MMNAME_' + MM_NAMES[i] for i in range(len(MM_NAMES))
> + })
> + gcroot.update({
> + i2notu32(LJ_T[k]) + GCROOT_BASEMT: 'GCROOT_BASEMT_' + k
> + for k in LJ_T.keys()
> + })
> + gcroot.update({
> + GCROOT_IO_INPUT: 'GCROOT_IO_INPUT',
> + GCROOT_IO_OUTPUT: 'GCROOT_IO_OUTPUT',
> + })
> + return {
> + # May be one of 2 slots depending on the result address.
> + 'ksimd': {
> + 0 * 2 + 0: 'LJ_KSIMD_ABS',
> + 0 * 2 + 1: 'LJ_KSIMD_ABS',
> + 1 * 2 + 0: 'LJ_KSIMD_NEG',
> + 1 * 2 + 1: 'LJ_KSIMD_NEG',
> + },
> + 'gcroot': gcroot,
> + }.get(field_name, None)
> +
> +
> +ggfname_cache = {}
> +
> +
> +# Get GG field name by given offset. Use in JIT dump.
> +def ggfname_by_offset(offset):
> + if offset in ggfname_cache:
> + return ggfname_cache[offset]
> +
> + field_path = dbg.member_by_offset('GG_State', offset)
> + if not field_path:
> + return None
> +
> + # Remove first '.'.
> + ggfname = 'offsetof(GG, {})'.format(field_path[1:])
> + ggfname_cache[offset] = ggfname
> + return ggfname
> +
> +
> def vm_state(g):
> return {
> i2notu32(0): 'INTERP',
> @@ -1087,6 +1352,555 @@ def lightudV(tv):
> return gcval(tv['gcr'])
>
>
> +# JIT engine.
> +
> +
> +IRS = [
> + # Guarded assertions.
> + 'LT',
> + 'GE',
> + 'LE',
> + 'GT',
> +
> + 'ULT',
> + 'UGE',
> + 'ULE',
> + 'UGT',
> +
> + 'EQ',
> + 'NE',
> +
> + 'ABC',
> + 'RETF',
> +
> + # Miscellaneous ops.
> + 'NOP',
> + 'BASE',
> + 'PVAL',
> + 'GCSTEP',
> + 'HIOP',
> + 'LOOP',
> + 'USE',
> + 'PHI',
> + 'RENAME',
> + 'PROF',
> +
> + # Constants.
> + 'KPRI',
> + 'KINT',
> + 'KGC',
> + 'KPTR',
> + 'KKPTR',
> + 'KNULL',
> + 'KNUM',
> + 'KINT64',
> + 'KSLOT',
> +
> + # Bit ops.
> + 'BNOT',
> + 'BSWAP',
> + 'BAND',
> + 'BOR',
> + 'BXOR',
> + 'BSHL',
> + 'BSHR',
> + 'BSAR',
> + 'BROL',
> + 'BROR',
> +
> + # Arithmetic ops. ORDER ARITH
> + 'ADD',
> + 'SUB',
> + 'MUL',
> + 'DIV',
> + 'MOD',
> + 'POW',
> + 'NEG',
> +
> + 'ABS',
> + 'LDEXP',
> + 'MIN',
> + 'MAX',
> + 'FPMATH',
> +
> + # Overflow-checking arithmetic ops.
> + 'ADDOV',
> + 'SUBOV',
> + 'MULOV',
> +
> + # Memory ops. A = array, H = hash, U = upvalue, F = field,
> + # S = stack.
> +
> + # Memory references.
> + 'AREF',
> + 'HREFK',
> + 'HREF',
> + 'NEWREF',
> + 'UREFO',
> + 'UREFC',
> + 'FREF',
> + 'STRREF',
> + 'LREF',
> +
> + # Loads and Stores. These must be in the same order.
> + 'ALOAD',
> + 'HLOAD',
> + 'ULOAD',
> + 'FLOAD',
> + 'XLOAD',
> + 'SLOAD',
> + 'VLOAD',
> +
> + 'ASTORE',
> + 'HSTORE',
> + 'USTORE',
> + 'FSTORE',
> + 'XSTORE',
> +
> + # Allocations.
> + 'SNEW',
> + 'XSNEW',
> + 'TNEW',
> + 'TDUP',
> + 'CNEW',
> + 'CNEWI',
> +
> + # Buffer operations.
> + 'BUFHDR',
> + 'BUFPUT',
> + 'BUFSTR',
> +
> + # Barriers.
> + 'TBAR',
> + 'OBAR',
> + 'XBAR',
> +
> + # Type conversions.
> + 'CONV',
> + 'TOBIT',
> + 'TOSTR',
> + 'STRTO',
> +
> + # Calls.
> + 'CALLN',
> + 'CALLA',
> + 'CALLL',
> + 'CALLS',
> + 'CALLXS',
> + 'CARG',
> +]
> +
> +
> +# Mode bits: Commutative, {Normal/Ref, Alloc, Load, Store},
> +# Non-weak guard. */
>
Typo: C comment end */
>
> +IRM_C = 0x10
> +IRM_A = 0x20
> +IRM_L = 0x40
> +IRM_S = 0x60
> +IRM_W = 0x80
> +
> +
> +# IR operand mode (2 bit).
> +IRM = [
> + 'ref',
> + 'lit',
> + 'cst',
> + '', # none
> +]
> +
> +
> +lj_ir_mode_ = None
> +
> +
> +def lj_ir_mode():
> + global lj_ir_mode_
> + if lj_ir_mode_:
> + return lj_ir_mode_
> + lj_ir_mode_ = dbg.lookup_global('lj_ir_mode')
> + return lj_ir_mode_
> +
> +
> +def ir_left(op):
> + return IRM[int(lj_ir_mode()[op] & 3)]
>
May be binary constant will be more clear? xxx & 0b0011
>
> +
> +
> +def ir_right(op):
> + return IRM[int(lj_ir_mode()[op] >> 2 & 3)]
>
May be binary constant will be more clear? (xxx & 0b1100) >> 2
>
> +
> +
> +def ir_mode(op):
> + mode = ''
> + ir_mode = int(lj_ir_mode()[op] ^ IRM_W)
>
>
> + if ir_mode == IRM_C:
> + mode = 'C'
> + elif ir_mode == IRM_A:
> + mode = 'A'
> + elif ir_mode == IRM_L:
> + mode = 'L'
> + elif ir_mode == IRM_S:
> + mode = 'S'
> + else:
> + mode = 'N'
>
>
> + mode += 'W' if ir_mode & IRM_W else ''
>
May be table with 16 items and comments will be more clear? E. g. return XXX[(lj_ir_mode()[op] & 0b11110000) >> 4]
And it will contain invalid values also.
# <flag bits in a comment>
XXX[0b0000] = ‘NW’ # Normal/Ref | !Non-weak guard
XXX[0b0001] = ‘CW’ # Commutative | !Non-weak guard
XXX[0b0011] = ‘Invalid’
...
XXX[0b1000] = ‘N’ # Normal/Ref | Non-weak guard
XXX[0b1001] = ‘C’ # Commutative | Non-weak guard
XXX[0b1011] = ‘Invalid’
...
>
> + return mode
>
>
> +
> +
> +IRTYPES = [
> + 'nil',
> + 'fal',
> + 'tru',
> + 'lud',
> + 'str',
> + 'p32',
> + 'thr',
> + 'pro',
> + 'fun',
> + 'p64',
> + 'cdt',
> + 'tab',
> + 'udt',
> + 'flt',
> + 'num',
> + 'i8 ',
> + 'u8 ',
> + 'i16',
> + 'u16',
> + 'int',
> + 'u32',
> + 'i64',
> + 'u64',
> + 'sfp',
> +]
> +
> +
> +IRT_NUM = 14
> +assert IRTYPES[IRT_NUM] == 'num', 'incorrect IRT_NUM definition'
> +
> +
> +IRFIELDS = [
> + 'str.len',
> + 'func.env',
> + 'func.pc',
> + 'func.ffid',
> + 'thread.env',
> + 'tab.meta',
> + 'tab.array',
> + 'tab.node',
> + 'tab.asize',
> + 'tab.hmask',
> + 'tab.nomm',
> + 'udata.meta',
> + 'udata.udtype',
> + 'udata.file',
> + 'cdata.ctypeid',
> + 'cdata.ptr',
> + 'cdata.int',
> + 'cdata.int64',
> + 'cdata.int64_4',
> +]
> +
> +
> +IRFPMS = [
> + 'floor',
> + 'ceil',
> + 'trunc',
> + 'sqrt',
> + 'exp2',
> + 'log',
> + 'log2',
> + 'other'
> +]
> +
> +
> +# Don't use *[ to be compatible with Python 2.
> +REGISTERS = {'x64': [
> + 'rax',
> + 'rcx',
> + 'rdx',
> + 'rbx',
> + 'rsp',
> + 'rbp',
> + 'rsi',
> + 'rdi',
> +] + [
> + 'r{}'.format(i) for i in range(8, 16) # r8 .. r15
> +] + [
> + 'xmm{}'.format(i) for i in range(0, 16) # xmm0 .. xmm15
> +], 'arm64': [
> + 'x{}'.format(i) for i in range(0, 31) # x0 .. x30
> +] + ['sp'] + [ # x31
> + 'd{}'.format(i) for i in range(0, 32) # d0 .. d31
> +]}
>
It seems, the ‘arm64’ registers are missed.
>
> +
> +
> +IR_CALLS = [
> + 'lj_str_cmp',
> + 'lj_str_find',
> + 'lj_str_new',
> + 'lj_strscan_num',
> + 'lj_strfmt_int',
> + 'lj_strfmt_num',
> + 'lj_strfmt_char',
> + 'lj_strfmt_putint',
> + 'lj_strfmt_putnum',
> + 'lj_strfmt_putquoted',
> + 'lj_strfmt_putfxint',
> + 'lj_strfmt_putfnum_int',
> + 'lj_strfmt_putfnum_uint',
> + 'lj_strfmt_putfnum',
> + 'lj_strfmt_putfstr',
> + 'lj_strfmt_putfchar',
> + 'lj_buf_putmem',
> + 'lj_buf_putstr',
> + 'lj_buf_putchar',
> + 'lj_buf_putstr_reverse',
> + 'lj_buf_putstr_lower',
> + 'lj_buf_putstr_upper',
> + 'lj_buf_putstr_rep',
> + 'lj_buf_puttab',
> + 'lj_buf_tostr',
> + 'lj_tab_new_ah',
> + 'lj_tab_new1',
> + 'lj_tab_dup',
> + 'lj_tab_clear',
> + 'lj_tab_newkey',
> + 'lj_tab_len',
> + 'lj_gc_step_jit',
> + 'lj_gc_barrieruv',
> + 'lj_mem_newgco',
> + 'lj_math_random_step',
> + 'lj_vm_modi',
> + 'log10',
> + 'exp',
> + 'sin',
> + 'cos',
> + 'tan',
> + 'asin',
> + 'acos',
> + 'atan',
> + 'sinh',
> + 'cosh',
> + 'tanh',
> + 'fputc',
> + 'fwrite',
> + 'fflush',
> + 'lj_vm_floor',
> + 'lj_vm_ceil',
> + 'lj_vm_trunc',
> + 'sqrt',
> + 'log',
> + 'lj_vm_log2',
> + 'pow',
> + 'atan2',
> + 'ldexp',
> + 'lj_vm_tobit',
> + 'softfp_add',
> + 'softfp_sub',
> + 'softfp_mul',
> + 'softfp_div',
> + 'softfp_cmp',
> + 'softfp_i2d',
> + 'softfp_d2i',
> + 'lj_vm_sfmin',
> + 'lj_vm_sfmax',
> + 'lj_vm_tointg',
> + 'softfp_ui2d',
> + 'softfp_f2d',
> + 'softfp_d2ui',
> + 'softfp_d2f',
> + 'softfp_i2f',
> + 'softfp_ui2f',
> + 'softfp_f2i',
> + 'softfp_f2ui',
> + 'fp64_l2d',
> + 'fp64_ul2d',
> + 'fp64_l2f',
> + 'fp64_ul2f',
> + 'fp64_d2l',
> + 'fp64_d2ul',
> + 'fp64_f2l',
> + 'fp64_f2ul',
> + 'lj_carith_divi64',
> + 'lj_carith_divu64',
> + 'lj_carith_modi64',
> + 'lj_carith_modu64',
> + 'lj_carith_powi64',
> + 'lj_carith_powu64',
> + 'lj_cdata_newv',
> + 'lj_cdata_setfin',
> + 'strlen',
> + 'memcpy',
> + 'memset',
> + 'lj_vm_errno',
> + 'lj_carith_mul64',
> + 'lj_carith_shl64',
> + 'lj_carith_shr64',
> + 'lj_carith_sar64',
> + 'lj_carith_rol64',
> + 'lj_carith_ror64',
> +]
> +
> +
> +def regname(reg_number):
> + if not hasattr(dbg, 'arch'):
> + dbg.arch = dbg.detect_arch()
> + return REGISTERS[dbg.arch][reg_number]
> +
> +
> +def litname_sload(mode):
> + modes_str = ''
> + modes_str += 'P' if mode & 0x1 else ''
> + modes_str += 'F' if mode & 0x2 else ''
> + modes_str += 'T' if mode & 0x4 else ''
> + modes_str += 'C' if mode & 0x8 else ''
> + modes_str += 'R' if mode & 0x10 else ''
> + modes_str += 'I' if mode & 0x20 else ''
> + return modes_str
> +
> +
> +def litname_xload(mode):
> + flags = ['-', 'R', 'V', 'RV', 'U', 'RU', 'VU', 'RVU']
>
Does we need a range check as in litname_bufhdr()?
>
> + return flags[mode]
> +
> +
> +def litname_conv(mode):
>
Does we need some range checking here?
>
> + IRCONV_DSH = 5
> + IRCONV_CSH = 12
> + IRCONV_SEXT = 0x800
> + IRCONV_SRCMASK = 0x1f
> + conv_str = '{to}.{frm}'.format(
> + to=IRTYPES[(mode >> IRCONV_DSH) & IRCONV_SRCMASK],
> + frm=IRTYPES[mode & IRCONV_SRCMASK]
> + )
> + conv_str += ' sext' if mode & IRCONV_SEXT else ''
> + num2int_mode = mode >> IRCONV_CSH
> + if num2int_mode == 2:
> + conv_str += ' index'
> + elif num2int_mode == 3:
> + conv_str += ' check'
> + return conv_str
> +
> +
> +def litname_irfield(mode):
> + if mode >= len(IRFIELDS):
> + return 'unknown irfield'
> + return IRFIELDS[mode]
> +
> +
> +def litname_fpm(mode):
> + if mode >= len(IRFPMS):
> + return 'unknown irfpm'
> + return IRFPMS[mode]
> +
> +
> +def litname_bufhdr(mode):
> + modes = ['RESET', 'APPEND']
> + if mode >= len(modes):
> + return 'unknown bufhdr mode'
> + return modes[mode]
> +
> +
> +def litname_tostr(mode):
> + modes = ['INT', 'NUM', 'CHAR']
> + if mode >= len(modes):
> + return 'unknown tostr mode'
> + return modes[mode]
> +
> +
> +IR_LITNAMES = {
> + 'SLOAD': litname_sload,
> + 'XLOAD': litname_xload,
> + 'CONV': litname_conv,
> + 'FLOAD': litname_irfield,
> + 'FREF': litname_irfield,
> + 'FPMATH': litname_fpm,
> + 'BUFHDR': litname_bufhdr,
> + 'TOSTR': litname_tostr
> +}
> +
> +# Additional flags.
> +IRT_MARK = 0x20 # Marker for misc. purposes.
> +IRT_ISPHI = 0x40 # Instruction is left or right PHI operand.
> +IRT_GUARD = 0x80 # Instruction is a guard.
> +# Masks.
> +IRT_TYPE = 0x1f
> +
> +RID_NONE = 0x80
> +RID_MASK = 0x7f
> +RID_INIT = (RID_NONE | RID_MASK)
> +RID_SINK = (RID_INIT - 1)
> +RID_SUNK = (RID_INIT - 2)
> +# Spill slot 0 means no spill slot has been allocated.
> +SPS_NONE = 0
> +
> +REF_BIAS = 0x8000
> +
> +TREF_SHIFT = 24
> +
> +TREF_REFMASK = 0x0000ffff
> +TREF_FRAME = 0x00010000
> +TREF_CONT = 0x00020000
> +# Snapshot flags and masks.
> +SNAP_FRAME = 0x010000
> +SNAP_SOFTFPNUM = 0x080000
> +
> +
> +def irt_type(t):
> + return dbg.cast('IRType', t['irt'] & IRT_TYPE)
> +
> +
> +def tref_type(tr):
> + return dbg.cast('IRType', (tr >> TREF_SHIFT) & IRT_TYPE)
> +
> +
> +def tref_ref(tr):
> + return int(tr & TREF_REFMASK)
> +
> +
> +def irt_ismarked(t):
> + return t['irt'] & IRT_MARK
>
I propose explicit bool cast (!= 0) here and below.
>
> +
> +
> +def irt_isphi(t):
> + return t['irt'] & IRT_ISPHI
> +
> +
> +def irt_isguard(t):
> + return t['irt'] & IRT_GUARD
> +
> +
> +def irt_toitype(irt):
> + t = irt_type(irt)
> + if LJ_DUALNUM and t > IRT_NUM:
> + return LJ_T['NUMX']
> + else:
> + return i2notu32(t)
> +
> +
> +def ir_kptr(ir):
> + irname = IRS[ir['o']]
> + assert irname == 'KPTR' or irname == 'KKPTR', 'wrong IR for ir_iptr()'
> + return mref('void *', dbg.cast('IRIns *',
> dbg.address(ir))[LJ_GC64]['ptr'])
> +
> +
> +def ir_kgc(ir):
> + irname = IRS[ir['o']]
> + assert irname == 'KGC', 'wrong IR for ir_kgc()'
> + return gcref(dbg.cast('IRIns *', dbg.address(ir))[LJ_GC64]['gcr'])
> +
> +
> +def ir_knum(ir):
> + irname = IRS[ir['o']]
> + assert irname == 'KNUM', 'wrong IR for ir_knum()'
> + return dbg.address(dbg.cast('IRIns *', dbg.address(ir))[1]['tv'])
> +
> +
> +def ir_kint64(ir):
> + irname = IRS[ir['o']]
> + assert irname == 'KINT64', 'wrong IR for ir_knum()'
> + return dbg.address(dbg.cast('IRIns *', dbg.address(ir))[1]['tv'])
> +
> +
> # Dumpers.
>
> # GCobj dumpers.
> @@ -1467,6 +2281,325 @@ def dump_func(func):
> return 'fast function #{}\n'.format(int(ffid))
>
>
> +# JIT dumpers.
> +
> +
> +def dump_call_func(trace, callop):
> + ctype = ''
> + if callop > 0:
> + ir = trace['ir'][REF_BIAS + callop]
> + if IRTYPES[irt_type(ir['t'])] == 'nil': # nil == CARG(func, ctype)
> + callop = int(ir['op1']) - REF_BIAS
> + cdt_idx_irk = trace['ir'][ir['op2']]
> + assert IRS[cdt_idx_irk['o']] == 'KINT', \
> + 'unexpected IR for ctype storage'
> + ctype_idx = cdt_idx_irk['i']
> + ctype = 'ctype: {}'.format(ctype_idx)
> +
> + func_str = ''
> + if callop < 0:
> + irk = trace['ir'][REF_BIAS + callop]
> + assert IRS[irk['o']] == 'KINT64', \
> + 'unexpected IR for FFI function storage'
> + func_addr = int(ir_kint64(irk)['u64'])
> + # TODO: Symbol demangling.
> + func_str = '[{:#x}]'.format(func_addr)
> + else:
> + func_str = '[{:04d}]'.format(callop)
> +
> + return func_str, ctype
> +
> +
> +def dump_call_args(trace, ins):
> + if ins < 0:
> + return '{{{}}}'.format(dump_irk(trace, ins))
> + else:
> + ir = trace['ir'][REF_BIAS + ins]
> + irname = IRS[ir['o']]
> + if irname == 'CARG':
> + last_arg = ''
> + args = dump_call_args(trace, int(ir['op1']) - REF_BIAS)
> + op2 = int(ir['op2']) - REF_BIAS
> + if op2 < 0:
> + last_arg = '{{{}}}'.format(dump_irk(trace, op2))
> + else:
> + last_arg = '{{{:04d}}}'.format(op2)
> + return args + ', ' + last_arg
> + else:
> + return '{{{:04d}}}'.format(ins)
> +
> +
> +# Special FP constant.
> +CONST_BIAS = 2 ** 52 + 2 ** 51
> +
> +
> +def dump_irk(trace, idx):
> + ref = idx + REF_BIAS
> + assert ref >= trace['nk'] and ref < REF_BIAS, 'bad constant in IR dump'
> + irins = trace['ir'][ref]
> + irname = IRS[irins['o']]
> + slot = ''
> + if irname == 'KSLOT':
> + slot = ' KSLOT: @{}'.format(int(irins['op2']))
> + irins = trace['ir'][irins['op1']]
> + irname = IRS[irins['o']]
> +
> + irtype = irins['t']
> + if irname == 'KPRI':
> + typename = typenames(irt_toitype(irtype))
> + # Trivial dump for primitives.
> + irk = tv_dumpers.get(
> + typename, dump_lj_tv_invalid # noqa: F821 # Generated.
> + )(0)
> + elif irname == 'KINT':
> + irk = 'integer {}'.format(dbg.cast('int32_t', irins['i']))
> + elif irname == 'KGC':
> + typename = typenames(irt_toitype(irtype))
> + irk = gco_dumpers.get(typename, dump_lj_gco_invalid)(ir_kgc(irins))
> + elif irname == 'KKPTR':
> + addr = ir_kptr(irins)
> + if addr == dbg.address(G(L())['nilnode']):
> + return '[g->nilnode]' + slot
> + irk = '[{}]'.format(strx64(addr))
> + elif irname == 'KPTR':
> + irk = '[{}]'.format(strx64(ir_kptr(irins)))
> + elif irname == 'KNULL':
> + irk = 'NULL'
> + elif irname == 'KNUM':
> + tv_num = ir_knum(irins)
> + if float(tv_num['n']) == CONST_BIAS:
> + return 'bias'
> + irk = dump_lj_tv_numx(tv_num)
> + elif irname == 'KINT64':
> + irk = 'int64_t {}'.format(dbg.cast(
> + 'int64_t', int(ir_kint64(irins)['u64'])
> + ))
> + else:
> + return 'Unknown IRK: ' + irname
> + return irk + slot
> +
> +
> +def dump_irins(irins, trace=None):
> + irop = int(irins['o'])
> + if irop >= len(IRS):
> + return 'INVALID'
> +
> + irname = IRS[irop]
> + leftop = ir_left(irop)
> + rightop = ir_right(irop)
> + irt = irins['t']
> + is_sinksunk = irins['r'] == RID_SINK or irins['r'] == RID_SUNK
> + flags = '{is_sinksunk}{is_marked}{is_guard}{is_phi}'.format(
> + # Sink flag should be the first to match sink slots during
> + # the dump of registers.
> + is_sinksunk='}' if is_sinksunk else ' ',
> + is_marked='!' if irt_ismarked(irt) else ' ',
> + is_guard='>' if irt_isguard(irt) else ' ',
> + is_phi='+' if irt_isphi(irt) else ' '
> + )
> +
> + if not trace:
> + g = G(L(None))
> + compiling = jit_state(g) != 'IDLE'
> + assert compiling, 'attempt to dump IR for J.cur trace in bad VM state'
> + trace = J(g)['cur']
> +
> + left = ''
> + right = ''
> + lisref = leftop == 'ref'
> + risref = rightop == 'ref'
> + op1 = int((irins['op1'] - REF_BIAS) if lisref else irins['op1'])
> + op2 = int((irins['op2'] - REF_BIAS) if risref else irins['op2'])
>
>
> +
> + skip_right = False
> + if re.match('CALL', irname):
> + ctype = ''
> + args = ''
> + if rightop == 'lit':
> + func = IR_CALLS[op2]
> + else:
> + func, ctype = dump_call_func(trace, op2)
> +
> + if op1 != -1:
> + args = dump_call_args(trace, int(op1))
> +
> + return '{flags} {type} {name:6} [{mode:2}] {f}({args}) {ct}\n'.format(
> + flags=flags,
> + name=irname,
> + mode=ir_mode(irop),
> + type=IRTYPES[irt_type(irt)],
> + ct=ctype,
> + args=args,
> + f=func,
> + )
> + elif irname == 'CNEW' and op2 == -1:
> + left = dump_irk(trace, op1)
> + skip_right = True
> + elif leftop:
> + if op1 < 0:
> + left = dump_irk(trace, op1)
> + elif leftop == 'cst':
> + idx = irins - dbg.address(trace['ir'][REF_BIAS])
> + left = dump_irk(trace, idx)
> + else:
> + left = ('{:04d}' if lisref else '#{:<3d}').format(op1)
> +
> + if rightop:
> + if rightop == 'lit':
> + litname = IR_LITNAMES.get(irname, None)
> + if litname:
> + # Try to handle `lj_ir_ggfload()`.
> + ggfname = None
> + if irname == 'FLOAD' and left == 'nil' \
> + and op2 >= len(IRFIELDS):
> + ggfname = ggfname_by_offset(op2 << 2)
> +
> + if ggfname:
> + right = ggfname
> + else:
> + right = litname(op2)
> + elif irname == 'UREFO' or irname == 'UREFC':
> + right = '#{:<3d}'.format(op2 >> 8)
> + else:
> + right = '#{:<3d}'.format(op2)
> + elif op2 < 0:
> + right = dump_irk(trace, op2)
> + else:
> + right = ('{:04d}').format(op2)
> +
> + typename = ''
> + if irname == 'LOOP':
> + typename = '---'
> + elif irname == 'NOP':
> + typename = ' '
> + else:
> + typename = IRTYPES[irt_type(irt)]
> +
> + return '{flags} {type} {name:6} [{mode:2}] {left:<9s} {right}\n'.format(
>
> + flags=flags,
> + name=irname,
> + mode=ir_mode(irop),
> + type=typename,
> + left=(leftop + ': ' + left) if leftop else '',
> + right=(rightop + ': ' + right) if rightop and not skip_right else '',
> + )
> +
> +
> +def dump_snap(trace, snapno, snap):
> + dump = 'SNAP #{:<3d} ['.format(snapno)
> + snap_map = dbg.address(trace['snapmap'][snap['mapofs']])
> + snap_entry_num = 0
> + for slot in range(0, snap['nslots']):
> + dump += ' '
> + snap_entry = int(snap_map[snap_entry_num])
> + if snap_entry_num < snap['nent'] and snap_entry >> TREF_SHIFT == slot:
> + snap_entry_num += 1
> + ref = int((snap_entry & TREF_REFMASK) - REF_BIAS)
> + if ref < 0:
> + if int(snap_entry) == 0x1057fff:
> + dump += '----'
> + continue
> + elif (snap_entry & TREF_CONT):
> + dump += 'contpc'
> + elif (snap_entry & TREF_FRAME):
> + dump += 'ftsz '
> + else:
> + dump += '{{{const}}}'.format(const=dump_irk(trace, ref))
> + elif snap_entry & SNAP_SOFTFPNUM:
> + dump += '{:04d}/{:04d}'.format(ref, ref + 1)
> + else:
> + dump += '{:04d}'.format(ref)
> +
> + if snap_entry & SNAP_FRAME:
> + dump += '|'
> + else:
> + dump += '----'
> +
> + dump += ' ]\n'
> + return dump
> +
> +
> +def dump_sink_slot(rid, spill, ins_number):
> + assert rid == RID_SINK or rid == RID_SUNK, 'incorrect rid in sink dump'
> + tp = 'sink' if rid == RID_SINK else 'sunk'
> + return '{{{}'.format(tp) if spill == RID_INIT or spill == SPS_NONE \
> + else '{{{:04d}'.format(int(ins_number - spill))
> +
> +
> +def dump_regsp(irins, ins_number):
> + rid = irins['r']
> + spill = irins['s']
> + if rid == RID_SINK or rid == RID_SUNK:
> + return dump_sink_slot(rid, spill, ins_number)
> + elif irins['prev'] > 255:
> + return '[{:#05x}]'.format(int(spill * 4))
> + elif rid < 128:
> + return regname(rid)
> + else:
> + return ''
> +
> +
> +def dump_trace(trace, flags):
> + dump = 'Trace {num} start\n\tproto: {start_pt}\n\tBC:
> {start_bc}\n'.format(
> + num=trace['traceno'],
> + start_pt=gcref(trace['startpt']),
> + start_bc=mref('BCIns *', trace['startpc']),
> + )
> +
> + nins = trace['nins'] - REF_BIAS
> + dump += '---- TRACE IR\n'
> + nsnap = 0
> + snap = trace['snap'][nsnap]
> + snapref = snap['ref']
> + for irnum in range(1, nins):
> + irref = REF_BIAS + irnum
> + if 's' in flags and irref >= snapref and nsnap < trace['nsnap']:
> + dump += '.... '
> + if 'r' in flags:
> + dump += ' ' * 7
> + dump += dump_snap(trace, nsnap, snap)
> + nsnap += 1
> + snap = trace['snap'][nsnap]
> + snapref = snap['ref']
> + dump += '{:04d} '.format(irnum)
> + if 'r' in flags:
> + dump += '{:>7}'.format(dump_regsp(trace['ir'][irref], irnum))
> + dump += dump_irins(trace['ir'][irref], trace)
> + return dump
> +
> +
> +def dump_tref(tref):
> + return '[{F}{C}] {tp} {ref:#x}'.format(
> + F='F' if tref & TREF_FRAME else ' ',
> + C='C' if tref & TREF_CONT else ' ',
> + tp=IRTYPES[tref_type(tref)],
> + ref=tref_ref(tref)
> + )
> +
> +
> +def dump_jslots(coroutine):
> + lstate = L(None)
> + g = G(lstate or coroutine)
> + j = J(g)
> +
> + dump = ''
> + maxslot = j['baseslot'] + j['maxslot']
> + first_base_slot = 1 + LJ_FR2
> + for n in reversed(range(first_base_slot, maxslot)):
> + tref = j['slot'][n]
> + ref = tref_ref(tref)
> + address = dbg.address(tref)
> + dump += '{addr} {nslot:04d} {base:1s} {tref}{const}\n'.format(
> + addr=address,
> + base='B' if address == j['base'] else ' ',
> + nslot=n,
> + tref=dump_tref(tref),
> + const=' ' + dump_irk(j['cur'], ref - REF_BIAS)
> + if ref != 0 and ref < REF_BIAS else ''
> + )
> + return dump
> +
> +
> # Extension commands. ############################################
>
>
> @@ -1600,6 +2733,42 @@ error message occurs.
> dbg.write('{}\n'.format(dump_gcobj(gcobj)))
>
>
> +class LJDumpIR(dbg.LJBase):
> + '''
> +lj-ir <IRIns *>
> +
> +The command receives a pointer to <ir> (IRIns address) and dumps
> +the IR type and some info related to it. The format is similar to
> +the `jit.dump` tool but also provides information about IR mode and
> +operands modes.
> +
> +For the list of IR names and modes (operand types), see:
> + https://github.com/tarantool/tarantool/wiki/LuaJIT-SSA-IR.
> + '''
> +
> + def execute(self, arg):
> + dbg.write('{}'.format(dump_irins(dbg.cast('IRIns *', dbg.eval(arg)))))
> +
> +
> +class LJDumpJSlots(dbg.LJBase):
> + '''
> +lj-jslots [<lua_State *>]
> +
> +The command receives an optional lua_State address and dumps the
> +slots of JIT stack map:
> +
> +<slot ptr> <slot number> [<FRAME|CONTINUATION>] <IR reference>
> +
> +The lua_State pointer is optional to help in finding the VM's JIT state
> +when there is no coroutine to be inspected in the debugged frame.
> + '''
> +
> + def execute(self, arg):
> + dbg.write('{}'.format(
> + dump_jslots(dbg.cast('lua_State *', dbg.eval(arg)))
> + ))
> +
> +
> class LJDumpProto(dbg.LJBase):
> '''
> lj-proto <GCproto *>
> @@ -1784,19 +2953,44 @@ error message occurs.
> dbg.write('{}\n'.format(dump_tvalue(tv)))
>
>
> +class LJDumpTrace(dbg.LJBase):
> + '''
> +lj-trace [/FLAGS] <GCtrace *>
> +
> +The command receives a pointer to <trace> (IRIns address) and dumps
> +its number, IRs, and information about start location. The format is
> +similar to the `jit.dump` tool but also provides information about
> +IR mode and operands modes.
> +
> +Trace may be preceded with /FLAGS:
> +* r: Dump registers associated with IR, if any.
> +* s: Dump snapshots for the trace.
> + '''
> +
> + def execute(self, arg):
> + arg, flags = dbg.extract_flags(arg, 'rs')
> + dbg.write('{}'.format(dump_trace(
> + dbg.cast('GCtrace *', dbg.eval(arg)),
> + flags
> + )))
> +
> +
> def load(event=None):
> dbg.initialize_extension({
> - 'lj-arch': LJDumpArch,
> - 'lj-bc': LJDumpBC,
> - 'lj-func': LJDumpFunc,
> - 'lj-gc': LJGC,
> - 'lj-gco': LJDumpGCobj,
> - 'lj-proto': LJDumpProto,
> - 'lj-stack': LJDumpStack,
> - 'lj-state': LJState,
> - 'lj-str': LJDumpString,
> - 'lj-tab': LJDumpTable,
> - 'lj-tv': LJDumpTValue,
> + 'lj-arch': LJDumpArch,
> + 'lj-bc': LJDumpBC,
> + 'lj-func': LJDumpFunc,
> + 'lj-gc': LJGC,
> + 'lj-gco': LJDumpGCobj,
> + 'lj-ir': LJDumpIR,
> + 'lj-jslots': LJDumpJSlots,
> + 'lj-proto': LJDumpProto,
> + 'lj-stack': LJDumpStack,
> + 'lj-state': LJState,
> + 'lj-str': LJDumpString,
> + 'lj-tab': LJDumpTable,
> + 'lj-trace': LJDumpTrace,
> + 'lj-tv': LJDumpTValue,
> })
>
>
> diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py
> b/test/tarantool-debugger-tests/debug-extension-tests.py
> index 7e8ea5a2..76543daa 100644
> --- a/test/tarantool-debugger-tests/debug-extension-tests.py
> +++ b/test/tarantool-debugger-tests/debug-extension-tests.py
> @@ -46,7 +46,9 @@ else:
> RX_ADDR = r'0x[a-f0-9]+'
> RX_HASH = RX_ADDR # The same pattern for hexademic values.
> RX_BCN = r'00\d\d'
> +RX_IRN = RX_BCN # The same as for the bytecodes.
> RX_FRAME = r'\[(S|\s)(B|\s)(T|\s)(M|\s)\]'
> +RX_IRREF = r'0x\d\d\d\d'
>
>
> def persist(data):
> @@ -101,6 +103,9 @@ IS_GC64 = execute_process([
> LUAJIT_BINARY, '-e', "print(require('ffi').abi('gc64'))"
> ]).strip() == 'true'
>
> +# Regexp for pointer type in IR.
> +RX_P = 'p64' if IS_GC64 else 'p32'
> +
> # If it is the guaranteed DUALNUM build (for example, on aarch64),
> # we use this regexp for the guaranteed 'integer' check and
> # 'number' for single-number build.
> @@ -108,6 +113,18 @@ RX_INT = r'integer' if IS_DUALNUM else r'number'
> RX_ISDUALNUM = r'True' if IS_DUALNUM else r'False'
>
>
> +# Assume not cross-platform debugging.
> +machine = os.uname().machine
> +if machine == 'x86_64':
> + RX_GPR = r'r\w\w'
> + RX_FPR = r'xmm\d+'
> +elif machine == 'arm64' or machine == 'aarch64':
> + RX_GPR = r'x\d+'
> + RX_FPR = r'd\d+'
> +else:
> + raise Exception('Unknown archeticture in testing')
> +
> +
> class TestCaseBase(unittest.TestCase):
> @classmethod
> def construct_cmds(cls):
> @@ -193,6 +210,16 @@ def mref(arg, tp):
> return '((' + tp + '*)(' + arg + ').ptr32)'
>
>
> +def gcref(arg):
> + if SUPPORT_MACRO_EXPAND:
> + return 'gcref(' + arg + ')'
> + else:
> + if IS_GC64:
> + return '(' + arg + ').gcptr64'
> + else:
> + return '(' + arg + ').gcptr32'
> +
> +
> class TestLoad(TestCaseBase):
> extension_cmds = ''
> location = 'lj_cf_print'
> @@ -203,11 +230,14 @@ class TestLoad(TestCaseBase):
> r'lj-func command initialized\n'
> r'lj-gc command initialized\n'
> r'lj-gco command initialized\n'
> + r'lj-ir command initialized\n'
> + r'lj-jslots command initialized\n'
> r'lj-proto command initialized\n'
> r'lj-stack command initialized\n'
> r'lj-state command initialized\n'
> r'lj-str command initialized\n'
> r'lj-tab command initialized\n'
> + r'lj-trace command initialized\n'
> r'lj-tv command initialized\n'
> r'LuaJIT debug extension is successfully loaded'
> )
> @@ -473,6 +503,341 @@ class TestLJBC(TestCaseBase):
> )
>
>
> +# JIT engine.
> +
> +
> +class TestLJTraceBase(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'jit.opt.start("hotloop=1")\n'
> + 'for _ = 1, 4 do end\n'
> + 'print()\n'
> + )
> + pattern = (
> + r'Trace 1 start\n'
> + r'\t*proto: ' + RX_ADDR + r'\n' +
> + r'\t*BC: ' + RX_ADDR + r'\n' +
> + r'---- TRACE IR\n' +
> + RX_IRN + r'\s+ int SLOAD \[N \] lit: #[12] lit: C?I\n' +
> + RX_IRN + r'\s+ \+ int ADD \[C \] ref: ' + RX_IRN +
> + r' ref: integer 1\n' +
> + RX_IRN + r'\s+ > int LE \[N \] ref: ' + RX_IRN +
> + r' ref: integer 4\n' +
> + RX_IRN + r'\s+ > --- LOOP \[N \]\s*\n' +
> + RX_IRN + r'\s+ \+ int ADD \[C \] ref: ' + RX_IRN +
> + r' ref: integer 1\n' +
> + RX_IRN + r'\s+ > int LE \[N \] ref: ' + RX_IRN +
> + r' ref: integer 4\n' +
> + RX_IRN + r'\s+ int PHI \[S \] ref: ' + RX_IRN + r' ref: ' +
> + RX_IRN + r'\n' +
> + RX_IRN + r'\s+ NOP \[N \]\s*\n'
> + )
> +
> +
> +# Check the IR enumeration correcness by test the lowest (LT) and
> +# the highest (CARG) IRs. Also, checks CALL* occasionally.
> +class TestLJTraceIRRange(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'local ffi = require("ffi")\n'
> + 'ffi.cdef[[int getpid(int, int);]]\n' # Use argument for testing.
> + 'jit.opt.start("hotloop=1")\n'
> + 'for i = 1, 4 do\n'
> + ' if i < 100 then\n' # LT.
> + ' ffi.C.getpid(i, 1LL)\n' # CARG and CALLXS.
> + ' end\n'
> + 'end\n'
> + 'print()\n'
> + )
> + # IRs from variant part of the trace.
> + pattern = (
> + RX_IRN + r'\s+ > int LT \[N \] ref: ' +
> + RX_IRN + r' ref: integer 100\n' +
> + RX_IRN + r'\s+ nil CARG \[N \] ref: ' +
> + RX_IRN + r' ref: integer 1\n' +
> + RX_IRN + r'\s+ int CALLXS \[S \] \[' + RX_ADDR +
> + r'\]\(\{' + RX_IRN + r'\}, \{integer 1\}\)'
> + )
> +
> +
> +# Test /rs flags.
> +class TestLJTraceFlags(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace /rs ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'jit.opt.start("hotloop=1")\n'
> + 'local r = 0.1\n'
> + 'for i = 1, 4 do\n'
> + ' r = i + r\n'
> + 'end\n'
> + 'print()\n'
> + )
> + # IRs and snapshot from variant part of the trace.
> + pattern = (
> + RX_IRN + r'\s+' + RX_FPR + r'\s* \+ num ADD.*\n' +
> + RX_IRN + r'\s+' + RX_GPR + r'\s* \+ int ADD.*\n' +
> + r'\.\.\.\.\s* SNAP #\d \[ (---- )*' + RX_IRN + r' \]'
> + )
> +
> +
> +class TestLJIRConst(TestCaseBase):
> + location = 'trace_stop'
> +
> + # No narrowing of 42.
> + if IS_DUALNUM:
> + # KNUM occupies 2 slots.
> + _knum_irnum = '6'
> + _kgc_irnum = '8' if IS_GC64 else '7'
> + _kptr_irnum = '10' if IS_GC64 else '8'
> + else:
> + # KNUM occupies 2 slots.
> + _knum_irnum = '8'
> + _kgc_irnum = '10' if IS_GC64 else '9'
> + _kptr_irnum = '12' if IS_GC64 else '10'
> + extension_cmds = (
> + 'n\n' # Load J.
> + 'lj-ir &J->cur.ir[0x8000 - 0]\n'
> + 'lj-ir &J->cur.ir[0x8000 - 1]\n'
> + 'lj-ir &J->cur.ir[0x8000 - 2]\n'
> + 'lj-ir &J->cur.ir[0x8000 - 3]\n'
> + 'lj-ir &J->cur.ir[0x8000 - 4]\n'
> + # Skip non-DUALNUM narrowed value.
> + 'lj-ir &J->cur.ir[0x8000 - ' + _knum_irnum + ']\n'
> + 'lj-ir &J->cur.ir[0x8000 - ' + _kgc_irnum + ']\n'
> + 'lj-ir &J->cur.ir[0x8000 - ' + _kptr_irnum + ']\n'
> + )
> + lua_script = (
> + 'jit.opt.start("hotloop=1")\n'
> + 'local function trace(x)\n'
> + ' return x + 42, x + 0.5, x .. "1"\n'
> + 'end\n'
> + 'trace(1)\n'
> + 'trace(1)\n'
> + )
> + pattern = (
> + RX_P + r' BASE.*\n' +
> + r'\s* nil KPRI.*\n'
> + r'\s* fal KPRI.*\n'
> + r'\s* tru KPRI.*\n'
> + r'\s* int KINT.*cst: integer 42\s*\n'
> + r'\s* num KNUM.*cst: number 0.5\s*\n'
> + r'\s* str KGC.*cst: string "1".*\n' +
> + r'\s*' + RX_P + r' KPTR.*cst: \[' + RX_ADDR + r'\]'
> + )
> +
> +
> +class TestLJIRFloadNeg(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'jit.opt.start("hotloop=1")\n'
> + 'local function trace(a)\n'
> + ' local x = -a\n'
> + ' return x\n'
> + 'end\n'
> + 'trace(1.1)\n'
> + 'trace(1.1)\n'
> + 'print()\n'
> + )
> + pattern = (
> + r'num FLOAD .* ref: nil lit: offsetof\(GG, J\.ksimd\[LJ_KSIMD_NEG\]\)'
> + )
> +
> +
> +class TestLJIRFloadAbs(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'jit.opt.start("hotloop=1")\n'
> + 'local math_abs = math.abs\n'
> + 'local function trace(a)\n'
> + ' local x = math_abs(a)\n'
> + ' return x\n'
> + 'end\n'
> + 'trace(1)\n'
> + 'trace(1)\n'
> + 'print()\n'
> + )
> + pattern = (
> + r'num FLOAD .* ref: nil lit: offsetof\(GG, J\.ksimd\[LJ_KSIMD_ABS\]\)'
> + )
> +
> +
> +# XXX: Implemented only for GC64 in LuaJIT until backporting the
> +# corresponding commit.
> +if IS_GC64:
> + class TestLJIRFloadGCRootBaseMT(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'jit.opt.start("hotloop=1")\n'
> + 'local function trace(a)\n'
> + 'local x = a.sub(1, 2)\n'
> + ' return x\n'
> + 'end\n'
> + 'trace("12")\n'
> + 'trace("12")\n'
> + 'print()\n'
> + )
> + pattern = (
> + r'tab FLOAD .* ref: nil lit: '
> + r'offsetof\(GG, g\.gcroot\[GCROOT_BASEMT_STR\]\.gcptr64\)'
> + )
> +
> + class TestLJIRFloadGCRootIO(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'jit.opt.start("hotloop=1")\n'
> + 'local io_flush = io.flush\n'
> + 'local function trace()\n'
> + ' io_flush()\n'
> + 'end\n'
> + 'trace()\n'
> + 'trace()\n'
> + 'print()\n'
> + )
> + pattern = (
> + r'udt FLOAD .* ref: nil lit: '
> + r'offsetof\(GG, g\.gcroot\[GCROOT_IO_OUTPUT\]\.gcptr64\)'
> + )
> +
> +
> +# Some IRs related to tables.
> +class TestLJIRTable(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'jit.opt.start("hotloop=1")\n'
> + 'local function trace(t)\n'
> + ' t.a = nil\n'
> + ' t.b = 1\n'
> + ' return t\n'
> + 'end\n'
> + 'trace({a = 1})\n'
> + 'trace({a = 1})\n'
> + 'print()\n'
> + )
> + pattern = (
> + r'(?s)int FLOAD .* tab\.hmask\n'
> + r'.*' + RX_P + r' FLOAD .* tab\.node\n'
> + r'.*' + RX_P + r' HREFK .* string "a" @ ' + RX_ADDR +
> + r' KSLOT: @\d\n'
> + r'.*' + RX_P + r' HREF .* string "b" @ ' + RX_ADDR + r'\s*\n'
> + r'.*' + RX_P + r' EQ .* \[g->nilnode\]'
> + )
> +
> +
> +class TestLJIRUref(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'jit.opt.start("hotloop=1")\n'
> + 'local uv = 0\n'
> + 'local function trace(a)\n'
> + ' uv = a\n'
> + ' return uv\n'
> + 'end\n'
> + 'trace(1)\n'
> + 'trace(1)\n'
> + 'print()\n'
> + )
> + pattern = r'UREFO .* lit: #0'
> +
> +
> +# Check border values (that always avalable) of CALL IRs.
> +class TestLJIRCall(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'local ffi = require("ffi")\n'
> + 'jit.opt.start("hotloop=1")\n'
> + 'local function trace(a, b)\n'
> + ' return a < b, ffi.errno()\n'
> + 'end\n'
> + 'trace("abc", "abd")\n'
> + 'trace("abc", "abd")\n'
> + 'print(1)\n'
> + )
> + pattern = (
> + r'(?s)int CALLN .* '
> + r'lj_str_cmp\(\{' + RX_IRN + r'\}, \{' + RX_IRN + r'\}\)'
> + r'.*int CALLS .* lj_vm_errno\(\)'
> + )
> +
> +
> +# Test ffi call with ctype stored in CARG.
> +class TestLJIRCallXSCType(TestCaseBase):
> + location = 'lj_cf_print'
> + extension_cmds = (
> + 'n\n' # Load L.
> + 'lj-trace ' + gcref('((GG_State *)L)->J->trace[1]')
> + )
> + lua_script = (
> + 'local ffi = require("ffi")\n'
> + 'ffi.cdef[[int printf(const char *fmt, ...);]]\n'
> + 'jit.opt.start("hotloop=1")\n'
> + 'local function trace()\n'
> + ' local t = ffi.C.printf("")\n'
> + ' return t\n'
> + 'end\n'
> + 'trace()\n'
> + 'trace()\n'
> + 'print()\n'
> + )
> + pattern = r'int CALLXS .* [' + RX_ADDR + r'\]\(.*\) ctype: \d+'
> +
> +
> +class TestLJJSlotsBase(TestCaseBase):
> + location = 'trace_stop'
> + extension_cmds = (
> + 'n\n' # Load J.
> + 'lj-jslots J->L\n'
> + )
> + lua_script = (
> + 'jit.opt.start("hotloop=1")\n'
> + 'for _ = 1, 4 do end\n'
> + )
> + pattern = (
> + r'(?s)(.*' +
> + RX_ADDR + ' ' + RX_IRN + r' (B|\s) \[(F|\s)(C|\s)\] \w\w\w ' +
> + RX_IRREF +
> + r'.*)+'
> + )
> +
> +
> for test_cls in TestCaseBase.__subclasses__():
> test_cls.test = lambda self: self.check()
>
> --
> 2.54.0
>
[-- Attachment #2: Type: text/html, Size: 53767 bytes --]
next prev parent reply other threads:[~2026-06-28 1:03 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-25 20:29 [Tarantool-patches] [PATCH luajit 0/3] Extend debug extension Sergey Kaplun via Tarantool-patches
2026-06-25 20:29 ` [Tarantool-patches] [PATCH luajit 1/3] dbg: introduce lj-ir, lj-jslots, lj-trace dumpers Sergey Kaplun via Tarantool-patches
2026-06-28 1:03 ` Evgeniy Temirgaleev via Tarantool-patches [this message]
2026-06-25 20:29 ` [Tarantool-patches] [PATCH luajit 2/3] dbg: introduce lj-ctype command, extend cdata dump Sergey Kaplun via Tarantool-patches
2026-06-25 20:29 ` [Tarantool-patches] [PATCH luajit 3/3] test: add verbose mode for debug extension tests Sergey Kaplun via Tarantool-patches
2026-06-28 1:31 ` Evgeniy Temirgaleev via Tarantool-patches
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=1782608610.240669306@f725.i.mail.ru \
--to=tarantool-patches@dev.tarantool.org \
--cc=e.temirgaleev@tarantool.org \
--cc=skaplun@tarantool.org \
--subject='Re: [Tarantool-patches] [PATCH luajit 1/3] dbg: introduce lj-ir, lj-jslots, lj-trace dumpers' \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox