Tarantool development patches archive
 help / color / mirror / Atom feed
From: Sergey Kaplun via Tarantool-patches <tarantool-patches@dev.tarantool.org>
To: Sergey Bronnikov <sergeyb@tarantool.org>,
	Evgeniy Temirgaleev <e.temirgaleev@tarantool.org>
Cc: tarantool-patches@dev.tarantool.org
Subject: [Tarantool-patches] [PATCH luajit 1/3] dbg: introduce lj-ir, lj-jslots, lj-trace dumpers
Date: Thu, 25 Jun 2026 23:29:01 +0300	[thread overview]
Message-ID: <20260625202903.3157425-2-skaplun@tarantool.org> (raw)
In-Reply-To: <20260625202903.3157425-1-skaplun@tarantool.org>

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:
+            # 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. */
+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)]
+
+
+def ir_right(op):
+    return IRM[int(lj_ir_mode()[op] >> 2 & 3)]
+
+
+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 ''
+    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
+]}
+
+
+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']
+    return flags[mode]
+
+
+def litname_conv(mode):
+    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
+
+
+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


  reply	other threads:[~2026-06-25 20:30 UTC|newest]

Thread overview: 4+ 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 ` Sergey Kaplun 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

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=20260625202903.3157425-2-skaplun@tarantool.org \
    --to=tarantool-patches@dev.tarantool.org \
    --cc=e.temirgaleev@tarantool.org \
    --cc=sergeyb@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